diff --git a/Tiku.Api/Configuration/ObservabilityExtensions.cs b/Tiku.Api/Configuration/ObservabilityExtensions.cs index dfee121..7d3e216 100644 --- a/Tiku.Api/Configuration/ObservabilityExtensions.cs +++ b/Tiku.Api/Configuration/ObservabilityExtensions.cs @@ -30,7 +30,7 @@ internal static class ObservabilityExtensions .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddMeter(DatabasePerformanceTelemetry.MeterName, WorkerTelemetry.MeterName, - AuthorizationCacheTelemetry.MeterName, "Tiku.Security.Redis", "Npgsql") + AuthorizationCacheTelemetry.MeterName, "Tiku.Security.Redis", "Tiku.Learning", "Npgsql") .ApplyIf(hasOtlpEndpoint, builder => builder.AddOtlpExporter(options => options.Endpoint = endpointUri!))); return services; diff --git a/Tiku.Api/Contracts/LearningDtos.cs b/Tiku.Api/Contracts/LearningDtos.cs index 572a9b4..b6054de 100644 --- a/Tiku.Api/Contracts/LearningDtos.cs +++ b/Tiku.Api/Contracts/LearningDtos.cs @@ -172,10 +172,14 @@ public sealed class SubmitPracticeSessionDto [Required] public Guid PracticeSessionId { get; set; } - public PracticeSessionFilter ToFilter() - { - return new PracticeSessionFilter(PracticeSessionId); - } + [Range(1, long.MaxValue)] + public long ExpectedSessionVersion { get; set; } + + [Required, StringLength(200, MinimumLength = 1)] + public string IdempotencyKey { get; set; } = string.Empty; + + public SubmitPracticeSessionCommand ToCommand() => + new(PracticeSessionId, ExpectedSessionVersion, IdempotencyKey); } /// @@ -190,9 +194,27 @@ public sealed class SubmitAnswerDto public Guid SessionQuestionId { get; set; } /// - /// 已选选项。 + /// 客户端读取会话时获得的版本。 /// - public IReadOnlyCollection? SelectedOptions { get; set; } + [Range(1, long.MaxValue)] + public long ExpectedSessionVersion { get; set; } + + /// + /// 客户端在本会话内单调递增的序列。 + /// + [Range(1, long.MaxValue)] + public long ClientSequence { get; set; } + + /// + /// 网络重试幂等键。 + /// + [Required, StringLength(200, MinimumLength = 1)] + public string IdempotencyKey { get; set; } = string.Empty; + + /// + /// 已选选项的零基索引。 + /// + public IReadOnlyCollection? SelectedOptionIndices { get; set; } /// /// 文字答案。 @@ -200,18 +222,15 @@ public sealed class SubmitAnswerDto [StringLength(10000)] public string? AnswerText { get; set; } - /// - /// 主观题自评是否正确。 - /// - public bool? SelfJudgedCorrect { get; set; } - public SubmitAnswerCommand ToCommand() { return new SubmitAnswerCommand( SessionQuestionId, - SelectedOptions, - AnswerText, - SelfJudgedCorrect); + ExpectedSessionVersion, + ClientSequence, + IdempotencyKey, + SelectedOptionIndices, + AnswerText); } } diff --git a/Tiku.Api/Controllers/LearningController.cs b/Tiku.Api/Controllers/LearningController.cs index f12522d..5eaea24 100644 --- a/Tiku.Api/Controllers/LearningController.cs +++ b/Tiku.Api/Controllers/LearningController.cs @@ -83,7 +83,7 @@ public sealed class LearningController( { return Ok(await learningActivityService.SubmitPracticeSessionAsync( ResolveActor(), - request.ToFilter(), + request.ToCommand(), cancellationToken)); } diff --git a/Tiku.Api/Middleware/ExceptionHandlingMiddleware.cs b/Tiku.Api/Middleware/ExceptionHandlingMiddleware.cs index a3404e9..a64dcec 100644 --- a/Tiku.Api/Middleware/ExceptionHandlingMiddleware.cs +++ b/Tiku.Api/Middleware/ExceptionHandlingMiddleware.cs @@ -505,7 +505,11 @@ public sealed class ExceptionHandlingMiddleware( { return code switch { - "no_practice_questions" or "practice_session_empty" => StatusCodes.Status409Conflict, + "no_practice_questions" or "practice_session_empty" or + "idempotency_conflict" or "practice_session_version_conflict" or + "practice_client_sequence_conflict" or "practice_answer_conflict" or + "practice_submission_conflict" or "practice_session_not_active" or + "practice_session_expired" => StatusCodes.Status409Conflict, _ => StatusCodes.Status400BadRequest }; } diff --git a/Tiku.Application/Learning/ILearningActivityService.cs b/Tiku.Application/Learning/ILearningActivityService.cs index 3e9b5d7..cde565a 100644 --- a/Tiku.Application/Learning/ILearningActivityService.cs +++ b/Tiku.Application/Learning/ILearningActivityService.cs @@ -93,7 +93,7 @@ public interface ILearningActivityService Task SubmitPracticeSessionAsync( LearningActor actor, - PracticeSessionFilter filter, + SubmitPracticeSessionCommand command, CancellationToken cancellationToken = default); Task GetPracticeSessionReportAsync( diff --git a/Tiku.Application/Learning/LearningActivityModels.cs b/Tiku.Application/Learning/LearningActivityModels.cs index 4148c32..f24d39e 100644 --- a/Tiku.Application/Learning/LearningActivityModels.cs +++ b/Tiku.Application/Learning/LearningActivityModels.cs @@ -11,9 +11,16 @@ public sealed record LearningList(IReadOnlyCollection Items); public sealed record SubmitAnswerCommand( Guid SessionQuestionId, - IReadOnlyCollection? SelectedOptions, - string? AnswerText, - bool? SelfJudgedCorrect); + long ExpectedSessionVersion, + long ClientSequence, + string IdempotencyKey, + IReadOnlyCollection? SelectedOptionIndices, + string? AnswerText); + +public sealed record SubmitPracticeSessionCommand( + Guid PracticeSessionId, + long ExpectedSessionVersion, + string IdempotencyKey); public sealed record QuestionActionCommand(QuestionLocator Locator, bool? Favorite); @@ -112,10 +119,13 @@ public sealed record AnswerRecordItem( Guid Id, Guid SessionQuestionId, Guid PracticeSessionId, - JsonElement SelectedOptions, + JsonElement SelectedOptionIndices, string? AnswerText, - bool? IsCorrect, - DateTimeOffset AnsweredAt); + string Status, + int Revision, + long ClientSequence, + long SessionVersion, + DateTimeOffset AcceptedAt); public sealed record FavoriteQuestionItem( Guid QuestionReferenceId, @@ -169,7 +179,9 @@ public sealed record PracticeSessionItem( DateTimeOffset? FinishedAt, DateTimeOffset? ExpiresAt, JsonElement Metadata, - string Status); + PracticeSessionStatus Status, + long Version, + long LastClientSequence); public sealed record PracticeSessionDetailItem( PracticeSessionItem Session, @@ -187,8 +199,7 @@ public sealed record PracticeSessionQuestionItem( JsonElement Tags, Guid? VersionId, string? Content, - JsonElement Options, - string? Explanation); + JsonElement Options); public sealed record PracticeSessionReportItem( Guid Id, @@ -207,6 +218,11 @@ public sealed record PracticeSessionReportItem( int DurationSeconds, DateTimeOffset? StartedAt, DateTimeOffset SubmittedAt, + PracticeReportStatus Status, + int Version, + bool IsFinal, + int PendingReviewCount, + int ScoringVersion, JsonElement SectionStats, JsonElement QuestionResults, JsonElement WrongQuestionIds, diff --git a/Tiku.Application/Learning/QuestionGrading.cs b/Tiku.Application/Learning/QuestionGrading.cs new file mode 100644 index 0000000..9272e59 --- /dev/null +++ b/Tiku.Application/Learning/QuestionGrading.cs @@ -0,0 +1,180 @@ +using System.Globalization; +using System.Text; +using System.Text.Json; +using Tiku.Domain.Learning; + +namespace Tiku.Application.Learning; + +public sealed record QuestionGradingInput( + string QuestionType, + int? CorrectOptionIndex, + JsonElement CorrectOptionIndices, + string? CorrectAnswerText, + JsonElement GradingRules, + IReadOnlyCollection? SelectedOptionIndices, + string? AnswerText, + decimal Score); + +public sealed record QuestionGradingResult( + AnswerGradingStatus Status, + bool? IsCorrect, + decimal? AwardedScore); + +public static class QuestionGrader +{ + private static readonly IReadOnlySet SubjectiveTypes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "short_answer", "reading", "programming" + }; + + public static QuestionGradingResult Grade(QuestionGradingInput input) + { + var type = input.QuestionType.Trim().ToLowerInvariant(); + if (SubjectiveTypes.Contains(type)) + { + return new QuestionGradingResult(AnswerGradingStatus.PendingReview, null, null); + } + + var correct = type switch + { + "choice" => GradeChoice(input), + "multiple_choice" => GradeMultipleChoice(input), + "true_false" => GradeTrueFalse(input), + "fill_blank" => GradeFillBlank(input), + _ => throw new InvalidOperationException($"Unsupported question type '{input.QuestionType}'.") + }; + return new QuestionGradingResult( + correct ? AnswerGradingStatus.Correct : AnswerGradingStatus.Incorrect, + correct, + correct ? input.Score : 0); + } + + public static bool HasValidAuthoritativeAnswer( + string questionType, + int? correctOptionIndex, + JsonElement correctOptionIndices, + string? answerText) + { + return questionType.Trim().ToLowerInvariant() switch + { + "choice" => correctOptionIndex is >= 0, + "multiple_choice" => ReadIndices(correctOptionIndices).Count > 0, + "true_false" => TryNormalizeBoolean(answerText, out _), + "fill_blank" => !string.IsNullOrWhiteSpace(answerText), + "short_answer" or "reading" or "programming" => true, + _ => false + }; + } + + private static bool GradeChoice(QuestionGradingInput input) + { + var selected = input.SelectedOptionIndices?.Distinct().ToArray() ?? []; + return input.CorrectOptionIndex.HasValue && + selected.Length == 1 && + selected[0] == input.CorrectOptionIndex.Value; + } + + private static bool GradeMultipleChoice(QuestionGradingInput input) + { + var expected = ReadIndices(input.CorrectOptionIndices); + var selected = (input.SelectedOptionIndices ?? []).Distinct().Order().ToArray(); + return expected.Count > 0 && expected.SequenceEqual(selected); + } + + private static bool GradeTrueFalse(QuestionGradingInput input) + { + return TryNormalizeBoolean(input.CorrectAnswerText, out var expected) && + TryNormalizeBoolean(input.AnswerText, out var actual) && + expected == actual; + } + + private static bool GradeFillBlank(QuestionGradingInput input) + { + var accepted = new HashSet(StringComparer.Ordinal); + AddNormalized(accepted, input.CorrectAnswerText); + if (input.GradingRules.ValueKind == JsonValueKind.Object && + input.GradingRules.TryGetProperty("acceptedAnswers", out var alternatives) && + alternatives.ValueKind == JsonValueKind.Array) + { + foreach (var alternative in alternatives.EnumerateArray()) + { + if (alternative.ValueKind == JsonValueKind.String) + { + AddNormalized(accepted, alternative.GetString()); + } + } + } + + return accepted.Count > 0 && accepted.Contains(NormalizeText(input.AnswerText)); + } + + private static IReadOnlyList ReadIndices(JsonElement value) + { + if (value.ValueKind != JsonValueKind.Array) + { + return []; + } + + return value.EnumerateArray() + .Where(item => item.TryGetInt32(out _)) + .Select(item => item.GetInt32()) + .Distinct() + .Order() + .ToArray(); + } + + private static bool TryNormalizeBoolean(string? value, out bool result) + { + switch (NormalizeText(value)) + { + case "true" or "1" or "yes" or "正确" or "对": + result = true; + return true; + case "false" or "0" or "no" or "错误" or "错": + result = false; + return true; + default: + result = false; + return false; + } + } + + private static void AddNormalized(ISet values, string? value) + { + var normalized = NormalizeText(value); + if (normalized.Length > 0) + { + values.Add(normalized); + } + } + + private static string NormalizeText(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + var normalized = value.Normalize(NormalizationForm.FormKC).Trim().ToLower(CultureInfo.InvariantCulture); + var builder = new StringBuilder(normalized.Length); + var previousWhitespace = false; + foreach (var character in normalized) + { + if (char.IsWhiteSpace(character)) + { + if (!previousWhitespace) + { + builder.Append(' '); + previousWhitespace = true; + } + } + else + { + builder.Append(character); + previousWhitespace = false; + } + } + + return builder.ToString(); + } +} diff --git a/Tiku.Domain/Learning/LearningEntities.cs b/Tiku.Domain/Learning/LearningEntities.cs index ee885e0..5ada1dd 100644 --- a/Tiku.Domain/Learning/LearningEntities.cs +++ b/Tiku.Domain/Learning/LearningEntities.cs @@ -19,6 +19,9 @@ public sealed class PracticeSession : TenantEntity public int? DurationMinutes { get; set; } public decimal? TotalScore { get; set; } public DateTimeOffset? ExpiresAt { get; set; } + public PracticeSessionStatus Status { get; set; } = PracticeSessionStatus.Active; + public long Version { get; set; } = 1; + public long LastClientSequence { get; set; } public PracticeAccessMode AccessMode { get; set; } = PracticeAccessMode.Free; public Guid? AccessEntitlementId { get; set; } public int ConsumedFreeQuota { get; set; } @@ -35,6 +38,28 @@ public sealed class PracticeSessionQuestion : TenantEntity public Guid QuestionVersionId { get; set; } public int Position { get; set; } public decimal? Score { get; set; } + public string QuestionType { get; set; } = "choice"; + public string? TypeLabelSnapshot { get; set; } + public int? DifficultySnapshot { get; set; } + public JsonElement TagsSnapshot { get; set; } = JsonDefaults.Array(); + public string? ContentSnapshot { get; set; } + public JsonElement OptionsSnapshot { get; set; } = JsonDefaults.Array(); + public int? CorrectOptionIndexSnapshot { get; set; } + public JsonElement CorrectOptionIndicesSnapshot { get; set; } = JsonDefaults.Array(); + public string? AnswerTextSnapshot { get; set; } + public string? ExplanationSnapshot { get; set; } + public JsonElement GradingRulesSnapshot { get; set; } = JsonDefaults.Object(); + public int SnapshotVersion { get; set; } = 1; +} + +public enum PracticeSessionStatus +{ + Active, + Scoring, + PendingReview, + Submitted, + Expired, + Cancelled } public enum PracticeAccessMode @@ -55,10 +80,38 @@ public sealed class AnswerRecord : TenantEntity public JsonElement SelectedOptions { get; set; } = JsonDefaults.Array(); public string? AnswerText { get; set; } public bool? IsCorrect { get; set; } + public AnswerGradingStatus GradingStatus { get; set; } = AnswerGradingStatus.PendingReview; + public decimal? AwardedScore { get; set; } + public int Revision { get; set; } = 1; + public long ClientSequence { get; set; } + public string IdempotencyKey { get; set; } = string.Empty; + public string RequestHash { get; set; } = string.Empty; + public bool IsCurrent { get; set; } = true; public DateTimeOffset AnsweredAt { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; } +public enum AnswerGradingStatus +{ + PendingReview, + Correct, + Incorrect, + LegacyUnverified +} + +public sealed class LearningOperationIdempotency : Entity, ITenantOwned +{ + public Guid TenantId { get; set; } + public Guid UserId { get; set; } + public Guid PracticeSessionId { get; set; } + public string OperationType { get; set; } = string.Empty; + public string IdempotencyKey { get; set; } = string.Empty; + public string RequestHash { get; set; } = string.Empty; + public JsonElement ResponseSnapshot { get; set; } = JsonDefaults.Object(); + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? CompletedAt { get; set; } +} + public sealed class FavoriteQuestion : ITenantOwned { public Guid TenantId { get; set; } diff --git a/Tiku.Domain/Learning/LearningReportEntities.cs b/Tiku.Domain/Learning/LearningReportEntities.cs index 361dd7c..720b998 100644 --- a/Tiku.Domain/Learning/LearningReportEntities.cs +++ b/Tiku.Domain/Learning/LearningReportEntities.cs @@ -109,12 +109,24 @@ public sealed class PracticeSessionReport : AuditableTenantEntity public int DurationSeconds { get; set; } public DateTimeOffset? StartedAt { get; set; } public DateTimeOffset SubmittedAt { get; set; } = DateTimeOffset.UtcNow; + public PracticeReportStatus Status { get; set; } = PracticeReportStatus.Final; + public int Version { get; set; } = 1; + public bool IsFinal { get; set; } = true; + public int PendingReviewCount { get; set; } + public int ScoringVersion { get; set; } = 1; 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 enum PracticeReportStatus +{ + PendingReview, + Final, + LegacyUnverified +} + public sealed class PracticeSessionReportSection : AuditableTenantEntity { public Guid ReportId { get; set; } diff --git a/Tiku.Infrastructure/Learning/LearningActivityService.cs b/Tiku.Infrastructure/Learning/LearningActivityService.cs index f1aacbf..a6f8f11 100644 --- a/Tiku.Infrastructure/Learning/LearningActivityService.cs +++ b/Tiku.Infrastructure/Learning/LearningActivityService.cs @@ -1,5 +1,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Diagnostics.Metrics; +using System.Security.Cryptography; +using System.Text; using System.Text.Json; using Tiku.Application.Learning; using Tiku.Application.QuestionBanks; @@ -16,17 +20,27 @@ public sealed class LearningActivityService( TikuDbContext dbContext, IQuestionReferenceService questionReferenceService, IPublicQuestionAccessPolicy publicQuestionAccessPolicy, - ITenantExecutionScope tenantExecutionScope) : ILearningActivityService + ITenantExecutionScope tenantExecutionScope, + ILogger logger) : ILearningActivityService { private const int DefaultLimit = 100; private const int MaxLimit = 500; + private static readonly Meter LearningMeter = new("Tiku.Learning"); + private static readonly Counter IdempotencyReplays = LearningMeter.CreateCounter("tiku.learning.idempotency.replays"); + private static readonly Counter AnswerConflicts = LearningMeter.CreateCounter("tiku.learning.answer.conflicts"); + private static readonly Counter SubmissionConflicts = LearningMeter.CreateCounter("tiku.learning.submission.conflicts"); + private static readonly Counter ScoringFailures = LearningMeter.CreateCounter("tiku.learning.scoring.failures"); public async Task GetStatsAsync( LearningActor actor, CancellationToken cancellationToken = default) { var answers = dbContext.AnswerRecords.AsNoTracking() - .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId); + .Where(item => item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.IsCurrent && + item.GradingStatus != AnswerGradingStatus.PendingReview && + item.GradingStatus != AnswerGradingStatus.LegacyUnverified); return new LearningStatsItem( await answers.CountAsync(cancellationToken), await answers.CountAsync(item => item.IsCorrect == true, cancellationToken), @@ -46,7 +60,9 @@ public sealed class LearningActivityService( item => item.TenantId == actor.TenantId && item.UserId == actor.UserId, cancellationToken), await dbContext.PracticeSessionReports.AsNoTracking().CountAsync( - item => item.TenantId == actor.TenantId && item.UserId == actor.UserId, + item => item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.Status == PracticeReportStatus.Final, cancellationToken)); } @@ -60,6 +76,9 @@ public sealed class LearningActivityService( .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId && + item.IsCurrent && + item.GradingStatus != AnswerGradingStatus.PendingReview && + item.GradingStatus != AnswerGradingStatus.LegacyUnverified && item.AnsweredAt >= since) .Select(item => new { item.AnsweredAt, item.IsCorrect }) .ToArrayAsync(cancellationToken); @@ -81,7 +100,10 @@ public sealed class LearningActivityService( CancellationToken cancellationToken = default) { var rows = await dbContext.AnswerRecords.AsNoTracking() - .Where(item => item.TenantId == actor.TenantId) + .Where(item => item.TenantId == actor.TenantId && + item.IsCurrent && + item.GradingStatus != AnswerGradingStatus.PendingReview && + item.GradingStatus != AnswerGradingStatus.LegacyUnverified) .GroupBy(item => item.UserId) .Select(group => new { @@ -120,22 +142,18 @@ public sealed class LearningActivityService( SubmitAnswerCommand command, CancellationToken cancellationToken = default) { + if (string.IsNullOrWhiteSpace(command.IdempotencyKey)) + { + throw new LearningValidationException("idempotency_key_required", "An idempotency key is required."); + } + if (command.SelectedOptionIndices?.Any(index => index < 0) == true) + { + throw new LearningValidationException("selected_option_index_invalid", "Selected option indices must be zero-based non-negative values."); + } + var now = DateTimeOffset.UtcNow; - var sessionQuestion = await dbContext.PracticeSessionQuestions - .AsNoTracking() - .Where(item => - item.TenantId == actor.TenantId && - item.Id == command.SessionQuestionId) - .Join( - dbContext.PracticeSessions.AsNoTracking().Where(session => - session.TenantId == actor.TenantId && - session.UserId == actor.UserId && - session.FinishedAt == null && - (!session.ExpiresAt.HasValue || session.ExpiresAt > now)), - item => new { item.TenantId, Id = item.PracticeSessionId }, - session => new { session.TenantId, session.Id }, - (item, session) => item) - .SingleOrDefaultAsync(cancellationToken); + var sessionQuestion = await dbContext.PracticeSessionQuestions.SingleOrDefaultAsync(item => + item.TenantId == actor.TenantId && item.Id == command.SessionQuestionId, cancellationToken); if (sessionQuestion is null) { @@ -144,49 +162,143 @@ public sealed class LearningActivityService( "An active practice session question was not found."); } + var session = await dbContext.PracticeSessions.SingleOrDefaultAsync(item => + item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.Id == sessionQuestion.PracticeSessionId, cancellationToken); + if (session is null) + { + throw new LearningResourceNotFoundException("practice_session_not_found", "Practice session was not found."); + } + + var requestHash = HashAnswer(command); + var existingOperation = await dbContext.LearningOperationIdempotencies.AsNoTracking().SingleOrDefaultAsync(item => + item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.PracticeSessionId == session.Id && + item.OperationType == "answer" && + item.IdempotencyKey == command.IdempotencyKey, cancellationToken); + if (existingOperation is not null) + { + if (!string.Equals(existingOperation.RequestHash, requestHash, StringComparison.Ordinal)) + { + AnswerConflicts.Add(1); + throw new LearningValidationException("idempotency_conflict", "The idempotency key was used with a different request."); + } + + IdempotencyReplays.Add(1); + return existingOperation.ResponseSnapshot.Deserialize() + ?? throw new InvalidOperationException("The stored answer response is invalid."); + } + + if (session.ExpiresAt.HasValue && session.ExpiresAt <= now) + { + session.Status = PracticeSessionStatus.Expired; + session.Version++; + await dbContext.SaveChangesAsync(cancellationToken); + throw new LearningValidationException("practice_session_expired", "The practice session has expired."); + } + EnsureAnswerSessionState(session, command); + var current = await dbContext.AnswerRecords.SingleOrDefaultAsync(answer => + answer.TenantId == actor.TenantId && + answer.UserId == actor.UserId && + answer.PracticeSessionId == session.Id && + answer.SessionQuestionId == sessionQuestion.Id && + answer.IsCurrent, cancellationToken); + if (current is not null) + { + current.IsCurrent = false; + } + + var selectedIndices = command.SelectedOptionIndices?.Distinct().Order().ToArray() ?? []; + var score = sessionQuestion.Score ?? 1; + QuestionGradingResult grading; + try + { + grading = QuestionGrader.Grade(new QuestionGradingInput( + sessionQuestion.QuestionType, + sessionQuestion.CorrectOptionIndexSnapshot, + sessionQuestion.CorrectOptionIndicesSnapshot, + sessionQuestion.AnswerTextSnapshot, + sessionQuestion.GradingRulesSnapshot, + selectedIndices, + command.AnswerText, + score)); + } + catch (InvalidOperationException exception) + { + ScoringFailures.Add(1); + logger.LogWarning(exception, + "Question scoring failed for tenant {TenantId}, session {PracticeSessionId}, question {SessionQuestionId}", + actor.TenantId, session.Id, sessionQuestion.Id); + throw new LearningValidationException("question_grading_rule_invalid", exception.Message); + } + var record = new AnswerRecord { TenantId = actor.TenantId, UserId = actor.UserId, PracticeSessionId = sessionQuestion.PracticeSessionId, SessionQuestionId = sessionQuestion.Id, - SelectedOptions = JsonSerializer.SerializeToElement(command.SelectedOptions ?? []), + SelectedOptions = JsonSerializer.SerializeToElement(selectedIndices), AnswerText = command.AnswerText, - IsCorrect = command.SelfJudgedCorrect, + IsCorrect = grading.IsCorrect, + GradingStatus = grading.Status, + AwardedScore = grading.AwardedScore, + Revision = (current?.Revision ?? 0) + 1, + ClientSequence = command.ClientSequence, + IdempotencyKey = command.IdempotencyKey.Trim(), + RequestHash = requestHash, + IsCurrent = true, AnsweredAt = now, CreatedAt = now }; dbContext.AnswerRecords.Add(record); - - if (command.SelfJudgedCorrect == false) + session.Version++; + session.LastClientSequence = command.ClientSequence; + var response = ToItem(record, session.Version); + dbContext.LearningOperationIdempotencies.Add(new LearningOperationIdempotency { - var wrongQuestion = await dbContext.WrongQuestions.FindAsync( - [actor.TenantId, actor.UserId, sessionQuestion.QuestionReferenceId], - cancellationToken); + TenantId = actor.TenantId, + UserId = actor.UserId, + PracticeSessionId = session.Id, + OperationType = "answer", + IdempotencyKey = command.IdempotencyKey.Trim(), + RequestHash = requestHash, + ResponseSnapshot = JsonSerializer.SerializeToElement(response), + CompletedAt = now + }); - if (wrongQuestion is null) + try + { + await dbContext.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + throw new LearningValidationException("practice_session_version_conflict", "The practice session changed. Reload it before answering."); + } + catch (DbUpdateException exception) when ( + exception.InnerException is Npgsql.PostgresException postgresException && + postgresException.SqlState == Npgsql.PostgresErrorCodes.UniqueViolation) + { + dbContext.ChangeTracker.Clear(); + var replay = await dbContext.LearningOperationIdempotencies.AsNoTracking().SingleOrDefaultAsync(item => + item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.PracticeSessionId == session.Id && + item.OperationType == "answer" && + item.IdempotencyKey == command.IdempotencyKey, cancellationToken); + if (replay is not null && string.Equals(replay.RequestHash, requestHash, StringComparison.Ordinal)) { - dbContext.WrongQuestions.Add(new WrongQuestion - { - TenantId = actor.TenantId, - UserId = actor.UserId, - QuestionReferenceId = sessionQuestion.QuestionReferenceId, - QuestionOwnerTenantId = sessionQuestion.QuestionOwnerTenantId, - QuestionId = sessionQuestion.QuestionId, - WrongCount = 1, - LastWrongAt = now - }); - } - else - { - wrongQuestion.WrongCount++; - wrongQuestion.LastWrongAt = now; - wrongQuestion.ResolvedAt = null; + IdempotencyReplays.Add(1); + return replay.ResponseSnapshot.Deserialize() + ?? throw new InvalidOperationException("The stored answer response is invalid."); } + AnswerConflicts.Add(1); + throw new LearningValidationException("practice_answer_conflict", "The answer conflicted with another client operation."); } - await dbContext.SaveChangesAsync(cancellationToken); - return ToItem(record); + return response; } public async Task> GetFavoriteQuestionsAsync( @@ -664,11 +776,23 @@ public sealed class LearningActivityService( }; dbContext.PracticeSessions.Add(session); - await dbContext.SaveChangesAsync(cancellationToken); var selections = await LoadQuestionSelectionsAsync( actor.TenantId, questionReferenceIds, cancellationToken); + foreach (var selection in selections) + { + if (!QuestionGrader.HasValidAuthoritativeAnswer( + selection.QuestionType, + selection.CorrectOptionIndex, + selection.CorrectOptionIndices, + selection.AnswerText)) + { + throw new LearningValidationException( + "practice_question_grading_rule_invalid", + $"Question '{selection.QuestionId}' has no valid authoritative grading rule."); + } + } var scorePerQuestion = session.TotalScore.HasValue && selections.Count > 0 ? session.TotalScore.Value / selections.Count : (decimal?)null; @@ -682,7 +806,19 @@ public sealed class LearningActivityService( QuestionId = selection.QuestionId, QuestionVersionId = selection.QuestionVersionId, Position = index, - Score = scorePerQuestion + Score = scorePerQuestion, + QuestionType = selection.QuestionType, + TypeLabelSnapshot = selection.TypeLabel, + DifficultySnapshot = selection.Difficulty, + TagsSnapshot = selection.Tags, + ContentSnapshot = selection.Content, + OptionsSnapshot = selection.Options, + CorrectOptionIndexSnapshot = selection.CorrectOptionIndex, + CorrectOptionIndicesSnapshot = selection.CorrectOptionIndices, + AnswerTextSnapshot = selection.AnswerText, + ExplanationSnapshot = selection.Explanation, + GradingRulesSnapshot = BuildGradingRules(selection), + SnapshotVersion = 1 })); dbContext.PracticeAccessEvents.Add(new PracticeAccessEvent { @@ -717,39 +853,104 @@ public sealed class LearningActivityService( .Where(answer => answer.TenantId == actor.TenantId && answer.UserId == actor.UserId && - answer.PracticeSessionId == session.Id) + answer.PracticeSessionId == session.Id && + answer.IsCurrent) .ToArrayAsync(cancellationToken); var answersByQuestion = answers .GroupBy(answer => answer.SessionQuestionId) .ToDictionary( group => group.Key, - group => ToItem(group.OrderByDescending(answer => answer.AnsweredAt).First())); + group => ToItem( + group.OrderByDescending(answer => answer.Revision).First(), + session.Version)); return new PracticeSessionDetailItem(ToItem(session), orderedQuestions, answersByQuestion); } public async Task SubmitPracticeSessionAsync( LearningActor actor, - PracticeSessionFilter filter, + SubmitPracticeSessionCommand command, CancellationToken cancellationToken = default) { - var session = await GetPracticeSessionAsync(actor, filter.PracticeSessionId, cancellationToken); - var existing = await dbContext.PracticeSessionReports - .AsNoTracking() - .SingleOrDefaultAsync( - report => - report.TenantId == actor.TenantId && - report.PracticeSessionId == session.Id, - cancellationToken); - if (existing is not null) + if (string.IsNullOrWhiteSpace(command.IdempotencyKey)) { - return ToItem(existing); + throw new LearningValidationException("idempotency_key_required", "An idempotency key is required."); } + await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken); + var session = await GetPracticeSessionAsync(actor, command.PracticeSessionId, cancellationToken); + var requestHash = HashSubmission(command); + var existingOperation = await dbContext.LearningOperationIdempotencies.AsNoTracking().SingleOrDefaultAsync(item => + item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.PracticeSessionId == session.Id && + item.OperationType == "submit" && + item.IdempotencyKey == command.IdempotencyKey, cancellationToken); + if (existingOperation is not null) + { + if (!string.Equals(existingOperation.RequestHash, requestHash, StringComparison.Ordinal)) + { + SubmissionConflicts.Add(1); + throw new LearningValidationException("idempotency_conflict", "The idempotency key was used with a different request."); + } + + IdempotencyReplays.Add(1); + return existingOperation.ResponseSnapshot.Deserialize() + ?? throw new InvalidOperationException("The stored report response is invalid."); + } + + if (session.Status != PracticeSessionStatus.Active) + { + throw new LearningValidationException("practice_session_not_active", "Only an active practice session can be submitted."); + } + if (session.ExpiresAt.HasValue && session.ExpiresAt <= DateTimeOffset.UtcNow) + { + session.Status = PracticeSessionStatus.Expired; + session.Version++; + await dbContext.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + throw new LearningValidationException("practice_session_expired", "The practice session has expired."); + } + if (session.Version != command.ExpectedSessionVersion) + { + throw new LearningValidationException("practice_session_version_conflict", "The practice session changed. Reload it before submitting."); + } + + session.Status = PracticeSessionStatus.Scoring; + session.Version++; var report = await BuildPracticeSessionReportAsync(actor, session, cancellationToken); - session.FinishedAt ??= report.SubmittedAt; - await dbContext.SaveChangesAsync(cancellationToken); - return ToItem(report); + session.Status = report.IsFinal ? PracticeSessionStatus.Submitted : PracticeSessionStatus.PendingReview; + session.FinishedAt = report.SubmittedAt; + session.Version++; + var response = ToItem(report); + dbContext.LearningOperationIdempotencies.Add(new LearningOperationIdempotency + { + TenantId = actor.TenantId, + UserId = actor.UserId, + PracticeSessionId = session.Id, + OperationType = "submit", + IdempotencyKey = command.IdempotencyKey.Trim(), + RequestHash = requestHash, + ResponseSnapshot = JsonSerializer.SerializeToElement(response), + CompletedAt = report.SubmittedAt + }); + try + { + await dbContext.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + SubmissionConflicts.Add(1); + throw new LearningValidationException("practice_session_version_conflict", "The practice session changed during submission."); + } + catch (DbUpdateException exception) when (exception.InnerException is Npgsql.NpgsqlException) + { + SubmissionConflicts.Add(1); + throw new LearningValidationException("practice_submission_conflict", "The practice session was already submitted by another request."); + } + + return response; } public async Task GetPracticeSessionReportAsync( @@ -851,7 +1052,7 @@ public sealed class LearningActivityService( row.session.StartedAt, row.session.FinishedAt, row.session.ExpiresAt, - PracticeSessionStatus(row.session, now), + ResolvePracticeSessionHistoryStatus(row.session, now), row.report?.Id, row.report?.Score, row.report?.TotalScore, @@ -1055,7 +1256,17 @@ public sealed class LearningActivityService( reference.Id, reference.QuestionOwnerTenantId, reference.QuestionId, - version.Id)) + version.Id, + question.Type, + question.TypeLabel, + question.Difficulty, + question.Tags, + version.Content, + version.Options, + version.CorrectOptionIndex, + version.CorrectOptionIndices, + version.AnswerText, + version.Explanation)) .ToArrayAsync(token); }, cancellationToken); @@ -1085,21 +1296,6 @@ public sealed class LearningActivityService( var systemDbContext = provider.GetRequiredService(); return await ( from sessionQuestion in systemDbContext.PracticeSessionQuestions.AsNoTracking() - join question in systemDbContext.Questions.AsNoTracking() - on new - { - TenantId = sessionQuestion.QuestionOwnerTenantId, - Id = sessionQuestion.QuestionId - } - equals new { question.TenantId, question.Id } - join version in systemDbContext.QuestionVersions.AsNoTracking() - on new - { - TenantId = sessionQuestion.QuestionOwnerTenantId, - sessionQuestion.QuestionId, - Id = sessionQuestion.QuestionVersionId - } - equals new { version.TenantId, version.QuestionId, version.Id } where sessionQuestion.TenantId == tenantId && sessionQuestion.PracticeSessionId == practiceSessionId orderby sessionQuestion.Position @@ -1111,15 +1307,14 @@ public sealed class LearningActivityService( ? QuestionSource.Tenant : QuestionSource.Platform, sessionQuestion.QuestionId), - question.Id, - question.Type, - question.TypeLabel, - question.Difficulty, - question.Tags, - version.Id, - version.Content, - version.Options, - version.Explanation)) + sessionQuestion.QuestionId, + sessionQuestion.QuestionType, + sessionQuestion.TypeLabelSnapshot, + sessionQuestion.DifficultySnapshot, + sessionQuestion.TagsSnapshot, + sessionQuestion.QuestionVersionId, + sessionQuestion.ContentSnapshot, + sessionQuestion.OptionsSnapshot)) .ToArrayAsync(token); }, cancellationToken); @@ -1172,32 +1367,35 @@ public sealed class LearningActivityService( .Where(answer => answer.TenantId == actor.TenantId && answer.UserId == actor.UserId && - answer.PracticeSessionId == session.Id) + answer.PracticeSessionId == session.Id && + answer.IsCurrent) .ToArrayAsync(cancellationToken); - var latestAnswers = answers - .GroupBy(answer => answer.SessionQuestionId) - .ToDictionary( - group => group.Key, - group => group.OrderByDescending(answer => answer.AnsweredAt).First()); + var latestAnswers = answers.ToDictionary(answer => answer.SessionQuestionId); var totalQuestions = sessionQuestions.Length; var answeredCount = sessionQuestions.Count(question => latestAnswers.ContainsKey(question.Id)); var correctCount = sessionQuestions.Count(question => latestAnswers.TryGetValue(question.Id, out var answer) && - answer.IsCorrect == true); + answer.GradingStatus == AnswerGradingStatus.Correct); var wrongCount = sessionQuestions.Count(question => latestAnswers.TryGetValue(question.Id, out var answer) && - answer.IsCorrect != true); + answer.GradingStatus == AnswerGradingStatus.Incorrect); + var pendingReviewCount = sessionQuestions.Count(question => + latestAnswers.TryGetValue(question.Id, out var answer) && + answer.GradingStatus == AnswerGradingStatus.PendingReview); var unansweredCount = Math.Max(0, totalQuestions - answeredCount); - var totalScore = session.TotalScore ?? totalQuestions; - var scorePerQuestion = totalQuestions == 0 ? 0 : totalScore / totalQuestions; - var score = Math.Round(correctCount * scorePerQuestion, 2); - var accuracy = totalQuestions == 0 ? 0 : Math.Round((decimal)correctCount / totalQuestions, 4); + var totalScore = sessionQuestions.Sum(question => question.Score ?? 1); + var score = Math.Round(answers.Sum(answer => answer.AwardedScore ?? 0), 2); + var objectivelyGradedCount = correctCount + wrongCount; + var accuracy = objectivelyGradedCount == 0 + ? 0 + : Math.Round((decimal)correctCount / objectivelyGradedCount, 4); + var isFinal = pendingReviewCount == 0; var submittedAt = DateTimeOffset.UtcNow; var durationSeconds = Math.Max(0, (int)(submittedAt - session.StartedAt).TotalSeconds); var wrongQuestionIds = sessionQuestions .Where(question => latestAnswers.TryGetValue(question.Id, out var answer) && - answer.IsCorrect != true) + answer.GradingStatus == AnswerGradingStatus.Incorrect) .Select(question => question.QuestionReferenceId) .ToArray(); var questionResults = sessionQuestions @@ -1211,10 +1409,15 @@ public sealed class LearningActivityService( questionId = question.QuestionId, source = question.QuestionOwnerTenantId == actor.TenantId ? "tenant" : "platform", answered = answer is not null, - isCorrect = answer?.IsCorrect, - score = answer?.IsCorrect == true ? scorePerQuestion : 0, - totalScore = scorePerQuestion, - answeredAt = answer?.AnsweredAt + gradingStatus = answer?.GradingStatus.ToString(), + isCorrect = isFinal ? answer?.IsCorrect : null, + score = answer?.AwardedScore, + totalScore = question.Score ?? 1, + answeredAt = answer?.AnsweredAt, + correctOptionIndex = isFinal ? question.CorrectOptionIndexSnapshot : null, + correctOptionIndices = isFinal ? question.CorrectOptionIndicesSnapshot : JsonDefaults.Array(), + answerText = isFinal ? question.AnswerTextSnapshot : null, + explanation = isFinal ? question.ExplanationSnapshot : null }; }) .ToArray(); @@ -1232,6 +1435,7 @@ public sealed class LearningActivityService( score, totalScore, accuracy, + pendingReviewCount, sortOrder = 0 } }; @@ -1255,13 +1459,17 @@ public sealed class LearningActivityService( DurationSeconds = durationSeconds, StartedAt = session.StartedAt, SubmittedAt = submittedAt, + Status = isFinal ? PracticeReportStatus.Final : PracticeReportStatus.PendingReview, + Version = 1, + IsFinal = isFinal, + PendingReviewCount = pendingReviewCount, + ScoringVersion = 1, SectionStats = JsonSerializer.SerializeToElement(sectionStats), QuestionResults = JsonSerializer.SerializeToElement(questionResults), WrongQuestionIds = JsonSerializer.SerializeToElement(wrongQuestionIds), Metadata = JsonSerializer.SerializeToElement(new { - scoringVersion = 1, - scorePerQuestion + scoringVersion = 1 }) }; dbContext.PracticeSessionReports.Add(report); @@ -1283,6 +1491,33 @@ public sealed class LearningActivityService( SortOrder = 0 }); + foreach (var question in sessionQuestions.Where(question => + latestAnswers.TryGetValue(question.Id, out var answer) && + answer.GradingStatus == AnswerGradingStatus.Incorrect)) + { + var wrongQuestion = await dbContext.WrongQuestions.FindAsync( + [actor.TenantId, actor.UserId, question.QuestionReferenceId], cancellationToken); + if (wrongQuestion is null) + { + dbContext.WrongQuestions.Add(new WrongQuestion + { + TenantId = actor.TenantId, + UserId = actor.UserId, + QuestionReferenceId = question.QuestionReferenceId, + QuestionOwnerTenantId = question.QuestionOwnerTenantId, + QuestionId = question.QuestionId, + WrongCount = 1, + LastWrongAt = submittedAt + }); + } + else + { + wrongQuestion.WrongCount++; + wrongQuestion.LastWrongAt = submittedAt; + wrongQuestion.ResolvedAt = null; + } + } + return report; } @@ -1322,7 +1557,7 @@ public sealed class LearningActivityService( } } - private static AnswerRecordItem ToItem(AnswerRecord record) + private static AnswerRecordItem ToItem(AnswerRecord record, long sessionVersion) { return new AnswerRecordItem( record.Id, @@ -1330,7 +1565,10 @@ public sealed class LearningActivityService( record.PracticeSessionId, record.SelectedOptions, record.AnswerText, - record.IsCorrect, + record.GradingStatus == AnswerGradingStatus.PendingReview ? "pending_review" : "accepted", + record.Revision, + record.ClientSequence, + sessionVersion, record.AnsweredAt); } @@ -1372,7 +1610,9 @@ public sealed class LearningActivityService( item.FinishedAt, item.ExpiresAt, item.Metadata, - PracticeSessionStatus(item, DateTimeOffset.UtcNow)); + item.Status, + item.Version, + item.LastClientSequence); } private static PracticeSessionReportItem ToItem(PracticeSessionReport item) @@ -1394,6 +1634,11 @@ public sealed class LearningActivityService( item.DurationSeconds, item.StartedAt, item.SubmittedAt, + item.Status, + item.Version, + item.IsFinal, + item.PendingReviewCount, + item.ScoringVersion, item.SectionStats, item.QuestionResults, item.WrongQuestionIds, @@ -1416,14 +1661,60 @@ public sealed class LearningActivityService( .ToList(); } - private static string PracticeSessionStatus(PracticeSession session, DateTimeOffset now) + private static void EnsureAnswerSessionState( + PracticeSession session, + SubmitAnswerCommand command) { - if (session.FinishedAt.HasValue) + if (session.Status != PracticeSessionStatus.Active) + { + throw new LearningValidationException("practice_session_not_active", "Only an active practice session accepts answers."); + } + if (session.Version != command.ExpectedSessionVersion) + { + throw new LearningValidationException("practice_session_version_conflict", "The practice session changed. Reload it before answering."); + } + if (command.ClientSequence <= session.LastClientSequence) + { + throw new LearningValidationException("practice_client_sequence_conflict", "Client sequence must increase within a practice session."); + } + } + + private static JsonElement BuildGradingRules(QuestionSelection selection) => + JsonSerializer.SerializeToElement(new + { + version = 1, + normalization = selection.QuestionType.Equals("fill_blank", StringComparison.OrdinalIgnoreCase) + ? "nfkc_trim_casefold_whitespace" + : "exact" + }); + + private static string HashAnswer(SubmitAnswerCommand command) => Hash(JsonSerializer.Serialize(new + { + command.SessionQuestionId, + command.ExpectedSessionVersion, + command.ClientSequence, + selectedOptionIndices = command.SelectedOptionIndices?.Distinct().Order().ToArray() ?? [], + answerText = command.AnswerText?.Trim() + })); + + private static string HashSubmission(SubmitPracticeSessionCommand command) => Hash(JsonSerializer.Serialize(new + { + command.PracticeSessionId, + command.ExpectedSessionVersion + })); + + private static string Hash(string value) => + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant(); + + private static string ResolvePracticeSessionHistoryStatus(PracticeSession session, DateTimeOffset now) + { + if (session.Status is PracticeSessionStatus.Submitted or PracticeSessionStatus.PendingReview) { return "finished"; } - if (session.ExpiresAt.HasValue && session.ExpiresAt.Value <= now) + if (session.Status == PracticeSessionStatus.Expired || + session.ExpiresAt.HasValue && session.ExpiresAt.Value <= now) { return "expired"; } @@ -1479,7 +1770,17 @@ public sealed class LearningActivityService( Guid QuestionReferenceId, Guid QuestionOwnerTenantId, Guid QuestionId, - Guid QuestionVersionId); + Guid QuestionVersionId, + string QuestionType, + string? TypeLabel, + int? Difficulty, + JsonElement Tags, + string? Content, + JsonElement Options, + int? CorrectOptionIndex, + JsonElement CorrectOptionIndices, + string? AnswerText, + string? Explanation); } public class LearningException(string code, string message) : Exception(message) diff --git a/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs index 8ff5632..cadd20c 100644 --- a/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs +++ b/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs @@ -18,12 +18,15 @@ internal sealed class PracticeSessionConfiguration : IEntityTypeConfiguration entity.TargetType).HasMaxLength(50); builder.Property(entity => entity.StartedAt).HasDefaultValueSql("now()"); builder.Property(entity => entity.TotalScore).HasPrecision(8, 2); + builder.Property(entity => entity.Status).HasSnakeCaseEnum().HasDefaultValue(PracticeSessionStatus.Active); + builder.Property(entity => entity.Version).IsConcurrencyToken().HasDefaultValue(1L); builder.Property(entity => entity.AccessMode) .HasSnakeCaseEnum() .HasDefaultValue(PracticeAccessMode.Free); builder.Property(entity => entity.AccessSnapshot).IsJson("{}"); builder.Property(entity => entity.Metadata).IsJson("{}"); builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.StartedAt }); + builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.Status, entity.StartedAt }); builder.ToTable(table => { @@ -62,6 +65,13 @@ internal sealed class PracticeSessionQuestionConfiguration : IEntityTypeConfigur builder.HasAlternateKey(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Id }); builder.HasIndex(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Position }).IsUnique(); builder.Property(entity => entity.Score).HasPrecision(8, 2); + builder.Property(entity => entity.QuestionType).HasMaxLength(50).HasDefaultValue("choice"); + builder.Property(entity => entity.TypeLabelSnapshot).HasMaxLength(100); + builder.Property(entity => entity.TagsSnapshot).IsJson("[]"); + builder.Property(entity => entity.OptionsSnapshot).IsJson("[]"); + builder.Property(entity => entity.CorrectOptionIndicesSnapshot).IsJson("[]"); + builder.Property(entity => entity.GradingRulesSnapshot).IsJson("{}"); + builder.Property(entity => entity.SnapshotVersion).HasDefaultValue(1); builder.HasOne().WithMany() .HasForeignKey(entity => new { entity.TenantId, entity.PracticeSessionId }) @@ -102,6 +112,12 @@ internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration entity.LegacyQuestionId).HasMaxLength(64); builder.Property(entity => entity.LegacyCategoryId).HasMaxLength(64); builder.Property(entity => entity.SelectedOptions).IsJson("[]"); + builder.Property(entity => entity.GradingStatus).HasSnakeCaseEnum().HasDefaultValue(AnswerGradingStatus.PendingReview); + builder.Property(entity => entity.AwardedScore).HasPrecision(8, 2); + builder.Property(entity => entity.IdempotencyKey).HasMaxLength(200); + builder.Property(entity => entity.RequestHash).HasMaxLength(64); + builder.Property(entity => entity.Revision).HasDefaultValue(1); + builder.Property(entity => entity.IsCurrent).HasDefaultValue(true); builder.Property(entity => entity.AnsweredAt).HasDefaultValueSql("now()"); builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique(); @@ -111,6 +127,28 @@ internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration new + { + entity.TenantId, + entity.UserId, + entity.PracticeSessionId, + entity.ClientSequence + }).IsUnique(); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.UserId, + entity.PracticeSessionId, + entity.SessionQuestionId, + entity.Revision + }).IsUnique().HasDatabaseName("ux_answer_records_session_question_revision"); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.UserId, + entity.PracticeSessionId, + entity.SessionQuestionId + }).IsUnique().HasFilter("is_current").HasDatabaseName("ux_answer_records_current_session_question"); builder.HasOne().WithMany() .HasForeignKey(entity => entity.UserId) @@ -146,6 +184,39 @@ internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("learning_operation_idempotencies"); + builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id }); + builder.Property(entity => entity.OperationType).HasMaxLength(50); + builder.Property(entity => entity.IdempotencyKey).HasMaxLength(200); + builder.Property(entity => entity.RequestHash).HasMaxLength(64); + builder.Property(entity => entity.ResponseSnapshot).IsJson("{}"); + builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.UserId, + entity.PracticeSessionId, + entity.OperationType, + entity.IdempotencyKey + }).IsUnique(); + + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.TenantId) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.UserId) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.UserId, Id = entity.PracticeSessionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.UserId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + } +} + internal sealed class FavoriteQuestionConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) diff --git a/Tiku.Infrastructure/Persistence/Configurations/LearningReportConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/LearningReportConfigurations.cs index d2914de..1a071b8 100644 --- a/Tiku.Infrastructure/Persistence/Configurations/LearningReportConfigurations.cs +++ b/Tiku.Infrastructure/Persistence/Configurations/LearningReportConfigurations.cs @@ -189,6 +189,10 @@ internal sealed class PracticeSessionReportConfiguration : IEntityTypeConfigurat 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.Status).HasSnakeCaseEnum().HasDefaultValue(PracticeReportStatus.Final); + builder.Property(entity => entity.Version).HasDefaultValue(1); + builder.Property(entity => entity.IsFinal).HasDefaultValue(true); + builder.Property(entity => entity.ScoringVersion).HasDefaultValue(1); builder.Property(entity => entity.SectionStats).IsJson("[]"); builder.Property(entity => entity.QuestionResults).IsJson("[]"); builder.Property(entity => entity.WrongQuestionIds).IsJson("[]"); diff --git a/Tiku.Infrastructure/Persistence/Migrations/20260803013235_TrustedLearningCore.Designer.cs b/Tiku.Infrastructure/Persistence/Migrations/20260803013235_TrustedLearningCore.Designer.cs new file mode 100644 index 0000000..1f9db4b --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260803013235_TrustedLearningCore.Designer.cs @@ -0,0 +1,20739 @@ +// +using System; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Tiku.Infrastructure.Persistence; + +#nullable disable + +namespace Tiku.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(TikuDbContext))] + [Migration("20260803013235_TrustedLearningCore")] + partial class TrustedLearningCore + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext"); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "ltree"); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text") + .HasColumnName("friendly_name"); + + b.Property("Xml") + .HasColumnType("text") + .HasColumnName("xml"); + + b.HasKey("Id") + .HasName("pk_data_protection_keys"); + + b.ToTable("data_protection_keys", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_claims_user_id"); + + b.ToTable("user_claims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("text") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("text") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_logins_user_id"); + + b.ToTable("user_logins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_user_tokens"); + + b.ToTable("user_tokens", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CategoryType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("category_type"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SubjectId") + .HasColumnType("uuid") + .HasColumnName("subject_id"); + + b.Property("SvipQuestionLimit") + .HasColumnType("integer") + .HasColumnName("svip_question_limit"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_categories"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_categories_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_categories_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "NodeId") + .HasDatabaseName("ix_categories_tenant_id_node_id"); + + b.HasIndex("TenantId", "SubjectId") + .HasDatabaseName("ix_categories_tenant_id_subject_id"); + + b.ToTable("categories", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Major", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SchoolId") + .HasColumnType("uuid") + .HasColumnName("school_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("StudyTips") + .HasColumnType("text") + .HasColumnName("study_tips"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_majors"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_majors_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_majors_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_majors_tenant_id_region_id"); + + b.HasIndex("TenantId", "SchoolId") + .HasDatabaseName("ix_majors_tenant_id_school_id"); + + b.ToTable("majors", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.ModuleNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyModuleId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_module_id"); + + b.Property("LegacyParentId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_parent_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("path"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_module_nodes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_module_nodes_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_module_nodes_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "ModuleId") + .HasDatabaseName("ix_module_nodes_tenant_id_module_id"); + + b.HasIndex("TenantId", "ParentId") + .HasDatabaseName("ix_module_nodes_tenant_id_parent_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_module_nodes_tenant_id_region_id"); + + b.ToTable("module_nodes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.QuestionTaxonomyAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsPrimary") + .HasColumnType("boolean") + .HasColumnName("is_primary"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("TaxonomyNodeId") + .HasColumnType("uuid") + .HasColumnName("taxonomy_node_id"); + + b.Property("TaxonomyOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("taxonomy_owner_tenant_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_taxonomy_assignments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_taxonomy_assignments_tenant_id_id"); + + b.HasIndex("TaxonomyOwnerTenantId", "TaxonomyNodeId") + .HasDatabaseName("ix_question_taxonomy_assignments_taxonomy_owner_tenant_id_taxo~"); + + b.HasIndex("TenantId", "QuestionId", "TaxonomyOwnerTenantId", "TaxonomyNodeId") + .IsUnique() + .HasDatabaseName("ix_question_taxonomy_assignments_tenant_id_question_id_taxonom~"); + + b.ToTable("question_taxonomy_assignments", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("Config") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("config") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FullName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("full_name"); + + b.Property("Icon") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("icon"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsHot") + .HasColumnType("boolean") + .HasColumnName("is_hot"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Pinyin") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("pinyin"); + + b.Property("ShortName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("short_name"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_regions_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_regions_tenant_id_legacy_id"); + + b.ToTable("regions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.RegionModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Color") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("color"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Icon") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("icon"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsPrimarySchoolModule") + .HasColumnType("boolean") + .HasColumnName("is_primary_school_module"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("Route") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("route"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TextColor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("text_color"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_region_modules"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_region_modules_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_region_modules_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_region_modules_tenant_id_region_id"); + + b.ToTable("region_modules", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.School", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ProfessionalExamDate") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("professional_exam_date"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_schools"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_schools_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_schools_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "ModuleId") + .HasDatabaseName("ix_schools_tenant_id_module_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_schools_tenant_id_region_id"); + + b.ToTable("schools", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.ScorelineField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("FieldKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("field_key"); + + b.Property("FieldName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("field_name"); + + b.Property("FieldType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("field_type"); + + b.Property("IsFilter") + .HasColumnType("boolean") + .HasColumnName("is_filter"); + + b.Property("IsRequired") + .HasColumnType("boolean") + .HasColumnName("is_required"); + + b.Property("IsTrend") + .HasColumnType("boolean") + .HasColumnName("is_trend"); + + b.Property("IsVisible") + .HasColumnType("boolean") + .HasColumnName("is_visible"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Options") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("options") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("Placeholder") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("placeholder"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Unit") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("unit"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_scoreline_fields"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_scoreline_fields_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_scoreline_fields_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId", "FieldKey") + .IsUnique() + .HasDatabaseName("ix_scoreline_fields_tenant_id_region_id_field_key"); + + b.HasIndex("TenantId", "RegionId", "IsFilter", "SortOrder") + .HasDatabaseName("ix_scoreline_fields_tenant_id_region_id_is_filter_sort_order"); + + b.ToTable("scoreline_fields", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.ScorelineRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FieldValues") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("field_values") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("MajorId") + .HasColumnType("uuid") + .HasColumnName("major_id"); + + b.Property("MajorName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("major_name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SchoolId") + .HasColumnType("uuid") + .HasColumnName("school_id"); + + b.Property("SchoolName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("school_name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("Year") + .HasColumnType("integer") + .HasColumnName("year"); + + b.HasKey("Id") + .HasName("pk_scoreline_records"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_scoreline_records_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_scoreline_records_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "MajorId") + .HasDatabaseName("ix_scoreline_records_tenant_id_major_id"); + + b.HasIndex("TenantId", "SchoolId") + .HasDatabaseName("ix_scoreline_records_tenant_id_school_id"); + + b.HasIndex("TenantId", "Year") + .HasDatabaseName("ix_scoreline_records_tenant_id_year"); + + b.HasIndex("TenantId", "RegionId", "SchoolId", "MajorId", "Year") + .HasDatabaseName("ix_scoreline_records_tenant_id_region_id_school_id_major_id_ye~"); + + b.HasIndex("TenantId", "Year", "SchoolName", "MajorName", "Id") + .IsDescending(false, true, false, false, false) + .HasDatabaseName("ix_scoreline_records_tenant_id_year_school_name_major_name_id"); + + b.ToTable("scoreline_records", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Subject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Icon") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("icon"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("MajorId") + .HasColumnType("uuid") + .HasColumnName("major_id"); + + b.Property("MajorLegacyIds") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("major_legacy_ids") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SchoolId") + .HasColumnType("uuid") + .HasColumnName("school_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Stats") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("stats") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Type") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_subjects"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_subjects_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_subjects_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "MajorId") + .HasDatabaseName("ix_subjects_tenant_id_major_id"); + + b.HasIndex("TenantId", "ModuleId") + .HasDatabaseName("ix_subjects_tenant_id_module_id"); + + b.HasIndex("TenantId", "NodeId") + .HasDatabaseName("ix_subjects_tenant_id_node_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_subjects_tenant_id_region_id"); + + b.HasIndex("TenantId", "SchoolId") + .HasDatabaseName("ix_subjects_tenant_id_school_id"); + + b.ToTable("subjects", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.TaxonomyNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Depth") + .HasColumnType("integer") + .HasColumnName("depth"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("NodeType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("node_type"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("ParentOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("parent_owner_tenant_id"); + + b.Property("Path") + .HasColumnType("ltree") + .HasColumnName("path"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_taxonomy_nodes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_taxonomy_nodes_tenant_id_id"); + + b.HasIndex("ParentOwnerTenantId", "ParentId") + .HasDatabaseName("ix_taxonomy_nodes_parent_owner_tenant_id_parent_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_taxonomy_nodes_tenant_id_code"); + + b.ToTable("taxonomy_nodes", null, t => + { + t.HasCheckConstraint("ck_taxonomy_nodes_parent_pair", "(parent_owner_tenant_id is null) = (parent_id is null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.ActivationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AgentUserId") + .HasColumnType("uuid") + .HasColumnName("agent_user_id"); + + b.Property("BatchId") + .HasColumnType("uuid") + .HasColumnName("batch_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("code"); + + b.Property("CouponCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("coupon_code"); + + b.Property("CouponRedemptionId") + .HasColumnType("uuid") + .HasColumnName("coupon_redemption_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Days") + .HasColumnType("integer") + .HasColumnName("days"); + + b.Property("IsUsed") + .HasColumnType("boolean") + .HasColumnName("is_used"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Remark") + .HasColumnType("text") + .HasColumnName("remark"); + + b.Property("SaleType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("sale_type"); + + b.Property("SoldTo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("sold_to"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UnitPriceCents") + .HasColumnType("integer") + .HasColumnName("unit_price_cents"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_at"); + + b.Property("UsedBy") + .HasColumnType("uuid") + .HasColumnName("used_by"); + + b.Property("UsedRegionId") + .HasColumnType("uuid") + .HasColumnName("used_region_id"); + + b.HasKey("Id") + .HasName("pk_activation_codes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_activation_codes_tenant_id_id"); + + b.HasIndex("AgentUserId") + .HasDatabaseName("ix_activation_codes_agent_user_id"); + + b.HasIndex("UsedBy") + .HasDatabaseName("ix_activation_codes_used_by"); + + b.HasIndex("TenantId", "BatchId") + .HasDatabaseName("ix_activation_codes_tenant_id_batch_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_activation_codes_tenant_id_code"); + + b.HasIndex("TenantId", "CouponRedemptionId") + .HasDatabaseName("ix_activation_codes_tenant_id_coupon_redemption_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_activation_codes_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "UsedRegionId") + .HasDatabaseName("ix_activation_codes_tenant_id_used_region_id"); + + b.ToTable("activation_codes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CodeBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CampaignName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("campaign_name"); + + b.Property("Channel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("channel"); + + b.Property("CommissionRate") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)") + .HasColumnName("commission_rate"); + + b.Property("CostPriceCents") + .HasColumnType("integer") + .HasColumnName("cost_price_cents"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("Days") + .HasColumnType("integer") + .HasColumnName("days"); + + b.Property("DefaultUnitPriceCents") + .HasColumnType("integer") + .HasColumnName("default_unit_price_cents"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("issued_at"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyRegionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_region_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("Remark") + .HasColumnType("text") + .HasColumnName("remark"); + + b.Property("SaleType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("sale_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalCount") + .HasColumnType("integer") + .HasColumnName("total_count"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_code_batches"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_code_batches_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_code_batches_tenant_id_legacy_id"); + + b.ToTable("code_batches", null, t => + { + t.HasCheckConstraint("ck_code_batches_amounts", "default_unit_price_cents >= 0 and cost_price_cents >= 0"); + + t.HasCheckConstraint("ck_code_batches_counts", "total_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceAdjustmentVoucher", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("BatchId") + .HasColumnType("uuid") + .HasColumnName("batch_id"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("closed_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("currency"); + + b.Property("Direction") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("direction"); + + b.Property("IssueId") + .HasColumnType("uuid") + .HasColumnName("issue_id"); + + b.Property("ItemId") + .HasColumnType("uuid") + .HasColumnName("item_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("PaymentId") + .HasColumnType("uuid") + .HasColumnName("payment_id"); + + b.Property("ProofAssetKey") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("proof_asset_key"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("RefundRequestId") + .HasColumnType("uuid") + .HasColumnName("refund_request_id"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("reviewed_at"); + + b.Property("ReviewedBy") + .HasColumnType("uuid") + .HasColumnName("reviewed_by"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("VoucherNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("voucher_no"); + + b.HasKey("Id") + .HasName("pk_commerce_adjustment_vouchers"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commerce_adjustment_vouchers_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_commerce_adjustment_vouchers_created_by"); + + b.HasIndex("ReviewedBy") + .HasDatabaseName("ix_commerce_adjustment_vouchers_reviewed_by"); + + b.HasIndex("TenantId", "BatchId") + .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_batch_id"); + + b.HasIndex("TenantId", "ItemId") + .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_item_id"); + + b.HasIndex("TenantId", "OrderId") + .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_order_id"); + + b.HasIndex("TenantId", "PaymentId") + .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_payment_id"); + + b.HasIndex("TenantId", "RefundRequestId") + .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_refund_request_id"); + + b.HasIndex("TenantId", "VoucherNo") + .IsUnique() + .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_voucher_no"); + + b.HasIndex("TenantId", "IssueId", "CreatedAt") + .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_issue_id_created_at"); + + b.HasIndex("TenantId", "Status", "CreatedAt") + .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_status_created_at"); + + b.ToTable("commerce_adjustment_vouchers", null, t => + { + t.HasCheckConstraint("ck_commerce_adjustment_vouchers_amount", "amount_cents > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceAdjustmentVoucherEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActorUserId") + .HasColumnType("uuid") + .HasColumnName("actor_user_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Details") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("details") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("FromStatus") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("from_status"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("note"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("ToStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("to_status"); + + b.Property("VoucherId") + .HasColumnType("uuid") + .HasColumnName("voucher_id"); + + b.HasKey("Id") + .HasName("pk_commerce_adjustment_voucher_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commerce_adjustment_voucher_events_tenant_id_id"); + + b.HasIndex("ActorUserId") + .HasDatabaseName("ix_commerce_adjustment_voucher_events_actor_user_id"); + + b.HasIndex("TenantId", "VoucherId", "CreatedAt") + .HasDatabaseName("ix_commerce_adjustment_voucher_events_tenant_id_voucher_id_cre~"); + + b.ToTable("commerce_adjustment_voucher_events", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceReconciliationBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("BillDate") + .HasColumnType("date") + .HasColumnName("bill_date"); + + b.Property("BillType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("bill_type"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("DuplicateCount") + .HasColumnType("integer") + .HasColumnName("duplicate_count"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FeeCents") + .HasColumnType("integer") + .HasColumnName("fee_cents"); + + b.Property("IgnoredCount") + .HasColumnType("integer") + .HasColumnName("ignored_count"); + + b.Property("MatchedCount") + .HasColumnType("integer") + .HasColumnName("matched_count"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("MismatchCount") + .HasColumnType("integer") + .HasColumnName("mismatch_count"); + + b.Property("MissingLocalCount") + .HasColumnType("integer") + .HasColumnName("missing_local_count"); + + b.Property("MissingProviderCount") + .HasColumnType("integer") + .HasColumnName("missing_provider_count"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("RefundAmountCents") + .HasColumnType("integer") + .HasColumnName("refund_amount_cents"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("source"); + + b.Property("SourceHash") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("source_hash"); + + b.Property("SourceName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("source_name"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalCount") + .HasColumnType("integer") + .HasColumnName("total_count"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_commerce_reconciliation_batches"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commerce_reconciliation_batches_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_commerce_reconciliation_batches_created_by"); + + b.HasIndex("TenantId", "Status", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_batches_tenant_id_status_created_at"); + + b.HasIndex("TenantId", "Provider", "BillDate", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_batches_tenant_id_provider_bill_dat~"); + + b.ToTable("commerce_reconciliation_batches", null, t => + { + t.HasCheckConstraint("ck_commerce_reconciliation_batches_amounts", "amount_cents >= 0 and refund_amount_cents >= 0"); + + t.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"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceReconciliationIssue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("AssignedTo") + .HasColumnType("uuid") + .HasColumnName("assigned_to"); + + b.Property("BatchId") + .HasColumnType("uuid") + .HasColumnName("batch_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("due_at"); + + b.Property("IssueCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("issue_code"); + + b.Property("IssueNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("issue_no"); + + b.Property("ItemId") + .HasColumnType("uuid") + .HasColumnName("item_id"); + + b.Property("MatchStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("match_status"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("OrderNo") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("order_no"); + + b.Property("PaymentId") + .HasColumnType("uuid") + .HasColumnName("payment_id"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("ProviderRefundNo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("provider_refund_no"); + + b.Property("ProviderTradeNo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("provider_trade_no"); + + b.Property("RefundAmountCents") + .HasColumnType("integer") + .HasColumnName("refund_amount_cents"); + + b.Property("RefundNo") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("refund_no"); + + b.Property("RefundRequestId") + .HasColumnType("uuid") + .HasColumnName("refund_request_id"); + + b.Property("ResolutionNote") + .HasColumnType("text") + .HasColumnName("resolution_note"); + + b.Property("ResolutionType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resolution_type"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("resolved_at"); + + b.Property("ResolvedBy") + .HasColumnType("uuid") + .HasColumnName("resolved_by"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("severity"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("Summary") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("summary"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TransactionType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("transaction_type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_commerce_reconciliation_issues"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commerce_reconciliation_issues_tenant_id_id"); + + b.HasIndex("AssignedTo") + .HasDatabaseName("ix_commerce_reconciliation_issues_assigned_to"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_commerce_reconciliation_issues_created_by"); + + b.HasIndex("ResolvedBy") + .HasDatabaseName("ix_commerce_reconciliation_issues_resolved_by"); + + b.HasIndex("TenantId", "IssueNo") + .IsUnique() + .HasDatabaseName("ix_commerce_reconciliation_issues_tenant_id_issue_no"); + + b.HasIndex("TenantId", "ItemId") + .IsUnique() + .HasDatabaseName("ix_commerce_reconciliation_issues_tenant_id_item_id") + .HasFilter("item_id is not null and status in ('open', 'investigating', 'escalated')"); + + b.HasIndex("TenantId", "OrderId") + .HasDatabaseName("ix_commerce_reconciliation_issues_tenant_id_order_id"); + + b.HasIndex("TenantId", "RefundRequestId") + .HasDatabaseName("ix_commerce_reconciliation_issues_tenant_id_refund_request_id"); + + b.HasIndex("TenantId", "OrderNo", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_issues_tenant_id_order_no_created_at"); + + b.HasIndex("TenantId", "PaymentId", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_issues_tenant_id_payment_id_created~"); + + b.HasIndex("TenantId", "AssignedTo", "Status", "DueAt") + .HasDatabaseName("ix_commerce_reconciliation_issues_tenant_id_assigned_to_status~"); + + b.HasIndex("TenantId", "BatchId", "Status", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_issues_tenant_id_batch_id_status_cr~"); + + b.HasIndex("TenantId", "Status", "Severity", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_issues_tenant_id_status_severity_cr~"); + + b.ToTable("commerce_reconciliation_issues", null, t => + { + t.HasCheckConstraint("ck_commerce_reconciliation_issues_amounts", "amount_cents >= 0 and refund_amount_cents >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceReconciliationIssueEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActorUserId") + .HasColumnType("uuid") + .HasColumnName("actor_user_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Details") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("details") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("event_type"); + + b.Property("FromStatus") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("from_status"); + + b.Property("IssueId") + .HasColumnType("uuid") + .HasColumnName("issue_id"); + + b.Property("Note") + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("ToStatus") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("to_status"); + + b.HasKey("Id") + .HasName("pk_commerce_reconciliation_issue_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commerce_reconciliation_issue_events_tenant_id_id"); + + b.HasIndex("ActorUserId") + .HasDatabaseName("ix_commerce_reconciliation_issue_events_actor_user_id"); + + b.HasIndex("TenantId", "IssueId", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_issue_events_tenant_id_issue_id_cre~"); + + b.ToTable("commerce_reconciliation_issue_events", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceReconciliationItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("BatchId") + .HasColumnType("uuid") + .HasColumnName("batch_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Details") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("details") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("FeeCents") + .HasColumnType("integer") + .HasColumnName("fee_cents"); + + b.Property("IssueCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("issue_code"); + + b.Property("LocalStatus") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("local_status"); + + b.Property("MatchStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("match_status"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("OrderNo") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("order_no"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("paid_at"); + + b.Property("PaymentId") + .HasColumnType("uuid") + .HasColumnName("payment_id"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("ProviderRefundNo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("provider_refund_no"); + + b.Property("ProviderStatus") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("provider_status"); + + b.Property("ProviderTradeNo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("provider_trade_no"); + + b.Property("RefundAmountCents") + .HasColumnType("integer") + .HasColumnName("refund_amount_cents"); + + b.Property("RefundNo") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("refund_no"); + + b.Property("RefundRequestId") + .HasColumnType("uuid") + .HasColumnName("refund_request_id"); + + b.Property("RefundedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("refunded_at"); + + b.Property("RowNo") + .HasColumnType("integer") + .HasColumnName("row_no"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("severity"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TransactionType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("transaction_type"); + + b.HasKey("Id") + .HasName("pk_commerce_reconciliation_items"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commerce_reconciliation_items_tenant_id_id"); + + b.HasIndex("TenantId", "OrderId") + .HasDatabaseName("ix_commerce_reconciliation_items_tenant_id_order_id"); + + b.HasIndex("TenantId", "PaymentId") + .HasDatabaseName("ix_commerce_reconciliation_items_tenant_id_payment_id"); + + b.HasIndex("TenantId", "RefundRequestId") + .HasDatabaseName("ix_commerce_reconciliation_items_tenant_id_refund_request_id"); + + b.HasIndex("TenantId", "BatchId", "RowNo") + .IsUnique() + .HasDatabaseName("ix_commerce_reconciliation_items_tenant_id_batch_id_row_no"); + + b.HasIndex("TenantId", "OrderNo", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_items_tenant_id_order_no_created_at"); + + b.HasIndex("TenantId", "BatchId", "MatchStatus", "RowNo") + .HasDatabaseName("ix_commerce_reconciliation_items_tenant_id_batch_id_match_stat~"); + + b.HasIndex("TenantId", "Provider", "ProviderTradeNo", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_items_tenant_id_provider_provider_t~"); + + b.HasIndex("TenantId", "RefundNo", "ProviderRefundNo", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_items_tenant_id_refund_no_provider_~"); + + b.ToTable("commerce_reconciliation_items", null, t => + { + t.HasCheckConstraint("ck_commerce_reconciliation_items_amounts", "amount_cents >= 0 and refund_amount_cents >= 0"); + + t.HasCheckConstraint("ck_commerce_reconciliation_items_row", "row_no > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceRefundEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActorUserId") + .HasColumnType("uuid") + .HasColumnName("actor_user_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Details") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("details") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("event_type"); + + b.Property("FromStatus") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("from_status"); + + b.Property("RefundRequestId") + .HasColumnType("uuid") + .HasColumnName("refund_request_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("ToStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("to_status"); + + b.HasKey("Id") + .HasName("pk_commerce_refund_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commerce_refund_events_tenant_id_id"); + + b.HasIndex("ActorUserId") + .HasDatabaseName("ix_commerce_refund_events_actor_user_id"); + + b.HasIndex("TenantId", "RefundRequestId", "CreatedAt") + .HasDatabaseName("ix_commerce_refund_events_tenant_id_refund_request_id_created_~"); + + b.ToTable("commerce_refund_events", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceRefundRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("cancelled_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EntitlementAction") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("entitlement_action"); + + b.Property("FailedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("failed_at"); + + b.Property("FailureReason") + .HasColumnType("text") + .HasColumnName("failure_reason"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("PaymentId") + .HasColumnType("uuid") + .HasColumnName("payment_id"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at"); + + b.Property("ProcessedBy") + .HasColumnType("uuid") + .HasColumnName("processed_by"); + + b.Property("Provider") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("ProviderRefundNo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("provider_refund_no"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("RefundNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("refund_no"); + + b.Property("RequestedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("requested_at") + .HasDefaultValueSql("now()"); + + b.Property("RequestedBy") + .HasColumnType("uuid") + .HasColumnName("requested_by"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("reviewed_at"); + + b.Property("ReviewedBy") + .HasColumnType("uuid") + .HasColumnName("reviewed_by"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("SucceededAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("succeeded_at"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_commerce_refund_requests"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commerce_refund_requests_tenant_id_id"); + + b.HasIndex("ProcessedBy") + .HasDatabaseName("ix_commerce_refund_requests_processed_by"); + + b.HasIndex("RequestedBy") + .HasDatabaseName("ix_commerce_refund_requests_requested_by"); + + b.HasIndex("ReviewedBy") + .HasDatabaseName("ix_commerce_refund_requests_reviewed_by"); + + b.HasIndex("TenantId", "PaymentId") + .HasDatabaseName("ix_commerce_refund_requests_tenant_id_payment_id"); + + b.HasIndex("TenantId", "RefundNo") + .IsUnique() + .HasDatabaseName("ix_commerce_refund_requests_tenant_id_refund_no"); + + b.HasIndex("TenantId", "OrderId", "CreatedAt") + .HasDatabaseName("ix_commerce_refund_requests_tenant_id_order_id_created_at"); + + b.HasIndex("TenantId", "Status", "CreatedAt") + .HasDatabaseName("ix_commerce_refund_requests_tenant_id_status_created_at"); + + b.ToTable("commerce_refund_requests", null, t => + { + t.HasCheckConstraint("ck_commerce_refund_requests_amount", "amount_cents > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DiscountType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("discount_type"); + + b.Property("DiscountValue") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("discount_value"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("MaxUses") + .HasColumnType("integer") + .HasColumnName("max_uses"); + + b.Property("PlanId") + .HasColumnType("uuid") + .HasColumnName("plan_id"); + + b.Property("Remark") + .HasColumnType("text") + .HasColumnName("remark"); + + b.Property("Source") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UsedCount") + .HasColumnType("integer") + .HasColumnName("used_count"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone") + .HasColumnName("valid_from"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone") + .HasColumnName("valid_to"); + + b.HasKey("Id") + .HasName("pk_coupons"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_coupons_tenant_id_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_coupons_tenant_id_code"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_coupons_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "PlanId") + .HasDatabaseName("ix_coupons_tenant_id_plan_id"); + + b.ToTable("coupons", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CouponRedemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_at"); + + b.Property("CouponCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("coupon_code"); + + b.Property("CouponId") + .HasColumnType("uuid") + .HasColumnName("coupon_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DiscountAppliedCents") + .HasColumnType("integer") + .HasColumnName("discount_applied_cents"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("PlanId") + .HasColumnType("uuid") + .HasColumnName("plan_id"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("Remark") + .HasColumnType("text") + .HasColumnName("remark"); + + b.Property("Source") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_coupon_redemptions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_coupon_redemptions_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_coupon_redemptions_user_id"); + + b.HasIndex("TenantId", "CouponId") + .HasDatabaseName("ix_coupon_redemptions_tenant_id_coupon_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_coupon_redemptions_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "PlanId") + .HasDatabaseName("ix_coupon_redemptions_tenant_id_plan_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_coupon_redemptions_tenant_id_region_id"); + + b.HasIndex("TenantId", "OrderId", "UserId") + .HasDatabaseName("ix_coupon_redemptions_tenant_id_order_id_user_id") + .HasFilter("order_id is not null"); + + b.HasIndex("TenantId", "UserId", "CouponId") + .IsUnique() + .HasDatabaseName("ix_coupon_redemptions_tenant_id_user_id_coupon_id") + .HasFilter("coupon_id is not null"); + + b.ToTable("coupon_redemptions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Entitlement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EntitlementType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("entitlement_type"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("LegacySourceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_source_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.Property("RevokedBy") + .HasColumnType("uuid") + .HasColumnName("revoked_by"); + + b.Property("RevokedReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("revoked_reason"); + + b.Property("ScopeId") + .HasColumnType("uuid") + .HasColumnName("scope_id"); + + b.Property("ScopeType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("scope_type"); + + b.Property("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_type"); + + b.Property("StartsAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_at") + .HasDefaultValueSql("now()"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_entitlements"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_entitlements_tenant_id_id"); + + b.HasIndex("RevokedBy") + .HasDatabaseName("ix_entitlements_revoked_by"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_entitlements_user_id"); + + b.HasIndex("TenantId", "UserId", "Status", "ExpiresAt") + .HasDatabaseName("ix_entitlements_tenant_id_user_id_status_expires_at"); + + b.ToTable("entitlements", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Days") + .HasColumnType("integer") + .HasColumnName("days"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyPlanId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_plan_id"); + + b.Property("LegacyRegionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_region_id"); + + b.Property("LegacyUserId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_user_id"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("order_no"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("paid_at"); + + b.Property("PayMethod") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("pay_method"); + + b.Property("PayProvider") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("pay_provider"); + + b.Property("PlanId") + .HasColumnType("uuid") + .HasColumnName("plan_id"); + + b.Property("ProductName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("product_name"); + + b.Property("ProductType") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("product_type"); + + b.Property("RawPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("raw_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RefundedAmountCents") + .HasColumnType("integer") + .HasColumnName("refunded_amount_cents"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TradeNo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("trade_no"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_orders"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_orders_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_orders_user_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_orders_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "OrderNo") + .IsUnique() + .HasDatabaseName("ix_orders_tenant_id_order_no"); + + b.HasIndex("TenantId", "PlanId") + .HasDatabaseName("ix_orders_tenant_id_plan_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_orders_tenant_id_region_id"); + + b.HasIndex("TenantId", "UserId", "CreatedAt") + .HasDatabaseName("ix_orders_tenant_id_user_id_created_at"); + + b.ToTable("orders", null, t => + { + t.HasCheckConstraint("ck_orders_amount", "amount_cents >= 0"); + + t.HasCheckConstraint("ck_orders_refunded_amount", "refunded_amount_cents >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ItemId") + .HasColumnType("uuid") + .HasColumnName("item_id"); + + b.Property("ItemType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("item_type"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("quantity"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalAmountCents") + .HasColumnType("integer") + .HasColumnName("total_amount_cents"); + + b.Property("UnitAmountCents") + .HasColumnType("integer") + .HasColumnName("unit_amount_cents"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_order_items"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_order_items_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_order_items_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "OrderId") + .HasDatabaseName("ix_order_items_tenant_id_order_id"); + + b.ToTable("order_items", null, t => + { + t.HasCheckConstraint("ck_order_items_amounts", "unit_amount_cents >= 0 and total_amount_cents >= 0"); + + t.HasCheckConstraint("ck_order_items_quantity", "quantity > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyOrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_order_id"); + + b.Property("Method") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("method"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("paid_at"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("ProviderTradeNo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("provider_trade_no"); + + b.Property("RawPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("raw_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RefundedAmountCents") + .HasColumnType("integer") + .HasColumnName("refunded_amount_cents"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_payments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_payments_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_payments_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "OrderId", "UpdatedAt") + .HasDatabaseName("ix_payments_tenant_id_order_id_updated_at"); + + b.HasIndex("TenantId", "Provider", "ProviderTradeNo") + .IsUnique() + .HasDatabaseName("ix_payments_tenant_id_provider_provider_trade_no") + .HasFilter("provider_trade_no is not null"); + + b.ToTable("payments", null, t => + { + t.HasCheckConstraint("ck_payments_amount", "amount_cents >= 0"); + + t.HasCheckConstraint("ck_payments_refunded_amount", "refunded_amount_cents >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PaymentEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("EventId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("event_id"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("event_type"); + + b.Property("Payload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PaymentId") + .HasColumnType("uuid") + .HasColumnName("payment_id"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("SignatureValid") + .HasColumnType("boolean") + .HasColumnName("signature_valid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_payment_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_payment_events_tenant_id_id"); + + b.HasIndex("TenantId", "PaymentId") + .HasDatabaseName("ix_payment_events_tenant_id_payment_id"); + + b.HasIndex("TenantId", "Provider", "EventId") + .IsUnique() + .HasDatabaseName("ix_payment_events_tenant_id_provider_event_id"); + + b.ToTable("payment_events", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PointActivityClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ClaimedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Points") + .HasColumnType("integer") + .HasColumnName("points"); + + b.Property("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_type"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TaskId") + .HasColumnType("uuid") + .HasColumnName("task_id"); + + b.Property("TaskKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("task_key"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_point_activity_claims"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_point_activity_claims_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_point_activity_claims_user_id"); + + b.HasIndex("TenantId", "TaskId") + .HasDatabaseName("ix_point_activity_claims_tenant_id_task_id"); + + b.HasIndex("TenantId", "UserId", "CreatedAt") + .HasDatabaseName("ix_point_activity_claims_tenant_id_user_id_created_at"); + + b.HasIndex("TenantId", "UserId", "TaskId", "SourceType", "SourceId") + .IsUnique() + .HasDatabaseName("ix_point_activity_claims_tenant_id_user_id_task_id_source_type~") + .HasFilter("source_type is not null and source_id is not null"); + + b.ToTable("point_activity_claims", null, t => + { + t.HasCheckConstraint("ck_point_activity_claims_points", "points > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PointActivityTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("MaxClaimsPerUser") + .HasColumnType("integer") + .HasColumnName("max_claims_per_user"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Points") + .HasColumnType("integer") + .HasColumnName("points"); + + b.Property("Rules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("rules") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TaskKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("task_key"); + + b.Property("TaskType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("task_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_point_activity_tasks"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_point_activity_tasks_tenant_id_id"); + + b.HasIndex("TenantId", "TaskKey") + .IsUnique() + .HasDatabaseName("ix_point_activity_tasks_tenant_id_task_key"); + + b.HasIndex("TenantId", "Status", "SortOrder") + .HasDatabaseName("ix_point_activity_tasks_tenant_id_status_sort_order"); + + b.ToTable("point_activity_tasks", null, t => + { + t.HasCheckConstraint("ck_point_activity_tasks_max_claims", "max_claims_per_user > 0"); + + t.HasCheckConstraint("ck_point_activity_tasks_points", "points > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PointExchangeItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Days") + .HasColumnType("integer") + .HasColumnName("days"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("FulfillmentPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("fulfillment_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ItemKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("item_key"); + + b.Property("ItemType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("item_type"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PointsCost") + .HasColumnType("integer") + .HasColumnName("points_cost"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("Stock") + .HasColumnType("integer") + .HasColumnName("stock"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_point_exchange_items"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_point_exchange_items_tenant_id_id"); + + b.HasIndex("TenantId", "ItemKey") + .IsUnique() + .HasDatabaseName("ix_point_exchange_items_tenant_id_item_key"); + + b.HasIndex("TenantId", "RegionId", "Status", "SortOrder") + .HasDatabaseName("ix_point_exchange_items_tenant_id_region_id_status_sort_order"); + + b.ToTable("point_exchange_items", null, t => + { + t.HasCheckConstraint("ck_point_exchange_items_days", "days is null or days >= 0"); + + t.HasCheckConstraint("ck_point_exchange_items_points_cost", "points_cost > 0"); + + t.HasCheckConstraint("ck_point_exchange_items_stock", "stock is null or stock >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PointExchangeOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("cancelled_at"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FulfillmentSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("fulfillment_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ItemId") + .HasColumnType("uuid") + .HasColumnName("item_id"); + + b.Property("ItemName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("item_name"); + + b.Property("ItemType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("item_type"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("order_no"); + + b.Property("OrderedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("ordered_at") + .HasDefaultValueSql("now()"); + + b.Property("PointsCost") + .HasColumnType("integer") + .HasColumnName("points_cost"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_point_exchange_orders"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_point_exchange_orders_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_point_exchange_orders_user_id"); + + b.HasIndex("TenantId", "ItemId") + .HasDatabaseName("ix_point_exchange_orders_tenant_id_item_id"); + + b.HasIndex("TenantId", "OrderNo") + .IsUnique() + .HasDatabaseName("ix_point_exchange_orders_tenant_id_order_no"); + + b.HasIndex("TenantId", "UserId", "CreatedAt") + .HasDatabaseName("ix_point_exchange_orders_tenant_id_user_id_created_at"); + + b.ToTable("point_exchange_orders", null, t => + { + t.HasCheckConstraint("ck_point_exchange_orders_points_cost", "points_cost > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Cover") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("cover"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DetailImages") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("detail_images") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyPriceText") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("legacy_price_text"); + + b.Property("Link") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("link"); + + b.Property("PreviewIframe") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("preview_iframe"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Tags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("tags") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("title"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_products"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_products_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_products_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId", "Type", "IsActive", "SortOrder") + .HasDatabaseName("ix_products_tenant_id_region_id_type_is_active_sort_order"); + + b.ToTable("products", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.SvipPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Badge") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("badge"); + + b.Property("CouponOnly") + .HasColumnType("boolean") + .HasColumnName("coupon_only"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Days") + .HasColumnType("integer") + .HasColumnName("days"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("OriginalPriceCents") + .HasColumnType("integer") + .HasColumnName("original_price_cents"); + + b.Property("PerDayLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("per_day_label"); + + b.Property("PriceCents") + .HasColumnType("integer") + .HasColumnName("price_cents"); + + b.Property("Recommended") + .HasColumnType("boolean") + .HasColumnName("recommended"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("VpEnabled") + .HasColumnType("boolean") + .HasColumnName("vp_enabled"); + + b.Property("VpProductId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("vp_product_id"); + + b.HasKey("Id") + .HasName("pk_svip_plans"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_svip_plans_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_svip_plans_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId", "IsActive", "SortOrder") + .HasDatabaseName("ix_svip_plans_tenant_id_region_id_is_active_sort_order"); + + b.ToTable("svip_plans", null, t => + { + t.HasCheckConstraint("ck_svip_plans_days", "days >= 0"); + + t.HasCheckConstraint("ck_svip_plans_price", "price_cents >= 0 and (original_price_cents is null or original_price_cents >= 0)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AiRecommendationReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContextPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("context_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("generated_at"); + + b.Property("InputPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("input_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("model"); + + b.Property("PromptVersion") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("prompt_version"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("provider"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("ResultPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("result_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_ai_recommendation_reports"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_ai_recommendation_reports_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_ai_recommendation_reports_user_id"); + + b.HasIndex("TenantId", "RegionId", "CreatedAt") + .HasDatabaseName("ix_ai_recommendation_reports_tenant_id_region_id_created_at"); + + b.HasIndex("TenantId", "UserId", "CreatedAt") + .HasDatabaseName("ix_ai_recommendation_reports_tenant_id_user_id_created_at"); + + b.ToTable("ai_recommendation_reports", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AppAsset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssetKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("asset_key"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("FileName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("file_name"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_app_assets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_app_assets_tenant_id_id"); + + b.HasIndex("TenantId", "AssetKey") + .IsUnique() + .HasDatabaseName("ix_app_assets_tenant_id_asset_key"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_app_assets_tenant_id_legacy_id"); + + b.ToTable("app_assets", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentAsset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessRules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("access_rules") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("AssetKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("asset_key"); + + b.Property("AssetType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("asset_type"); + + b.Property("Bucket") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("bucket"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("category"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CdnUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("cdn_url"); + + b.Property("ChecksumSha256") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("checksum_sha256"); + + b.Property("ContentNodeId") + .HasColumnType("uuid") + .HasColumnName("content_node_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("DownloadCount") + .HasColumnType("integer") + .HasColumnName("download_count"); + + b.Property("FileName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("file_name"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint") + .HasColumnName("file_size_bytes"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("MimeType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mime_type"); + + b.Property("ObjectKey") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("object_key"); + + b.Property("PreviewObjectKey") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("preview_object_key"); + + b.Property("PreviewStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("preview_status"); + + b.Property("PreviewUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("preview_url"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SecurityFlags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("security_flags") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SecurityScanProvider") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("security_scan_provider"); + + b.Property("SecurityScanStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("not_required") + .HasColumnName("security_scan_status"); + + b.Property("SecurityScanSummary") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("security_scan_summary") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SecurityScannedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("security_scanned_at"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("StorageProvider") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("storage_provider"); + + b.Property("SubjectId") + .HasColumnType("uuid") + .HasColumnName("subject_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.Property("UploadStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("upload_status"); + + b.Property("VerificationDetails") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("verification_details") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.Property("VerifiedBy") + .HasColumnType("uuid") + .HasColumnName("verified_by"); + + b.Property("VerifiedChecksumSha256") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("verified_checksum_sha256"); + + b.Property("VerifiedSizeBytes") + .HasColumnType("bigint") + .HasColumnName("verified_size_bytes"); + + b.Property("Visibility") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("visibility"); + + b.HasKey("Id") + .HasName("pk_content_assets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_assets_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_content_assets_created_by"); + + b.HasIndex("UpdatedBy") + .HasDatabaseName("ix_content_assets_updated_by"); + + b.HasIndex("VerifiedBy") + .HasDatabaseName("ix_content_assets_verified_by"); + + b.HasIndex("TenantId", "CategoryId") + .HasDatabaseName("ix_content_assets_tenant_id_category_id"); + + b.HasIndex("TenantId", "ChecksumSha256") + .HasDatabaseName("ix_content_assets_tenant_id_checksum_sha256") + .HasFilter("checksum_sha256 is not null"); + + b.HasIndex("TenantId", "ContentNodeId") + .HasDatabaseName("ix_content_assets_tenant_id_content_node_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_content_assets_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "PreviewStatus") + .HasDatabaseName("ix_content_assets_tenant_id_preview_status") + .HasFilter("preview_status <> 'none'"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_content_assets_tenant_id_region_id"); + + b.HasIndex("TenantId", "SubjectId") + .HasDatabaseName("ix_content_assets_tenant_id_subject_id"); + + b.HasIndex("TenantId", "StorageProvider", "Bucket", "ObjectKey") + .HasDatabaseName("ix_content_assets_tenant_id_storage_provider_bucket_object_key"); + + b.HasIndex("TenantId", "UploadStatus", "Status", "UpdatedAt") + .HasDatabaseName("ix_content_assets_tenant_id_upload_status_status_updated_at"); + + b.HasIndex("TenantId", "SecurityScanStatus", "UploadStatus", "Status", "UpdatedAt") + .HasDatabaseName("ix_content_assets_tenant_id_security_scan_status_upload_status~"); + + b.HasIndex("TenantId", "Status", "Visibility", "AssetType", "RegionId", "SubjectId", "CategoryId", "SortOrder") + .HasDatabaseName("ix_content_assets_tenant_id_status_visibility_asset_type_regio~"); + + b.ToTable("content_assets", null, t => + { + t.HasCheckConstraint("ck_content_assets_download_count", "download_count >= 0"); + + t.HasCheckConstraint("ck_content_assets_file_size", "file_size_bytes is null or file_size_bytes >= 0"); + + t.HasCheckConstraint("ck_content_assets_verified_checksum", "verified_checksum_sha256 is null or verified_checksum_sha256 ~ '^[a-f0-9]{64}$'"); + + t.HasCheckConstraint("ck_content_assets_verified_size", "verified_size_bytes is null or verified_size_bytes >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentAssetAccessEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("access_type"); + + b.Property("ActorRole") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("actor_role"); + + b.Property("AssetId") + .HasColumnType("uuid") + .HasColumnName("asset_id"); + + b.Property("AssetType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("asset_type"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DenyCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("deny_code"); + + b.Property("Disposition") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("disposition"); + + b.Property("ExpiresInSeconds") + .HasColumnType("integer") + .HasColumnName("expires_in_seconds"); + + b.Property("IpAddress") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("ip_address"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Result") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("result"); + + b.Property("SignatureMode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("signature_mode"); + + b.Property("StorageProvider") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("storage_provider"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserAgent") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("user_agent"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Visibility") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("visibility"); + + b.HasKey("Id") + .HasName("pk_content_asset_access_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_asset_access_events_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_content_asset_access_events_user_id"); + + b.HasIndex("TenantId", "AssetId", "CreatedAt") + .HasDatabaseName("ix_content_asset_access_events_tenant_id_asset_id_created_at"); + + b.HasIndex("TenantId", "UserId", "CreatedAt") + .HasDatabaseName("ix_content_asset_access_events_tenant_id_user_id_created_at") + .HasFilter("user_id is not null"); + + b.HasIndex("TenantId", "AccessType", "Result", "CreatedAt") + .HasDatabaseName("ix_content_asset_access_events_tenant_id_access_type_result_cr~"); + + b.ToTable("content_asset_access_events", null, t => + { + t.HasCheckConstraint("ck_content_asset_access_events_expiry", "expires_in_seconds is null or expires_in_seconds > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentAssetSecurityScanEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssetId") + .HasColumnType("uuid") + .HasColumnName("asset_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Details") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("details") + .HasDefaultValueSql("'{}'::jsonb"); + + b.PrimitiveCollection("IssueCodes") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text[]") + .HasColumnName("issue_codes") + .HasDefaultValueSql("'{}'::text[]"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("provider"); + + b.Property("RiskLevel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("risk_level"); + + b.Property("ScanStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("scan_status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_content_asset_security_scan_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_asset_security_scan_events_tenant_id_id"); + + b.HasIndex("TenantId", "AssetId", "CreatedAt") + .HasDatabaseName("ix_content_asset_security_scan_events_tenant_id_asset_id_creat~"); + + b.HasIndex("TenantId", "ScanStatus", "RiskLevel", "CreatedAt") + .HasDatabaseName("ix_content_asset_security_scan_events_tenant_id_scan_status_ri~"); + + b.ToTable("content_asset_security_scan_events", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessRules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("access_rules") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("EntryKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("entry_key"); + + b.Property("EntryType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("entry_type"); + + b.Property("Icon") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("icon"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LayoutConfig") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("layout_config") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("Route") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("route"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.Property("Visibility") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("visibility"); + + b.HasKey("Id") + .HasName("pk_content_entries"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_entries_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_content_entries_created_by"); + + b.HasIndex("UpdatedBy") + .HasDatabaseName("ix_content_entries_updated_by"); + + b.HasIndex("TenantId", "EntryKey") + .IsUnique() + .HasDatabaseName("ix_content_entries_tenant_id_entry_key"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_content_entries_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId", "EntryType", "IsActive", "SortOrder") + .HasDatabaseName("ix_content_entries_tenant_id_region_id_entry_type_is_active_so~"); + + b.ToTable("content_entries", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentImportIssue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Details") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("details") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("FieldPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("field_path"); + + b.Property("ItemId") + .HasColumnType("uuid") + .HasColumnName("item_id"); + + b.Property("JobId") + .HasColumnType("uuid") + .HasColumnName("job_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("message"); + + b.Property("RowNo") + .HasColumnType("integer") + .HasColumnName("row_no"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("severity"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_content_import_issues"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_import_issues_tenant_id_id"); + + b.HasIndex("TenantId", "ItemId") + .HasDatabaseName("ix_content_import_issues_tenant_id_item_id"); + + b.HasIndex("TenantId", "JobId", "Severity", "RowNo") + .HasDatabaseName("ix_content_import_issues_tenant_id_job_id_severity_row_no"); + + b.ToTable("content_import_issues", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentImportItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("content_hash"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ExternalId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("external_id"); + + b.Property("IssuesCount") + .HasColumnType("integer") + .HasColumnName("issues_count"); + + b.Property("JobId") + .HasColumnType("uuid") + .HasColumnName("job_id"); + + b.Property("NormalizedPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("normalized_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RowNo") + .HasColumnType("integer") + .HasColumnName("row_no"); + + b.Property("SourcePayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("source_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TargetId") + .HasColumnType("uuid") + .HasColumnName("target_id"); + + b.Property("TargetType") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("target_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_content_import_items"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_import_items_tenant_id_id"); + + b.HasIndex("TenantId", "JobId", "RowNo") + .IsUnique() + .HasDatabaseName("ix_content_import_items_tenant_id_job_id_row_no"); + + b.HasIndex("TenantId", "JobId", "Status", "RowNo") + .HasDatabaseName("ix_content_import_items_tenant_id_job_id_status_row_no"); + + b.ToTable("content_import_items", null, t => + { + t.HasCheckConstraint("ck_content_import_items_issues_count", "issues_count >= 0"); + + t.HasCheckConstraint("ck_content_import_items_row_no", "row_no >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentImportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("DryRun") + .HasColumnType("boolean") + .HasColumnName("dry_run"); + + b.Property("ErrorCount") + .HasColumnType("integer") + .HasColumnName("error_count"); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_at"); + + b.Property("ImportType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("import_type"); + + b.Property("InsertedCount") + .HasColumnType("integer") + .HasColumnName("inserted_count"); + + b.Property("NormalizedPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("normalized_payload") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("RawPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("raw_payload") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("SkippedCount") + .HasColumnType("integer") + .HasColumnName("skipped_count"); + + b.Property("SourceFormat") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("source_format"); + + b.Property("SourceHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_hash"); + + b.Property("SourceName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("source_name"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("Summary") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("summary") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TargetCategoryId") + .HasColumnType("uuid") + .HasColumnName("target_category_id"); + + b.Property("TargetContentNodeId") + .HasColumnType("uuid") + .HasColumnName("target_content_node_id"); + + b.Property("TargetQuestionBankId") + .HasColumnType("uuid") + .HasColumnName("target_question_bank_id"); + + b.Property("TargetRegionId") + .HasColumnType("uuid") + .HasColumnName("target_region_id"); + + b.Property("TargetSubjectId") + .HasColumnType("uuid") + .HasColumnName("target_subject_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalCount") + .HasColumnType("integer") + .HasColumnName("total_count"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedCount") + .HasColumnType("integer") + .HasColumnName("updated_count"); + + b.Property("ValidCount") + .HasColumnType("integer") + .HasColumnName("valid_count"); + + b.Property("WarningCount") + .HasColumnType("integer") + .HasColumnName("warning_count"); + + b.HasKey("Id") + .HasName("pk_content_import_jobs"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_import_jobs_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_content_import_jobs_created_by"); + + b.HasIndex("TenantId", "TargetCategoryId") + .HasDatabaseName("ix_content_import_jobs_tenant_id_target_category_id"); + + b.HasIndex("TenantId", "TargetContentNodeId") + .HasDatabaseName("ix_content_import_jobs_tenant_id_target_content_node_id"); + + b.HasIndex("TenantId", "TargetQuestionBankId") + .HasDatabaseName("ix_content_import_jobs_tenant_id_target_question_bank_id"); + + b.HasIndex("TenantId", "TargetRegionId") + .HasDatabaseName("ix_content_import_jobs_tenant_id_target_region_id"); + + b.HasIndex("TenantId", "TargetSubjectId") + .HasDatabaseName("ix_content_import_jobs_tenant_id_target_subject_id"); + + b.HasIndex("TenantId", "ImportType", "Status", "CreatedAt") + .HasDatabaseName("ix_content_import_jobs_tenant_id_import_type_status_created_at"); + + b.ToTable("content_import_jobs", null, t => + { + t.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"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessRules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("access_rules") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("Depth") + .HasColumnType("integer") + .HasColumnName("depth"); + + b.Property("EntryId") + .HasColumnType("uuid") + .HasColumnName("entry_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsLeaf") + .HasColumnType("boolean") + .HasColumnName("is_leaf"); + + b.Property("IsSelectable") + .HasColumnType("boolean") + .HasColumnName("is_selectable"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("MarkerConfig") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("marker_config") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("MarkerType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("marker_type"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("NodeKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node_key"); + + b.Property("NodeType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("node_type"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .HasColumnType("ltree") + .HasColumnName("path"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.HasKey("Id") + .HasName("pk_content_nodes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_nodes_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_content_nodes_created_by"); + + b.HasIndex("Path") + .HasDatabaseName("ix_content_nodes_path"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Path"), "gist"); + + b.HasIndex("UpdatedBy") + .HasDatabaseName("ix_content_nodes_updated_by"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_content_nodes_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "MarkerType") + .HasDatabaseName("ix_content_nodes_tenant_id_marker_type") + .HasFilter("marker_type is not null"); + + b.HasIndex("TenantId", "ParentId") + .HasDatabaseName("ix_content_nodes_tenant_id_parent_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_content_nodes_tenant_id_region_id"); + + b.HasIndex("TenantId", "EntryId", "NodeKey") + .IsUnique() + .HasDatabaseName("ix_content_nodes_tenant_id_entry_id_node_key"); + + b.HasIndex("TenantId", "EntryId", "ParentId", "SortOrder") + .HasDatabaseName("ix_content_nodes_tenant_id_entry_id_parent_id_sort_order"); + + b.ToTable("content_nodes", null, t => + { + t.HasCheckConstraint("ck_content_nodes_depth", "depth >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.HandbookChapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentNodeId") + .HasColumnType("uuid") + .HasColumnName("content_node_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EntryId") + .HasColumnType("uuid") + .HasColumnName("entry_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SourceHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_hash"); + + b.Property("SubjectId") + .HasColumnType("uuid") + .HasColumnName("subject_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_handbook_chapters"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_handbook_chapters_tenant_id_id"); + + b.HasIndex("TenantId", "ContentNodeId") + .HasDatabaseName("ix_handbook_chapters_tenant_id_content_node_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_handbook_chapters_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "SubjectId") + .HasDatabaseName("ix_handbook_chapters_tenant_id_subject_id"); + + b.HasIndex("TenantId", "EntryId", "ContentNodeId", "SubjectId", "IsActive", "SortOrder") + .HasDatabaseName("ix_handbook_chapters_tenant_id_entry_id_content_node_id_subjec~"); + + b.ToTable("handbook_chapters", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.HandbookEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ChapterId") + .HasColumnType("uuid") + .HasColumnName("chapter_id"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("ContentNodeId") + .HasColumnType("uuid") + .HasColumnName("content_node_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EntryId") + .HasColumnType("uuid") + .HasColumnName("entry_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SourceHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_hash"); + + b.Property("Summary") + .HasColumnType("text") + .HasColumnName("summary"); + + b.Property("Tags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("tags") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_handbook_entries"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_handbook_entries_tenant_id_id"); + + b.HasIndex("TenantId", "ChapterId") + .HasDatabaseName("ix_handbook_entries_tenant_id_chapter_id"); + + b.HasIndex("TenantId", "ContentNodeId") + .HasDatabaseName("ix_handbook_entries_tenant_id_content_node_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_handbook_entries_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "EntryId", "ContentNodeId", "ChapterId", "IsActive", "SortOrder") + .HasDatabaseName("ix_handbook_entries_tenant_id_entry_id_content_node_id_chapter~"); + + b.ToTable("handbook_entries", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.HandbookSubject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Color") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("color"); + + b.Property("ContentNodeId") + .HasColumnType("uuid") + .HasColumnName("content_node_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EntryId") + .HasColumnType("uuid") + .HasColumnName("entry_id"); + + b.Property("Icon") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("icon"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("MajorId") + .HasColumnType("uuid") + .HasColumnName("major_id"); + + b.Property("MajorLegacyIds") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("major_legacy_ids") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SchoolId") + .HasColumnType("uuid") + .HasColumnName("school_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SourceHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_hash"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Type") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_handbook_subjects"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_handbook_subjects_tenant_id_id"); + + b.HasIndex("TenantId", "ContentNodeId") + .HasDatabaseName("ix_handbook_subjects_tenant_id_content_node_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_handbook_subjects_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "MajorId") + .HasDatabaseName("ix_handbook_subjects_tenant_id_major_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_handbook_subjects_tenant_id_region_id"); + + b.HasIndex("TenantId", "SchoolId") + .HasDatabaseName("ix_handbook_subjects_tenant_id_school_id"); + + b.HasIndex("TenantId", "EntryId", "ContentNodeId", "RegionId", "IsActive", "SortOrder") + .HasDatabaseName("ix_handbook_subjects_tenant_id_entry_id_content_node_id_region~"); + + b.ToTable("handbook_subjects", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ImageAsset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("category"); + + b.Property("CdnUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("cdn_url"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FileName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("file_name"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_images"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_images_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_images_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "Category", "IsPublic") + .HasDatabaseName("ix_images_tenant_id_category_is_public"); + + b.ToTable("images", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PracticeBlueprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessRules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("access_rules") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("AssemblyType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("assembly_type"); + + b.Property("CollectionId") + .HasColumnType("uuid") + .HasColumnName("collection_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("DurationMinutes") + .HasColumnType("integer") + .HasColumnName("duration_minutes"); + + b.Property("EntryId") + .HasColumnType("uuid") + .HasColumnName("entry_id"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("mode"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id"); + + b.Property("PassScore") + .HasPrecision(8, 2) + .HasColumnType("numeric(8,2)") + .HasColumnName("pass_score"); + + b.Property("QuestionLimit") + .HasColumnType("integer") + .HasColumnName("question_limit"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("Rules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("rules") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Sections") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sections") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalScore") + .HasPrecision(8, 2) + .HasColumnType("numeric(8,2)") + .HasColumnName("total_score"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.HasKey("Id") + .HasName("pk_practice_blueprints"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_blueprints_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_practice_blueprints_created_by"); + + b.HasIndex("UpdatedBy") + .HasDatabaseName("ix_practice_blueprints_updated_by"); + + b.HasIndex("TenantId", "CollectionId") + .HasDatabaseName("ix_practice_blueprints_tenant_id_collection_id"); + + b.HasIndex("TenantId", "EntryId") + .HasDatabaseName("ix_practice_blueprints_tenant_id_entry_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_practice_blueprints_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_practice_blueprints_tenant_id_region_id"); + + b.HasIndex("TenantId", "NodeId", "CollectionId", "Mode", "Status", "SortOrder") + .HasDatabaseName("ix_practice_blueprints_tenant_id_node_id_collection_id_mode_st~"); + + b.ToTable("practice_blueprints", null, t => + { + t.HasCheckConstraint("ck_practice_blueprints_duration", "duration_minutes is null or duration_minutes > 0"); + + t.HasCheckConstraint("ck_practice_blueprints_question_limit", "question_limit is null or question_limit > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessRules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("access_rules") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CollectionType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("collection_type"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("DurationMinutes") + .HasColumnType("integer") + .HasColumnName("duration_minutes"); + + b.Property("EntryId") + .HasColumnType("uuid") + .HasColumnName("entry_id"); + + b.Property("Filters") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("filters") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id"); + + b.Property("QuestionBankId") + .HasColumnType("uuid") + .HasColumnName("question_bank_id"); + + b.Property("QuestionCount") + .HasColumnType("integer") + .HasColumnName("question_count"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("source_type"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("SubjectId") + .HasColumnType("uuid") + .HasColumnName("subject_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalScore") + .HasPrecision(8, 2) + .HasColumnType("numeric(8,2)") + .HasColumnName("total_score"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.HasKey("Id") + .HasName("pk_question_collections"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_collections_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_question_collections_created_by"); + + b.HasIndex("UpdatedBy") + .HasDatabaseName("ix_question_collections_updated_by"); + + b.HasIndex("TenantId", "CategoryId") + .HasDatabaseName("ix_question_collections_tenant_id_category_id"); + + b.HasIndex("TenantId", "EntryId") + .HasDatabaseName("ix_question_collections_tenant_id_entry_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_question_collections_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "QuestionBankId") + .HasDatabaseName("ix_question_collections_tenant_id_question_bank_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_question_collections_tenant_id_region_id"); + + b.HasIndex("TenantId", "SubjectId") + .HasDatabaseName("ix_question_collections_tenant_id_subject_id"); + + b.HasIndex("TenantId", "NodeId", "Status", "SortOrder") + .HasDatabaseName("ix_question_collections_tenant_id_node_id_status_sort_order"); + + b.ToTable("question_collections", null, t => + { + t.HasCheckConstraint("ck_question_collections_duration", "duration_minutes is null or duration_minutes > 0"); + + t.HasCheckConstraint("ck_question_collections_question_count", "question_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionCollectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CollectionId") + .HasColumnType("uuid") + .HasColumnName("collection_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("QuestionOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_owner_tenant_id"); + + b.Property("QuestionReferenceId") + .HasColumnType("uuid") + .HasColumnName("question_reference_id"); + + b.Property("Required") + .HasColumnType("boolean") + .HasColumnName("required"); + + b.Property("Score") + .HasPrecision(8, 2) + .HasColumnType("numeric(8,2)") + .HasColumnName("score"); + + b.Property("SectionKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("section_key"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_question_collection_items"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_collection_items_tenant_id_id"); + + b.HasIndex("QuestionOwnerTenantId", "QuestionId") + .HasDatabaseName("ix_question_collection_items_question_owner_tenant_id_question~"); + + b.HasIndex("TenantId", "QuestionReferenceId") + .HasDatabaseName("ix_question_collection_items_tenant_id_question_reference_id"); + + b.HasIndex("TenantId", "CollectionId", "QuestionReferenceId") + .IsUnique() + .HasDatabaseName("ix_question_collection_items_tenant_id_collection_id_question_~"); + + b.HasIndex("TenantId", "CollectionId", "SectionKey", "SortOrder") + .HasDatabaseName("ix_question_collection_items_tenant_id_collection_id_section_k~"); + + b.ToTable("question_collection_items", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionTypeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SubjectId") + .HasColumnType("uuid") + .HasColumnName("subject_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Types") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("types") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_question_type_groups"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_type_groups_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_question_type_groups_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "SubjectId", "IsActive", "SortOrder") + .HasDatabaseName("ix_question_type_groups_tenant_id_subject_id_is_active_sort_or~"); + + b.ToTable("question_type_groups", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionVideo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyQuestionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_question_id"); + + b.Property("LegacyVideoId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_video_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("VideoId") + .HasColumnType("uuid") + .HasColumnName("video_id"); + + b.Property("VideoType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("video_type"); + + b.HasKey("Id") + .HasName("pk_question_videos"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_videos_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_question_videos_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "VideoId") + .HasDatabaseName("ix_question_videos_tenant_id_video_id"); + + b.HasIndex("TenantId", "QuestionId", "VideoId", "VideoType") + .IsUnique() + .HasDatabaseName("ix_question_videos_tenant_id_question_id_video_id_video_type"); + + b.ToTable("question_videos", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.SubjectShare", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("SourceSubjectId") + .HasColumnType("uuid") + .HasColumnName("source_subject_id"); + + b.Property("TargetSubjectId") + .HasColumnType("uuid") + .HasColumnName("target_subject_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("LegacyId") + .HasColumnType("text") + .HasColumnName("legacy_id"); + + b.Property("LegacySourceSubjectId") + .HasColumnType("text") + .HasColumnName("legacy_source_subject_id"); + + b.Property("LegacyTargetSubjectId") + .HasColumnType("text") + .HasColumnName("legacy_target_subject_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("TenantId", "SourceSubjectId", "TargetSubjectId") + .HasName("pk_subject_shares"); + + b.HasIndex("TenantId", "TargetSubjectId") + .HasDatabaseName("ix_subject_shares_tenant_id_target_subject_id"); + + b.ToTable("subject_shares", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TenantQuestionBankPreference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Alias") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("alias"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("IsVisible") + .HasColumnType("boolean") + .HasColumnName("is_visible"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("NavigationLocation") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("navigation_location"); + + b.Property("QuestionBankId") + .HasColumnType("uuid") + .HasColumnName("question_bank_id"); + + b.Property("QuestionBankOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_bank_owner_tenant_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.HasKey("Id") + .HasName("pk_tenant_question_bank_preferences"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_question_bank_preferences_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_tenant_question_bank_preferences_created_by"); + + b.HasIndex("UpdatedBy") + .HasDatabaseName("ix_tenant_question_bank_preferences_updated_by"); + + b.HasIndex("QuestionBankOwnerTenantId", "QuestionBankId") + .HasDatabaseName("ix_tenant_question_bank_preferences_question_bank_owner_tenant~"); + + b.HasIndex("TenantId", "QuestionBankOwnerTenantId", "QuestionBankId") + .IsUnique() + .HasDatabaseName("ix_tenant_question_bank_preferences_tenant_id_question_bank_ow~"); + + b.ToTable("tenant_question_bank_preferences", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TenantQuestionReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("QuestionOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_owner_tenant_id"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("source"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_question_references"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_question_references_tenant_id_id"); + + b.HasAlternateKey("TenantId", "QuestionOwnerTenantId", "QuestionId") + .HasName("ak_tenant_question_references_tenant_id_question_owner_tenant_~"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_tenant_question_references_created_by"); + + b.HasIndex("QuestionOwnerTenantId", "QuestionId") + .HasDatabaseName("ix_tenant_question_references_question_owner_tenant_id_questio~"); + + b.ToTable("tenant_question_references", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.UserWordFavorite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FavoritedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("favorited_at"); + + b.Property("LegacyId") + .HasColumnType("text") + .HasColumnName("legacy_id"); + + b.Property("LegacyUserId") + .HasColumnType("text") + .HasColumnName("legacy_user_id"); + + b.Property("LegacyWordId") + .HasColumnType("text") + .HasColumnName("legacy_word_id"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("note"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("WordId") + .HasColumnType("uuid") + .HasColumnName("word_id"); + + b.HasKey("Id") + .HasName("pk_user_word_favorites"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_user_word_favorites_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_word_favorites_user_id"); + + b.HasIndex("TenantId", "WordId") + .HasDatabaseName("ix_user_word_favorites_tenant_id_word_id"); + + b.HasIndex("TenantId", "UserId", "WordId") + .IsUnique() + .HasDatabaseName("ix_user_word_favorites_tenant_id_user_id_word_id"); + + b.ToTable("user_word_favorites", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.UserWordProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CorrectCount") + .HasColumnType("integer") + .HasColumnName("correct_count"); + + b.Property("CorrectStreak") + .HasColumnType("integer") + .HasColumnName("correct_streak"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DueLevel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("due_level"); + + b.Property("EaseFactor") + .ValueGeneratedOnAdd() + .HasPrecision(4, 2) + .HasColumnType("numeric(4,2)") + .HasDefaultValue(2.50m) + .HasColumnName("ease_factor"); + + b.Property("LastResult") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("last_result"); + + b.Property("LastReviewAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_review_at"); + + b.Property("LegacyId") + .HasColumnType("text") + .HasColumnName("legacy_id"); + + b.Property("LegacyUserId") + .HasColumnType("text") + .HasColumnName("legacy_user_id"); + + b.Property("LegacyWordId") + .HasColumnType("text") + .HasColumnName("legacy_word_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("NextReviewAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("next_review_at"); + + b.Property("ReviewCount") + .HasColumnType("integer") + .HasColumnName("review_count"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("WordId") + .HasColumnType("uuid") + .HasColumnName("word_id"); + + b.Property("WrongCount") + .HasColumnType("integer") + .HasColumnName("wrong_count"); + + b.HasKey("Id") + .HasName("pk_user_word_progress"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_user_word_progress_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_word_progress_user_id"); + + b.HasIndex("TenantId", "WordId") + .HasDatabaseName("ix_user_word_progress_tenant_id_word_id"); + + b.HasIndex("TenantId", "UserId", "WordId") + .IsUnique() + .HasDatabaseName("ix_user_word_progress_tenant_id_user_id_word_id"); + + b.HasIndex("TenantId", "UserId", "NextReviewAt", "Status", "DueLevel") + .HasDatabaseName("ix_user_word_progress_tenant_id_user_id_next_review_at_status_~"); + + b.ToTable("user_word_progress", null, t => + { + t.HasCheckConstraint("ck_user_word_progress_correct_streak", "correct_streak >= 0"); + + t.HasCheckConstraint("ck_user_word_progress_ease_factor", "ease_factor >= 1.30 and ease_factor <= 3.00"); + + t.HasCheckConstraint("ck_user_word_progress_review_count", "review_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.VideoExplanation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("DurationSeconds") + .HasColumnType("integer") + .HasColumnName("duration_seconds"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsGeneral") + .HasColumnType("boolean") + .HasColumnName("is_general"); + + b.Property("KnowledgeTags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("knowledge_tags") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacySubjectId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_subject_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SourceHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_hash"); + + b.Property("SubjectId") + .HasColumnType("uuid") + .HasColumnName("subject_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("ThumbnailUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("thumbnail_url"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("VideoUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("video_url"); + + b.HasKey("Id") + .HasName("pk_video_explanations"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_video_explanations_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_video_explanations_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "SourceHash") + .HasDatabaseName("ix_video_explanations_tenant_id_source_hash") + .HasFilter("source_hash is not null"); + + b.HasIndex("TenantId", "SubjectId", "IsActive", "SortOrder") + .HasDatabaseName("ix_video_explanations_tenant_id_subject_id_is_active_sort_order"); + + b.ToTable("video_explanations", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.VideoPlaybackProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DurationSeconds") + .HasColumnType("integer") + .HasColumnName("duration_seconds"); + + b.Property("IsCompleted") + .HasColumnType("boolean") + .HasColumnName("is_completed"); + + b.Property("LastPlayedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_played_at") + .HasDefaultValueSql("now()"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PlayCount") + .HasColumnType("integer") + .HasColumnName("play_count"); + + b.Property("PositionSeconds") + .HasColumnType("integer") + .HasColumnName("position_seconds"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("VideoId") + .HasColumnType("uuid") + .HasColumnName("video_id"); + + b.Property("WatchedSeconds") + .HasColumnType("integer") + .HasColumnName("watched_seconds"); + + b.HasKey("Id") + .HasName("pk_video_playback_progress"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_video_playback_progress_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_video_playback_progress_user_id"); + + b.HasIndex("TenantId", "QuestionId") + .HasDatabaseName("ix_video_playback_progress_tenant_id_question_id"); + + b.HasIndex("TenantId", "VideoId") + .HasDatabaseName("ix_video_playback_progress_tenant_id_video_id"); + + b.HasIndex("TenantId", "UserId", "LastPlayedAt") + .HasDatabaseName("ix_video_playback_progress_tenant_id_user_id_last_played_at"); + + b.HasIndex("TenantId", "UserId", "VideoId") + .IsUnique() + .HasDatabaseName("ix_video_playback_progress_tenant_id_user_id_video_id") + .HasFilter("question_id is null"); + + b.HasIndex("TenantId", "UserId", "VideoId", "QuestionId") + .IsUnique() + .HasDatabaseName("ix_video_playback_progress_tenant_id_user_id_video_id_question~") + .HasFilter("question_id is not null"); + + b.ToTable("video_playback_progress", null, t => + { + t.HasCheckConstraint("ck_video_playback_progress_duration", "duration_seconds is null or duration_seconds >= 0"); + + t.HasCheckConstraint("ck_video_playback_progress_play_count", "play_count >= 0"); + + t.HasCheckConstraint("ck_video_playback_progress_position", "position_seconds >= 0"); + + t.HasCheckConstraint("ck_video_playback_progress_watched", "watched_seconds >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.VocabularyUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentNodeId") + .HasColumnType("uuid") + .HasColumnName("content_node_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EntryId") + .HasColumnType("uuid") + .HasColumnName("entry_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SourceHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_hash"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("WordCount") + .HasColumnType("integer") + .HasColumnName("word_count"); + + b.HasKey("Id") + .HasName("pk_vocabulary_units"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_vocabulary_units_tenant_id_id"); + + b.HasIndex("TenantId", "ContentNodeId") + .HasDatabaseName("ix_vocabulary_units_tenant_id_content_node_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_vocabulary_units_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_vocabulary_units_tenant_id_region_id"); + + b.HasIndex("TenantId", "EntryId", "ContentNodeId", "RegionId", "IsActive", "SortOrder") + .HasDatabaseName("ix_vocabulary_units_tenant_id_entry_id_content_node_id_region_~"); + + b.ToTable("vocabulary_units", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.VocabularyWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentNodeId") + .HasColumnType("uuid") + .HasColumnName("content_node_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("EntryId") + .HasColumnType("uuid") + .HasColumnName("entry_id"); + + b.Property("Example") + .HasColumnType("text") + .HasColumnName("example"); + + b.Property("ExampleTranslation") + .HasColumnType("text") + .HasColumnName("example_translation"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Meaning") + .HasColumnType("text") + .HasColumnName("meaning"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Phonetic") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("phonetic"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SourceHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_hash"); + + b.Property("Tags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("tags") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UnitId") + .HasColumnType("uuid") + .HasColumnName("unit_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("Word") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("word"); + + b.HasKey("Id") + .HasName("pk_vocabulary_words"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_vocabulary_words_tenant_id_id"); + + b.HasIndex("TenantId", "ContentNodeId") + .HasDatabaseName("ix_vocabulary_words_tenant_id_content_node_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_vocabulary_words_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "UnitId", "IsActive", "SortOrder", "CreatedAt") + .HasDatabaseName("ix_vocabulary_words_tenant_id_unit_id_is_active_sort_order_cre~"); + + b.HasIndex("TenantId", "EntryId", "ContentNodeId", "UnitId", "IsActive", "SortOrder") + .HasDatabaseName("ix_vocabulary_words_tenant_id_entry_id_content_node_id_unit_id~"); + + b.ToTable("vocabulary_words", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CommissionSettlement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CommissionAmountCents") + .HasColumnType("integer") + .HasColumnName("commission_amount_cents"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DefaultRate") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)") + .HasColumnName("default_rate"); + + b.Property("EffectiveRate") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)") + .HasColumnName("effective_rate"); + + b.Property("GeneratedBy") + .HasColumnType("uuid") + .HasColumnName("generated_by"); + + b.Property("GrossAmountCents") + .HasColumnType("integer") + .HasColumnName("gross_amount_cents"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("paid_at"); + + b.Property("PaidBy") + .HasColumnType("uuid") + .HasColumnName("paid_by"); + + b.Property("PaidUserCount") + .HasColumnType("integer") + .HasColumnName("paid_user_count"); + + b.Property("PaymentAccount") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("payment_account"); + + b.Property("PaymentMethod") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("payment_method"); + + b.Property("PeriodEnd") + .HasColumnType("date") + .HasColumnName("period_end"); + + b.Property("PeriodStart") + .HasColumnType("date") + .HasColumnName("period_start"); + + b.Property("ReferrerUserId") + .HasColumnType("uuid") + .HasColumnName("referrer_user_id"); + + b.Property("Remark") + .HasColumnType("text") + .HasColumnName("remark"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("reviewed_at"); + + b.Property("ReviewedBy") + .HasColumnType("uuid") + .HasColumnName("reviewed_by"); + + b.Property("SettlementNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("settlement_no"); + + b.Property("SourceCount") + .HasColumnType("integer") + .HasColumnName("source_count"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_commission_settlements"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commission_settlements_tenant_id_id"); + + b.HasIndex("GeneratedBy") + .HasDatabaseName("ix_commission_settlements_generated_by"); + + b.HasIndex("PaidBy") + .HasDatabaseName("ix_commission_settlements_paid_by"); + + b.HasIndex("ReferrerUserId") + .HasDatabaseName("ix_commission_settlements_referrer_user_id"); + + b.HasIndex("ReviewedBy") + .HasDatabaseName("ix_commission_settlements_reviewed_by"); + + b.HasIndex("TenantId", "SettlementNo") + .IsUnique() + .HasDatabaseName("ix_commission_settlements_tenant_id_settlement_no"); + + b.HasIndex("TenantId", "ReferrerUserId", "PeriodStart") + .HasDatabaseName("ix_commission_settlements_referrer_start"); + + b.HasIndex("TenantId", "ReferrerUserId", "PeriodStart", "PeriodEnd") + .IsUnique() + .HasDatabaseName("ux_commission_settlements_referrer_period"); + + b.ToTable("commission_settlements", null, t => + { + t.HasCheckConstraint("ck_commission_settlements_amounts", "gross_amount_cents >= 0 and commission_amount_cents >= 0"); + + t.HasCheckConstraint("ck_commission_settlements_counts", "source_count >= 0 and paid_user_count >= 0"); + + t.HasCheckConstraint("ck_commission_settlements_period", "period_end >= period_start"); + + t.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))"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CommissionSettlementExportEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentSha256") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("content_sha256"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ExportFormat") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("export_format"); + + b.Property("ExportedBy") + .HasColumnType("uuid") + .HasColumnName("exported_by"); + + b.Property("Filename") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("filename"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RowCount") + .HasColumnType("integer") + .HasColumnName("row_count"); + + b.Property("SettlementId") + .HasColumnType("uuid") + .HasColumnName("settlement_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_commission_settlement_export_events"); + + b.HasIndex("ExportedBy") + .HasDatabaseName("ix_commission_settlement_export_events_exported_by"); + + b.HasIndex("TenantId", "SettlementId", "CreatedAt") + .HasDatabaseName("ix_commission_settlement_export_events_tenant_id_settlement_id~"); + + b.ToTable("commission_settlement_export_events", null, t => + { + t.HasCheckConstraint("ck_commission_export_events_rows", "row_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CommissionSettlementItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AttributionType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("attribution_type"); + + b.Property("CommissionAmountCents") + .HasColumnType("integer") + .HasColumnName("commission_amount_cents"); + + b.Property("CommissionRate") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)") + .HasColumnName("commission_rate"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("GrossAmountCents") + .HasColumnType("integer") + .HasColumnName("gross_amount_cents"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RateSource") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("rate_source"); + + b.Property("ReferrerUserId") + .HasColumnType("uuid") + .HasColumnName("referrer_user_id"); + + b.Property("SettlementId") + .HasColumnType("uuid") + .HasColumnName("settlement_id"); + + b.Property("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceNo") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_no"); + + b.Property("SourcePaidAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("source_paid_at"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("source_type"); + + b.Property("StudentUserId") + .HasColumnType("uuid") + .HasColumnName("student_user_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_commission_settlement_items"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commission_settlement_items_tenant_id_id"); + + b.HasIndex("ReferrerUserId") + .HasDatabaseName("ix_commission_settlement_items_referrer_user_id"); + + b.HasIndex("StudentUserId") + .HasDatabaseName("ix_commission_settlement_items_student_user_id"); + + b.HasIndex("TenantId", "SettlementId") + .HasDatabaseName("ix_commission_settlement_items_tenant_id_settlement_id"); + + b.HasIndex("TenantId", "ReferrerUserId", "SourcePaidAt") + .HasDatabaseName("ix_commission_settlement_items_tenant_id_referrer_user_id_sour~"); + + b.HasIndex("TenantId", "SourceType", "SourceId") + .IsUnique() + .HasDatabaseName("ix_commission_settlement_items_tenant_id_source_type_source_id"); + + b.ToTable("commission_settlement_items", null, t => + { + t.HasCheckConstraint("ck_commission_settlement_items_amounts", "gross_amount_cents >= 0 and commission_amount_cents >= 0"); + + t.HasCheckConstraint("ck_commission_settlement_items_rate", "commission_rate >= 0 and commission_rate <= 1"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CommissionSettlementProof", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("AssetId") + .HasColumnType("uuid") + .HasColumnName("asset_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ExternalUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("external_url"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("paid_at"); + + b.Property("PaymentAccount") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("payment_account"); + + b.Property("PaymentMethod") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("payment_method"); + + b.Property("ProofType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("proof_type"); + + b.Property("ReviewNote") + .HasColumnType("text") + .HasColumnName("review_note"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("reviewed_at"); + + b.Property("ReviewedBy") + .HasColumnType("uuid") + .HasColumnName("reviewed_by"); + + b.Property("SettlementId") + .HasColumnType("uuid") + .HasColumnName("settlement_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("SubmittedBy") + .HasColumnType("uuid") + .HasColumnName("submitted_by"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_commission_settlement_proofs"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commission_settlement_proofs_tenant_id_id"); + + b.HasIndex("ReviewedBy") + .HasDatabaseName("ix_commission_settlement_proofs_reviewed_by"); + + b.HasIndex("SubmittedBy") + .HasDatabaseName("ix_commission_settlement_proofs_submitted_by"); + + b.HasIndex("TenantId", "AssetId") + .HasDatabaseName("ix_commission_settlement_proofs_tenant_id_asset_id"); + + b.HasIndex("TenantId", "SettlementId", "Status", "CreatedAt") + .HasDatabaseName("ix_commission_settlement_proofs_tenant_id_settlement_id_status~"); + + b.ToTable("commission_settlement_proofs", null, t => + { + t.HasCheckConstraint("ck_commission_settlement_proofs_amount", "amount_cents is null or amount_cents >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CrmConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssignmentConfig") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("assignment_config") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("AssignmentCursor") + .HasColumnType("integer") + .HasColumnName("assignment_cursor"); + + b.Property("AssignmentMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("assignment_mode"); + + b.Property("AssignmentPool") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("assignment_pool") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DelaySeconds") + .HasColumnType("integer") + .HasColumnName("delay_seconds"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("ExamType") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("exam_type"); + + b.Property("FormName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("form_name"); + + b.Property("SecretRef") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("secret_ref"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TimeoutSeconds") + .HasColumnType("integer") + .HasColumnName("timeout_seconds"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("Url") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("pk_crm_config"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_crm_config_tenant_id_id"); + + b.HasIndex("TenantId") + .IsUnique() + .HasDatabaseName("ix_crm_config_tenant_id"); + + b.HasIndex("TenantId", "AssignmentMode") + .HasDatabaseName("ix_crm_config_tenant_id_assignment_mode"); + + b.ToTable("crm_config", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CrmWebhookLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Attempt") + .HasColumnType("integer") + .HasColumnName("attempt"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.Property("HttpCode") + .HasColumnType("integer") + .HasColumnName("http_code"); + + b.Property("LeadId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("lead_id"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Outcome") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("outcome"); + + b.Property("RecordId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("record_id"); + + b.Property("RequestBody") + .HasColumnType("text") + .HasColumnName("request_body"); + + b.Property("RequestPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("request_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ResponseSummary") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("response_summary"); + + b.Property("SignedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("signed_at"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_crm_webhook_log"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_crm_webhook_log_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_crm_webhook_log_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RecordId", "CreatedAt") + .HasDatabaseName("ix_crm_webhook_log_tenant_id_record_id_created_at"); + + b.ToTable("crm_webhook_log", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CrmWebhookQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IdempotencyKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("LastAttemptAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_at"); + + b.Property("LastError") + .HasColumnType("text") + .HasColumnName("last_error"); + + b.Property("LastHttpCode") + .HasColumnType("integer") + .HasColumnName("last_http_code"); + + b.Property("LastResponseSummary") + .HasColumnType("text") + .HasColumnName("last_response_summary"); + + b.Property("LeadId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("lead_id"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("NextAttemptAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("next_attempt_at"); + + b.Property("Payload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Provider") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("provider"); + + b.Property("RecordId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("record_id"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("scheduled_at"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("sent_at"); + + b.Property("Source") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("source"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TargetUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("target_url"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_crm_webhook_queue"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_crm_webhook_queue_tenant_id_id"); + + b.HasIndex("TenantId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_crm_webhook_queue_tenant_id_idempotency_key") + .HasFilter("idempotency_key is not null"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_crm_webhook_queue_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RecordId") + .IsUnique() + .HasDatabaseName("ix_crm_webhook_queue_tenant_id_record_id") + .HasFilter("record_id is not null"); + + b.HasIndex("TenantId", "RecordId", "CreatedAt") + .HasDatabaseName("ix_crm_webhook_queue_tenant_id_record_id_created_at") + .HasFilter("record_id is not null"); + + b.HasIndex("TenantId", "Source", "CreatedAt") + .HasDatabaseName("ix_crm_webhook_queue_tenant_id_source_created_at") + .HasFilter("source = 'tenant.student.crm_push'"); + + b.HasIndex("TenantId", "Status", "ScheduledAt", "NextAttemptAt") + .HasDatabaseName("ix_crm_webhook_queue_tenant_id_status_scheduled_at_next_attemp~"); + + b.ToTable("crm_webhook_queue", null, t => + { + t.HasCheckConstraint("ck_crm_webhook_queue_attempts", "attempts >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.ReferralCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Channel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("channel"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LandingPath") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("landing_path"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_referral_codes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_referral_codes_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_referral_codes_user_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_referral_codes_tenant_id_code"); + + b.HasIndex("TenantId", "UserId") + .IsUnique() + .HasDatabaseName("ix_referral_codes_tenant_id_user_id"); + + b.HasIndex("TenantId", "Code", "Status") + .HasDatabaseName("ix_referral_codes_tenant_id_code_status"); + + b.ToTable("referral_codes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.ReferralLead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("assigned_at"); + + b.Property("AssignedBy") + .HasColumnType("uuid") + .HasColumnName("assigned_by"); + + b.Property("AssignedToUserId") + .HasColumnType("uuid") + .HasColumnName("assigned_to_user_id"); + + b.Property("AssignmentMode") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("assignment_mode"); + + b.Property("BindType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("bind_type"); + + b.Property("BoundAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("bound_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FirstTrackId") + .HasColumnType("uuid") + .HasColumnName("first_track_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ProtectedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("protected_until"); + + b.Property("RefCode") + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("ref_code"); + + b.Property("ReferrerUserId") + .HasColumnType("uuid") + .HasColumnName("referrer_user_id"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("StudentUserId") + .HasColumnType("uuid") + .HasColumnName("student_user_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_referral_leads"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_referral_leads_tenant_id_id"); + + b.HasIndex("AssignedBy") + .HasDatabaseName("ix_referral_leads_assigned_by"); + + b.HasIndex("AssignedToUserId") + .HasDatabaseName("ix_referral_leads_assigned_to_user_id"); + + b.HasIndex("ReferrerUserId") + .HasDatabaseName("ix_referral_leads_referrer_user_id"); + + b.HasIndex("StudentUserId") + .HasDatabaseName("ix_referral_leads_student_user_id"); + + b.HasIndex("TenantId", "FirstTrackId") + .HasDatabaseName("ix_referral_leads_tenant_id_first_track_id"); + + b.HasIndex("TenantId", "StudentUserId") + .IsUnique() + .HasDatabaseName("ix_referral_leads_tenant_id_student_user_id"); + + b.HasIndex("TenantId", "ReferrerUserId", "BoundAt") + .HasDatabaseName("ix_referral_leads_tenant_id_referrer_user_id_bound_at"); + + b.HasIndex("TenantId", "AssignedToUserId", "Status", "BoundAt") + .HasDatabaseName("ix_referral_leads_tenant_id_assigned_to_user_id_status_bound_at"); + + b.ToTable("referral_leads", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.ReferralQrcode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Page") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("page"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("provider"); + + b.Property("QrcodeUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("qrcode_url"); + + b.Property("RefCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("ref_code"); + + b.Property("Scene") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("scene"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_referral_qrcodes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_referral_qrcodes_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_referral_qrcodes_user_id"); + + b.HasIndex("TenantId", "RefCode", "Status") + .HasDatabaseName("ix_referral_qrcodes_tenant_id_ref_code_status"); + + b.HasIndex("TenantId", "Provider", "Scene", "Page") + .IsUnique() + .HasDatabaseName("ix_referral_qrcodes_tenant_id_provider_scene_page"); + + b.ToTable("referral_qrcodes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.ReferralTeamEdge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LeaderUserId") + .HasColumnType("uuid") + .HasColumnName("leader_user_id"); + + b.Property("MemberUserId") + .HasColumnType("uuid") + .HasColumnName("member_user_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RelationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("relation_type"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_referral_team_edges"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_referral_team_edges_tenant_id_id"); + + b.HasIndex("LeaderUserId") + .HasDatabaseName("ix_referral_team_edges_leader_user_id"); + + b.HasIndex("MemberUserId") + .HasDatabaseName("ix_referral_team_edges_member_user_id"); + + b.HasIndex("TenantId", "LeaderUserId", "Status") + .HasDatabaseName("ix_referral_team_edges_tenant_id_leader_user_id_status"); + + b.HasIndex("TenantId", "MemberUserId", "RelationType") + .IsUnique() + .HasDatabaseName("ix_referral_team_edges_tenant_id_member_user_id_relation_type"); + + b.ToTable("referral_team_edges", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.ReferralTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("event_type"); + + b.Property("IpAddress") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("ip_address"); + + b.Property("LeadId") + .HasColumnType("uuid") + .HasColumnName("lead_id"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RefCode") + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("ref_code"); + + b.Property("ReferrerUserId") + .HasColumnType("uuid") + .HasColumnName("referrer_user_id"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source"); + + b.Property("TargetUserId") + .HasColumnType("uuid") + .HasColumnName("target_user_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserAgent") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("user_agent"); + + b.HasKey("Id") + .HasName("pk_referral_tracks"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_referral_tracks_tenant_id_id"); + + b.HasIndex("ReferrerUserId") + .HasDatabaseName("ix_referral_tracks_referrer_user_id"); + + b.HasIndex("TargetUserId") + .HasDatabaseName("ix_referral_tracks_target_user_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_referral_tracks_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "LeadId", "CreatedAt") + .HasDatabaseName("ix_referral_tracks_tenant_id_lead_id_created_at"); + + b.HasIndex("TenantId", "RefCode", "EventType", "CreatedAt") + .HasDatabaseName("ix_referral_tracks_tenant_id_ref_code_event_type_created_at"); + + b.ToTable("referral_tracks", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.TenantCommissionSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Config") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("config") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DefaultRate") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)") + .HasColumnName("default_rate"); + + b.Property("MinSettlementCents") + .HasColumnType("integer") + .HasColumnName("min_settlement_cents"); + + b.Property("SettlementCycle") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("settlement_cycle"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.HasKey("Id") + .HasName("pk_tenant_commission_settings"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_commission_settings_tenant_id_id"); + + b.HasIndex("TenantId") + .IsUnique() + .HasDatabaseName("ix_tenant_commission_settings_tenant_id"); + + b.HasIndex("UpdatedBy") + .HasDatabaseName("ix_tenant_commission_settings_updated_by"); + + b.ToTable("tenant_commission_settings", null, t => + { + t.HasCheckConstraint("ck_tenant_commission_settings_default_rate", "default_rate >= 0 and default_rate <= 1"); + + t.HasCheckConstraint("ck_tenant_commission_settings_min_settlement", "min_settlement_cents >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.StudentProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AvatarPreset") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("male") + .HasColumnName("avatar_preset"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LastCheckInDate") + .HasColumnType("date") + .HasColumnName("last_check_in_date"); + + b.Property("LegacyUserId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_user_id"); + + b.Property("MasteredWordsCount") + .HasColumnType("integer") + .HasColumnName("mastered_words_count"); + + b.Property("ModuleSelections") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("module_selections") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Progress") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("progress") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("QuestionsAnsweredToday") + .HasColumnType("integer") + .HasColumnName("questions_answered_today"); + + b.Property("RecentActivities") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("recent_activities") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SelectedMajorId") + .HasColumnType("uuid") + .HasColumnName("selected_major_id"); + + b.Property("SelectedSchoolId") + .HasColumnType("uuid") + .HasColumnName("selected_school_id"); + + b.Property("Stats") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("stats") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_student_profiles"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_profiles_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_profiles_user_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_student_profiles_tenant_id_region_id"); + + b.HasIndex("TenantId", "SelectedMajorId") + .HasDatabaseName("ix_student_profiles_tenant_id_selected_major_id"); + + b.HasIndex("TenantId", "SelectedSchoolId") + .HasDatabaseName("ix_student_profiles_tenant_id_selected_school_id"); + + b.HasIndex("TenantId", "UserId") + .IsUnique() + .HasDatabaseName("ix_student_profiles_tenant_id_user_id"); + + b.ToTable("student_profiles", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessFailedCount") + .HasColumnType("integer") + .HasColumnName("access_failed_count"); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("avatar_url"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("concurrency_stamp"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(320) + .HasColumnType("citext") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean") + .HasColumnName("email_confirmed"); + + b.Property("ForcePasswordChange") + .HasColumnType("boolean") + .HasColumnName("force_password_change"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_at"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("NormalizedEmail") + .HasMaxLength(320) + .HasColumnType("character varying(320)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("normalized_user_name"); + + b.Property("PasswordHash") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("password_hash"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("phone"); + + b.Property("PhoneNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean") + .HasColumnName("phone_number_confirmed"); + + b.Property("PrimaryRole") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("primary_role"); + + b.Property("RawProfile") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("raw_profile") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Score") + .HasColumnType("integer") + .HasColumnName("score"); + + b.Property("SecurityStamp") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("security_stamp"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean") + .HasColumnName("two_factor_enabled"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("LegacyId") + .IsUnique() + .HasDatabaseName("ix_users_legacy_id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("email_index"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("user_name_index"); + + b.HasIndex("Phone") + .IsUnique() + .HasDatabaseName("ix_users_phone"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.UserIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(320) + .HasColumnType("citext") + .HasColumnName("email"); + + b.Property("OpenId") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("open_id"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("phone"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("ProviderSubject") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("provider_subject"); + + b.Property("UnionId") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("union_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_identities"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_identities_user_id"); + + b.HasIndex("Provider", "ProviderSubject") + .IsUnique() + .HasDatabaseName("ix_user_identities_provider_provider_subject"); + + b.ToTable("user_identities", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Import.PocketBaseImportIssue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CollectionName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("collection_name"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FieldPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("field_path"); + + b.Property("IssueCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("issue_code"); + + b.Property("LegacyId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("legacy_id"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("RawValueSample") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("raw_value_sample"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("severity"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_pb_import_issues"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_pb_import_issues_tenant_id_id"); + + b.HasIndex("TenantId", "RunId") + .HasDatabaseName("ix_pb_import_issues_tenant_id_run_id"); + + b.HasIndex("RunId", "Severity", "CollectionName") + .HasDatabaseName("ix_pb_import_issues_run_id_severity_collection_name"); + + b.HasIndex("TenantId", "CollectionName", "LegacyId") + .HasDatabaseName("ix_pb_import_issues_tenant_id_collection_name_legacy_id"); + + b.ToTable("pb_import_issues", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Import.PocketBaseImportRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_at"); + + b.Property("SourceKind") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("source_kind"); + + b.Property("SourceName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("source_name"); + + b.Property("StartedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at") + .HasDefaultValueSql("now()"); + + b.Property("Stats") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("stats") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_pb_import_runs"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_pb_import_runs_tenant_id_id"); + + b.HasIndex("TenantId", "Status", "StartedAt") + .HasDatabaseName("ix_pb_import_runs_tenant_id_status_started_at"); + + b.ToTable("pb_import_runs", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Import.PocketBaseRawRecord", b => + { + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("CollectionName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("collection_name"); + + b.Property("LegacyId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("legacy_id"); + + b.Property("Errors") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("errors") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("ImportedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("imported_at") + .HasDefaultValueSql("now()"); + + b.Property("Normalized") + .HasColumnType("boolean") + .HasColumnName("normalized"); + + b.Property("Record") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("record") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("RunId", "CollectionName", "LegacyId") + .HasName("pk_pb_raw_records"); + + b.HasIndex("TenantId", "RunId") + .HasDatabaseName("ix_pb_raw_records_tenant_id_run_id"); + + b.HasIndex("TenantId", "CollectionName", "LegacyId") + .HasDatabaseName("ix_pb_raw_records_tenant_id_collection_name_legacy_id"); + + b.ToTable("pb_raw_records", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.AnswerRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerText") + .HasColumnType("text") + .HasColumnName("answer_text"); + + b.Property("AnsweredAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("answered_at") + .HasDefaultValueSql("now()"); + + b.Property("AwardedScore") + .HasPrecision(8, 2) + .HasColumnType("numeric(8,2)") + .HasColumnName("awarded_score"); + + b.Property("ClientSequence") + .HasColumnType("bigint") + .HasColumnName("client_sequence"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("GradingStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("pending_review") + .HasColumnName("grading_status"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("IsCorrect") + .HasColumnType("boolean") + .HasColumnName("is_correct"); + + b.Property("IsCurrent") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_current"); + + b.Property("LegacyCategoryId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_category_id"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyQuestionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_question_id"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("RequestHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("request_hash"); + + b.Property("Revision") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("revision"); + + b.Property("SelectedOptions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("selected_options") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("SessionQuestionId") + .HasColumnType("uuid") + .HasColumnName("session_question_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_answer_records"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_answer_records_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_answer_records_user_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_answer_records_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "PracticeSessionId", "SessionQuestionId") + .HasDatabaseName("ix_answer_records_tenant_id_practice_session_id_session_questi~"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId") + .HasDatabaseName("ix_answer_records_tenant_id_user_id_practice_session_id"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "ClientSequence") + .IsUnique() + .HasDatabaseName("ix_answer_records_tenant_id_user_id_practice_session_id_client~"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "SessionQuestionId") + .IsUnique() + .HasDatabaseName("ux_answer_records_current_session_question") + .HasFilter("is_current"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "SessionQuestionId", "Revision") + .IsUnique() + .HasDatabaseName("ux_answer_records_session_question_revision"); + + b.ToTable("answer_records", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.DashboardDailyStat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActiveUsers") + .HasColumnType("integer") + .HasColumnName("active_users"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyRegionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_region_id"); + + b.Property("NewOrders") + .HasColumnType("integer") + .HasColumnName("new_orders"); + + b.Property("NewQuestions") + .HasColumnType("integer") + .HasColumnName("new_questions"); + + b.Property("NewRevenueCents") + .HasColumnType("integer") + .HasColumnName("new_revenue_cents"); + + b.Property("NewUsers") + .HasColumnType("integer") + .HasColumnName("new_users"); + + b.Property("RebuiltAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("rebuilt_at"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("StatDate") + .HasColumnType("date") + .HasColumnName("stat_date"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_dashboard_daily_stats"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_dashboard_daily_stats_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_dashboard_daily_stats_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_dashboard_daily_stats_tenant_id_region_id"); + + b.HasIndex("TenantId", "StatDate", "RegionId") + .IsUnique() + .HasDatabaseName("ix_dashboard_daily_stats_tenant_id_stat_date_region_id"); + + NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("TenantId", "StatDate", "RegionId"), false); + + b.ToTable("dashboard_daily_stats", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ExamDate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ExamAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("exam_at"); + + b.Property("ExamName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("exam_name"); + + b.Property("ExamType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("exam_type"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SchoolId") + .HasColumnType("uuid") + .HasColumnName("school_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_exam_dates"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_exam_dates_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_exam_dates_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_exam_dates_tenant_id_region_id"); + + b.HasIndex("TenantId", "SchoolId", "ExamAt") + .HasDatabaseName("ix_exam_dates_tenant_id_school_id_exam_at"); + + b.ToTable("exam_dates", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.FavoriteQuestion", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("QuestionReferenceId") + .HasColumnType("uuid") + .HasColumnName("question_reference_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("QuestionOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_owner_tenant_id"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source"); + + b.HasKey("TenantId", "UserId", "QuestionReferenceId") + .HasName("pk_favorite_questions"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_favorite_questions_user_id"); + + b.HasIndex("QuestionOwnerTenantId", "QuestionId") + .HasDatabaseName("ix_favorite_questions_question_owner_tenant_id_question_id"); + + b.HasIndex("TenantId", "QuestionReferenceId") + .HasDatabaseName("ix_favorite_questions_tenant_id_question_reference_id"); + + b.ToTable("favorite_questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningOperationIdempotency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("OperationType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("operation_type"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("RequestHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("request_hash"); + + b.Property("ResponseSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("response_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_learning_operation_idempotencies"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_operation_idempotencies_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_learning_operation_idempotencies_user_id"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "OperationType", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_learning_operation_idempotencies_tenant_id_user_id_practice~"); + + b.ToTable("learning_operation_idempotencies", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeAccessEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("access_mode"); + + b.Property("ConsumedFreeQuota") + .HasColumnType("integer") + .HasColumnName("consumed_free_quota"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EntitlementId") + .HasColumnType("uuid") + .HasColumnName("entitlement_id"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("event_type"); + + b.Property("GrantedCount") + .HasColumnType("integer") + .HasColumnName("granted_count"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("RequestedCount") + .HasColumnType("integer") + .HasColumnName("requested_count"); + + b.Property("ScopeId") + .HasColumnType("uuid") + .HasColumnName("scope_id"); + + b.Property("ScopeType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("scope_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_practice_access_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_access_events_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_practice_access_events_user_id"); + + b.HasIndex("TenantId", "PracticeSessionId") + .HasDatabaseName("ix_practice_access_events_tenant_id_practice_session_id"); + + b.HasIndex("TenantId", "UserId", "CreatedAt") + .HasDatabaseName("ix_practice_access_events_tenant_id_user_id_created_at"); + + b.ToTable("practice_access_events", null, t => + { + t.HasCheckConstraint("ck_practice_access_events_consumed_free_quota", "consumed_free_quota >= 0"); + + t.HasCheckConstraint("ck_practice_access_events_granted_count", "granted_count >= 0"); + + t.HasCheckConstraint("ck_practice_access_events_requested_count", "requested_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeDailyUsage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FreeLimit") + .HasColumnType("integer") + .HasColumnName("free_limit"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ScopeId") + .HasColumnType("uuid") + .HasColumnName("scope_id"); + + b.Property("ScopeType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("scope_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UsageDate") + .ValueGeneratedOnAdd() + .HasColumnType("date") + .HasColumnName("usage_date") + .HasDefaultValueSql("current_date"); + + b.Property("UsedCount") + .HasColumnType("integer") + .HasColumnName("used_count"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_practice_daily_usage"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_daily_usage_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_practice_daily_usage_user_id"); + + b.HasIndex("TenantId", "UserId", "UsageDate") + .HasDatabaseName("ix_practice_daily_usage_tenant_id_user_id_usage_date"); + + b.HasIndex("TenantId", "UserId", "UsageDate", "ScopeType", "ScopeId") + .IsUnique() + .HasDatabaseName("ix_practice_daily_usage_tenant_id_user_id_usage_date_scope_typ~"); + + NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("TenantId", "UserId", "UsageDate", "ScopeType", "ScopeId"), false); + + b.ToTable("practice_daily_usage", null, t => + { + t.HasCheckConstraint("ck_practice_daily_usage_free_limit", "free_limit >= 0"); + + t.HasCheckConstraint("ck_practice_daily_usage_used_count", "used_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessEntitlementId") + .HasColumnType("uuid") + .HasColumnName("access_entitlement_id"); + + b.Property("AccessMode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("free") + .HasColumnName("access_mode"); + + b.Property("AccessSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("access_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("BlueprintId") + .HasColumnType("uuid") + .HasColumnName("blueprint_id"); + + b.Property("CollectionId") + .HasColumnType("uuid") + .HasColumnName("collection_id"); + + b.Property("ConsumedFreeQuota") + .HasColumnType("integer") + .HasColumnName("consumed_free_quota"); + + b.Property("ContentNodeId") + .HasColumnType("uuid") + .HasColumnName("content_node_id"); + + b.Property("DurationMinutes") + .HasColumnType("integer") + .HasColumnName("duration_minutes"); + + b.Property("EntryId") + .HasColumnType("uuid") + .HasColumnName("entry_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_at"); + + b.Property("LastClientSequence") + .HasColumnType("bigint") + .HasColumnName("last_client_sequence"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mode"); + + b.Property("QuestionCount") + .HasColumnType("integer") + .HasColumnName("question_count"); + + b.Property("StartedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at") + .HasDefaultValueSql("now()"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("active") + .HasColumnName("status"); + + b.Property("TargetId") + .HasColumnType("uuid") + .HasColumnName("target_id"); + + b.Property("TargetType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("target_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalScore") + .HasPrecision(8, 2) + .HasColumnType("numeric(8,2)") + .HasColumnName("total_score"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_practice_sessions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_sessions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "UserId", "Id") + .HasName("ak_practice_sessions_tenant_id_user_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_practice_sessions_user_id"); + + b.HasIndex("TenantId", "BlueprintId") + .HasDatabaseName("ix_practice_sessions_tenant_id_blueprint_id"); + + b.HasIndex("TenantId", "CollectionId") + .HasDatabaseName("ix_practice_sessions_tenant_id_collection_id"); + + b.HasIndex("TenantId", "ContentNodeId") + .HasDatabaseName("ix_practice_sessions_tenant_id_content_node_id"); + + b.HasIndex("TenantId", "EntryId") + .HasDatabaseName("ix_practice_sessions_tenant_id_entry_id"); + + b.HasIndex("TenantId", "UserId", "StartedAt") + .HasDatabaseName("ix_practice_sessions_tenant_id_user_id_started_at"); + + b.HasIndex("TenantId", "UserId", "Status", "StartedAt") + .HasDatabaseName("ix_practice_sessions_tenant_id_user_id_status_started_at"); + + b.ToTable("practice_sessions", null, t => + { + t.HasCheckConstraint("ck_practice_sessions_consumed_free_quota", "consumed_free_quota >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSessionQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerTextSnapshot") + .HasColumnType("text") + .HasColumnName("answer_text_snapshot"); + + b.Property("ContentSnapshot") + .HasColumnType("text") + .HasColumnName("content_snapshot"); + + b.Property("CorrectOptionIndexSnapshot") + .HasColumnType("integer") + .HasColumnName("correct_option_index_snapshot"); + + b.Property("CorrectOptionIndicesSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("correct_option_indices_snapshot") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("DifficultySnapshot") + .HasColumnType("integer") + .HasColumnName("difficulty_snapshot"); + + b.Property("ExplanationSnapshot") + .HasColumnType("text") + .HasColumnName("explanation_snapshot"); + + b.Property("GradingRulesSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("grading_rules_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("OptionsSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("options_snapshot") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("Position") + .HasColumnType("integer") + .HasColumnName("position"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("QuestionOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_owner_tenant_id"); + + b.Property("QuestionReferenceId") + .HasColumnType("uuid") + .HasColumnName("question_reference_id"); + + b.Property("QuestionType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("choice") + .HasColumnName("question_type"); + + b.Property("QuestionVersionId") + .HasColumnType("uuid") + .HasColumnName("question_version_id"); + + b.Property("Score") + .HasPrecision(8, 2) + .HasColumnType("numeric(8,2)") + .HasColumnName("score"); + + b.Property("SnapshotVersion") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("snapshot_version"); + + b.Property("TagsSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("tags_snapshot") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TypeLabelSnapshot") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label_snapshot"); + + b.HasKey("Id") + .HasName("pk_practice_session_questions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_session_questions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "PracticeSessionId", "Id") + .HasName("ak_practice_session_questions_tenant_id_practice_session_id_id"); + + b.HasIndex("QuestionOwnerTenantId", "QuestionId", "QuestionVersionId") + .HasDatabaseName("ix_practice_session_questions_question_owner_tenant_id_questio~"); + + b.HasIndex("TenantId", "PracticeSessionId", "Position") + .IsUnique() + .HasDatabaseName("ix_practice_session_questions_tenant_id_practice_session_id_po~"); + + b.HasIndex("TenantId", "QuestionOwnerTenantId", "QuestionId") + .HasDatabaseName("ix_practice_session_questions_tenant_id_question_owner_tenant_~"); + + b.ToTable("practice_session_questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSessionReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Accuracy") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)") + .HasColumnName("accuracy"); + + b.Property("AnsweredCount") + .HasColumnType("integer") + .HasColumnName("answered_count"); + + b.Property("BlueprintId") + .HasColumnType("uuid") + .HasColumnName("blueprint_id"); + + b.Property("CollectionId") + .HasColumnType("uuid") + .HasColumnName("collection_id"); + + b.Property("CorrectCount") + .HasColumnType("integer") + .HasColumnName("correct_count"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DurationSeconds") + .HasColumnType("integer") + .HasColumnName("duration_seconds"); + + b.Property("IsFinal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_final"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mode"); + + b.Property("PendingReviewCount") + .HasColumnType("integer") + .HasColumnName("pending_review_count"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("QuestionResults") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("question_results") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("Score") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("score"); + + b.Property("ScoringVersion") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("scoring_version"); + + b.Property("SectionStats") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("section_stats") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("final") + .HasColumnName("status"); + + b.Property("SubmittedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("submitted_at") + .HasDefaultValueSql("now()"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalQuestions") + .HasColumnType("integer") + .HasColumnName("total_questions"); + + b.Property("TotalScore") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("total_score"); + + b.Property("UnansweredCount") + .HasColumnType("integer") + .HasColumnName("unanswered_count"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("version"); + + b.Property("WrongCount") + .HasColumnType("integer") + .HasColumnName("wrong_count"); + + b.Property("WrongQuestionIds") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("wrong_question_ids") + .HasDefaultValueSql("'[]'::jsonb"); + + b.HasKey("Id") + .HasName("pk_practice_session_reports"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_session_reports_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_practice_session_reports_user_id"); + + b.HasIndex("TenantId", "CollectionId") + .HasDatabaseName("ix_practice_session_reports_tenant_id_collection_id"); + + b.HasIndex("TenantId", "PracticeSessionId") + .IsUnique() + .HasDatabaseName("ix_practice_session_reports_tenant_id_practice_session_id"); + + b.HasIndex("TenantId", "BlueprintId", "SubmittedAt") + .HasDatabaseName("ix_practice_session_reports_tenant_id_blueprint_id_submitted_at") + .HasFilter("blueprint_id is not null"); + + b.HasIndex("TenantId", "UserId", "SubmittedAt") + .HasDatabaseName("ix_practice_session_reports_tenant_id_user_id_submitted_at"); + + b.ToTable("practice_session_reports", null, t => + { + t.HasCheckConstraint("ck_practice_session_reports_accuracy", "accuracy >= 0 and accuracy <= 1"); + + t.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"); + + t.HasCheckConstraint("ck_practice_session_reports_duration", "duration_seconds >= 0"); + + t.HasCheckConstraint("ck_practice_session_reports_scores", "score >= 0 and total_score >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSessionReportSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Accuracy") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)") + .HasColumnName("accuracy"); + + b.Property("AnsweredCount") + .HasColumnType("integer") + .HasColumnName("answered_count"); + + b.Property("CorrectCount") + .HasColumnType("integer") + .HasColumnName("correct_count"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("QuestionCount") + .HasColumnType("integer") + .HasColumnName("question_count"); + + b.Property("QuestionType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("question_type"); + + b.Property("ReportId") + .HasColumnType("uuid") + .HasColumnName("report_id"); + + b.Property("Score") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("score"); + + b.Property("SectionKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("section_key"); + + b.Property("SectionName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("section_name"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalScore") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("total_score"); + + b.Property("UnansweredCount") + .HasColumnType("integer") + .HasColumnName("unanswered_count"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("WrongCount") + .HasColumnType("integer") + .HasColumnName("wrong_count"); + + b.HasKey("Id") + .HasName("pk_practice_session_report_sections"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_session_report_sections_tenant_id_id"); + + b.HasIndex("TenantId", "PracticeSessionId") + .HasDatabaseName("ix_practice_session_report_sections_tenant_id_practice_session~"); + + b.HasIndex("TenantId", "ReportId", "SectionKey") + .IsUnique() + .HasDatabaseName("ix_practice_session_report_sections_tenant_id_report_id_sectio~"); + + b.HasIndex("TenantId", "ReportId", "SortOrder") + .HasDatabaseName("ix_practice_session_report_sections_tenant_id_report_id_sort_o~"); + + b.ToTable("practice_session_report_sections", null, t => + { + t.HasCheckConstraint("ck_practice_session_report_sections_accuracy", "accuracy >= 0 and accuracy <= 1"); + + t.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"); + + t.HasCheckConstraint("ck_practice_session_report_sections_scores", "score >= 0 and total_score >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.RecentPractice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Color") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("color"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LastAccessAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_access_at"); + + b.Property("LastPracticeAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_practice_at"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PracticeType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("practice_type"); + + b.Property("Progress") + .HasColumnType("integer") + .HasColumnName("progress"); + + b.Property("TargetLegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("target_legacy_id"); + + b.Property("TargetName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("target_name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_recent_practices"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_recent_practices_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_recent_practices_user_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_recent_practices_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "UserId", "LastAccessAt") + .HasDatabaseName("ix_recent_practices_tenant_id_user_id_last_access_at"); + + b.ToTable("recent_practices", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.Report", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Attachments") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("attachments") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("category"); + + b.Property("Contact") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("contact"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("HandledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("handled_at"); + + b.Property("HandledBy") + .HasColumnType("uuid") + .HasColumnName("handled_by"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("priority"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("Resolution") + .HasColumnType("text") + .HasColumnName("resolution"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("title"); + + b.Property("Type") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_reports"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_reports_tenant_id_id"); + + b.HasIndex("HandledBy") + .HasDatabaseName("ix_reports_handled_by"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_reports_user_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_reports_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "QuestionId", "CreatedAt") + .HasDatabaseName("ix_reports_tenant_id_question_id_created_at") + .HasFilter("question_id is not null"); + + b.HasIndex("TenantId", "Status", "CreatedAt") + .HasDatabaseName("ix_reports_tenant_id_status_created_at"); + + b.ToTable("reports", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ReportStatusEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActorUserId") + .HasColumnType("uuid") + .HasColumnName("actor_user_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FromStatus") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("from_status"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Note") + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("ReportId") + .HasColumnType("uuid") + .HasColumnName("report_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("ToStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("to_status"); + + b.HasKey("Id") + .HasName("pk_report_status_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_report_status_events_tenant_id_id"); + + b.HasIndex("ActorUserId") + .HasDatabaseName("ix_report_status_events_actor_user_id"); + + b.HasIndex("TenantId", "ReportId", "CreatedAt") + .HasDatabaseName("ix_report_status_events_tenant_id_report_id_created_at"); + + b.ToTable("report_status_events", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.RevenueDailyStat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CodeCount") + .HasColumnType("integer") + .HasColumnName("code_count"); + + b.Property("CodeEstimated") + .HasColumnType("integer") + .HasColumnName("code_estimated"); + + b.Property("CodeUsed") + .HasColumnType("integer") + .HasColumnName("code_used"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EstimatedRevenueCents") + .HasColumnType("integer") + .HasColumnName("estimated_revenue_cents"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyRegionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_region_id"); + + b.Property("OrderCount") + .HasColumnType("integer") + .HasColumnName("order_count"); + + b.Property("RealRevenueCents") + .HasColumnType("integer") + .HasColumnName("real_revenue_cents"); + + b.Property("RebuiltAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("rebuilt_at"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SaleType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("sale_type"); + + b.Property("StatDate") + .HasColumnType("date") + .HasColumnName("stat_date"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_revenue_daily_stats"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_revenue_daily_stats_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_revenue_daily_stats_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_revenue_daily_stats_tenant_id_region_id"); + + b.HasIndex("TenantId", "StatDate", "RegionId", "SaleType") + .IsUnique() + .HasDatabaseName("ix_revenue_daily_stats_tenant_id_stat_date_region_id_sale_type"); + + NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("TenantId", "StatDate", "RegionId", "SaleType"), false); + + b.ToTable("revenue_daily_stats", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.UserScoreEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BalanceAfter") + .HasColumnType("integer") + .HasColumnName("balance_after"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("event_type"); + + b.Property("IdempotencyKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Points") + .HasColumnType("integer") + .HasColumnName("points"); + + b.Property("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_score_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_user_score_events_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_score_events_user_id"); + + b.HasIndex("TenantId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_user_score_events_tenant_id_idempotency_key"); + + b.HasIndex("TenantId", "UserId", "CreatedAt") + .HasDatabaseName("ix_user_score_events_tenant_id_user_id_created_at"); + + b.ToTable("user_score_events", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.WrongQuestion", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("QuestionReferenceId") + .HasColumnType("uuid") + .HasColumnName("question_reference_id"); + + b.Property("LastWrongAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_wrong_at") + .HasDefaultValueSql("now()"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("QuestionOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_owner_tenant_id"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("resolved_at"); + + b.Property("WrongCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("wrong_count"); + + b.HasKey("TenantId", "UserId", "QuestionReferenceId") + .HasName("pk_wrong_questions"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_wrong_questions_user_id"); + + b.HasIndex("QuestionOwnerTenantId", "QuestionId") + .HasDatabaseName("ix_wrong_questions_question_owner_tenant_id_question_id"); + + b.HasIndex("TenantId", "QuestionReferenceId") + .HasDatabaseName("ix_wrong_questions_tenant_id_question_reference_id"); + + b.ToTable("wrong_questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.Announcement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BackgroundColor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("background_color"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Link") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("link"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_announcements"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_announcements_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_announcements_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "IsActive", "SortOrder") + .HasDatabaseName("ix_announcements_tenant_id_is_active_sort_order"); + + b.ToTable("announcements", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("action"); + + b.Property("ActorUserId") + .HasColumnType("uuid") + .HasColumnName("actor_user_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Details") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("details") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("IpAddress") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("ip_address"); + + b.Property("TargetId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("target_id"); + + b.Property("TargetType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("target_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserAgent") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("user_agent"); + + b.HasKey("Id") + .HasName("pk_audit_logs"); + + b.HasIndex("ActorUserId") + .HasDatabaseName("ix_audit_logs_actor_user_id"); + + b.HasIndex("TenantId", "CreatedAt") + .HasDatabaseName("ix_audit_logs_tenant_id_created_at"); + + b.HasIndex("TenantId", "TargetType", "TargetId", "CreatedAt") + .HasDatabaseName("ix_audit_logs_tenant_id_target_type_target_id_created_at"); + + b.ToTable("audit_logs", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.AuthorizationCacheInvalidation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AttemptCount") + .HasColumnType("integer") + .HasColumnName("attempt_count"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at"); + + b.Property("Realm") + .HasColumnType("text") + .HasColumnName("realm"); + + b.Property("SessionId") + .HasColumnType("uuid") + .HasColumnName("session_id"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("target_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Version") + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_authorization_cache_invalidations"); + + b.HasIndex("ProcessedAt", "CreatedAt") + .HasDatabaseName("ix_authorization_cache_invalidations_processed_at_created_at"); + + b.HasIndex("TargetType", "TenantId", "UserId", "SessionId", "Version") + .HasDatabaseName("ix_authorization_cache_invalidations_target_type_tenant_id_use~"); + + b.ToTable("authorization_cache_invalidations", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.AuthorizationScopeVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Realm") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("realm"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_authorization_scope_versions"); + + b.HasIndex("Realm") + .IsUnique() + .HasDatabaseName("ix_authorization_scope_versions_realm") + .HasFilter("realm = 'platform'"); + + b.HasIndex("TenantId") + .IsUnique() + .HasDatabaseName("ix_authorization_scope_versions_tenant_id") + .HasFilter("tenant_id is not null"); + + b.ToTable("authorization_scope_versions", null, t => + { + t.HasCheckConstraint("ck_authorization_scope_versions_realm_tenant", "(realm = 'tenant' and tenant_id is not null) or (realm = 'platform' and tenant_id is null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.BackendMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Area") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("area"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Icon") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("icon"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("ParentCode") + .HasMaxLength(160) + .HasColumnType("character varying(160)") + .HasColumnName("parent_code"); + + b.Property("Path") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("path"); + + b.Property("PermissionCode") + .HasMaxLength(160) + .HasColumnType("character varying(160)") + .HasColumnName("permission_code"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_backend_menus"); + + b.HasAlternateKey("Code") + .HasName("ak_backend_menus_code"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_backend_menus_code"); + + b.HasIndex("PermissionCode") + .HasDatabaseName("ix_backend_menus_permission_code"); + + b.HasIndex("Area", "ParentCode", "SortOrder") + .HasDatabaseName("ix_backend_menus_area_parent_code_sort_order"); + + b.ToTable("backend_menus", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.BackendPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Area") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("area"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PermissionModuleCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("permission_module_code"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_backend_permissions"); + + b.HasAlternateKey("Code") + .HasName("ak_backend_permissions_code"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_backend_permissions_code"); + + b.HasIndex("PermissionModuleCode") + .HasDatabaseName("ix_backend_permissions_permission_module_code"); + + b.HasIndex("Area", "PermissionModuleCode", "SortOrder") + .HasDatabaseName("ix_backend_permissions_area_permission_module_code_sort_order"); + + b.ToTable("backend_permissions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.BackgroundJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CancellationReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("cancellation_reason"); + + b.Property("CancellationRequestedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("cancellation_requested_at"); + + b.Property("CancellationRequestedBy") + .HasColumnType("uuid") + .HasColumnName("cancellation_requested_by"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IdempotencyKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("JobType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("job_type"); + + b.Property("LastError") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("last_error"); + + b.Property("LockExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("lock_expires_at"); + + b.Property("LockedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("locked_by"); + + b.Property("MaxRetries") + .HasColumnType("integer") + .HasColumnName("max_retries"); + + b.Property("OutputAssetId") + .HasColumnType("uuid") + .HasColumnName("output_asset_id"); + + b.Property("Payload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Result") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("result") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RetryCount") + .HasColumnType("integer") + .HasColumnName("retry_count"); + + b.Property("RunAfter") + .HasColumnType("timestamp with time zone") + .HasColumnName("run_after"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_background_jobs"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_background_jobs_tenant_id_id"); + + b.HasIndex("TenantId", "OutputAssetId") + .HasDatabaseName("ix_background_jobs_tenant_id_output_asset_id"); + + b.HasIndex("TenantId", "JobType", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_background_jobs_tenant_id_job_type_idempotency_key") + .HasFilter("idempotency_key is not null"); + + b.HasIndex("TenantId", "JobType", "Status", "CreatedAt") + .HasDatabaseName("ix_background_jobs_tenant_id_job_type_status_created_at"); + + b.HasIndex("TenantId", "Status", "RunAfter", "CreatedAt") + .HasDatabaseName("ix_background_jobs_tenant_id_status_run_after_created_at"); + + b.ToTable("background_jobs", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.Badge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("category"); + + b.Property("ConditionExtra") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("condition_extra") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ConditionField") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("condition_field"); + + b.Property("ConditionOperator") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("condition_operator"); + + b.Property("ConditionValue") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)") + .HasColumnName("condition_value"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IconUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("icon_url"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Level") + .HasColumnType("integer") + .HasColumnName("level"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UnlockType") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("unlock_type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_badges"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_badges_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_badges_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "Category", "IsActive", "SortOrder") + .HasDatabaseName("ix_badges_tenant_id_category_is_active_sort_order"); + + b.ToTable("badges", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.Banner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BackgroundColor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("background_color"); + + b.Property("BorderColor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("border_color"); + + b.Property("ButtonLink") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("button_link"); + + b.Property("ButtonText") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("button_text"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Subtitle") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("subtitle"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_banners"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_banners_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_banners_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId", "IsActive", "SortOrder") + .HasDatabaseName("ix_banners_tenant_id_region_id_is_active_sort_order"); + + b.ToTable("banners", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.Faq", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Answer") + .HasColumnType("text") + .HasColumnName("answer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Question") + .HasColumnType("text") + .HasColumnName("question"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_faqs"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_faqs_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_faqs_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId", "IsActive", "SortOrder") + .HasDatabaseName("ix_faqs_tenant_id_region_id_is_active_sort_order"); + + b.ToTable("faqs", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_backend_roles"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_backend_roles_code"); + + b.HasIndex("Status", "Code") + .HasDatabaseName("ix_platform_backend_roles_status_code"); + + b.ToTable("platform_backend_roles", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRoleMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("MenuCode") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)") + .HasColumnName("menu_code"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_platform_backend_role_menus"); + + b.HasIndex("MenuCode") + .HasDatabaseName("ix_platform_backend_role_menus_menu_code"); + + b.HasIndex("RoleId", "MenuCode") + .IsUnique() + .HasDatabaseName("ix_platform_backend_role_menus_role_id_menu_code"); + + b.ToTable("platform_backend_role_menus", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("PermissionCode") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)") + .HasColumnName("permission_code"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_platform_backend_role_permissions"); + + b.HasIndex("PermissionCode") + .HasDatabaseName("ix_platform_backend_role_permissions_permission_code"); + + b.HasIndex("RoleId", "PermissionCode") + .IsUnique() + .HasDatabaseName("ix_platform_backend_role_permissions_role_id_permission_code"); + + b.ToTable("platform_backend_role_permissions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendUserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_platform_backend_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_platform_backend_user_roles_role_id"); + + b.HasIndex("UserId", "RoleId") + .IsUnique() + .HasDatabaseName("ix_platform_backend_user_roles_user_id_role_id"); + + b.ToTable("platform_backend_user_roles", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DataScope") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("data_scope") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_backend_roles"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_backend_roles_tenant_id_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_tenant_backend_roles_tenant_id_code"); + + b.HasIndex("TenantId", "Status", "Code") + .HasDatabaseName("ix_tenant_backend_roles_tenant_id_status_code"); + + b.ToTable("tenant_backend_roles", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRoleMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("MenuCode") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)") + .HasColumnName("menu_code"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_tenant_backend_role_menus"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_backend_role_menus_tenant_id_id"); + + b.HasIndex("MenuCode") + .HasDatabaseName("ix_tenant_backend_role_menus_menu_code"); + + b.HasIndex("TenantId", "RoleId", "MenuCode") + .IsUnique() + .HasDatabaseName("ix_tenant_backend_role_menus_tenant_id_role_id_menu_code"); + + b.ToTable("tenant_backend_role_menus", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("PermissionCode") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)") + .HasColumnName("permission_code"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_tenant_backend_role_permissions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_backend_role_permissions_tenant_id_id"); + + b.HasIndex("PermissionCode") + .HasDatabaseName("ix_tenant_backend_role_permissions_permission_code"); + + b.HasIndex("TenantId", "RoleId", "PermissionCode") + .IsUnique() + .HasDatabaseName("ix_tenant_backend_role_permissions_tenant_id_role_id_permissio~"); + + b.ToTable("tenant_backend_role_permissions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendUserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tenant_backend_user_roles"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_backend_user_roles_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tenant_backend_user_roles_user_id"); + + b.HasIndex("TenantId", "RoleId") + .HasDatabaseName("ix_tenant_backend_user_roles_tenant_id_role_id"); + + b.HasIndex("TenantId", "UserId", "RoleId") + .IsUnique() + .HasDatabaseName("ix_tenant_backend_user_roles_tenant_id_user_id_role_id"); + + b.ToTable("tenant_backend_user_roles", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantContentNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActionLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("action_label"); + + b.Property("ActionPath") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("action_path"); + + b.Property("AdoptionId") + .HasColumnType("uuid") + .HasColumnName("adoption_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("DedupeKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("dedupe_key"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("NotificationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("notification_type"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("read_at"); + + b.Property("ReadBy") + .HasColumnType("uuid") + .HasColumnName("read_by"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("resolved_at"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("severity"); + + b.Property("SourceQuestionBankId") + .HasColumnType("uuid") + .HasColumnName("source_question_bank_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_content_notifications"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_content_notifications_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_tenant_content_notifications_created_by"); + + b.HasIndex("ReadBy") + .HasDatabaseName("ix_tenant_content_notifications_read_by"); + + b.HasIndex("TenantId", "SourceQuestionBankId") + .HasDatabaseName("ix_tenant_content_notifications_tenant_id_source_question_bank~"); + + b.HasIndex("TenantId", "NotificationType", "DedupeKey") + .IsUnique() + .HasDatabaseName("ix_tenant_content_notifications_tenant_id_notification_type_de~") + .HasFilter("dedupe_key is not null"); + + b.HasIndex("TenantId", "Status", "CreatedAt") + .HasDatabaseName("ix_tenant_content_notifications_tenant_id_status_created_at"); + + b.HasIndex("TenantId", "AdoptionId", "NotificationType", "Status") + .HasDatabaseName("ix_tenant_content_notifications_tenant_id_adoption_id_notifica~"); + + b.ToTable("tenant_content_notifications", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantLifecycleOperation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ExportAssetId") + .HasColumnType("uuid") + .HasColumnName("export_asset_id"); + + b.Property("LastError") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("last_error"); + + b.Property("OperationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("operation_type"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("RequestedBy") + .HasColumnType("uuid") + .HasColumnName("requested_by"); + + b.Property("Result") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("result") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TargetUserId") + .HasColumnType("uuid") + .HasColumnName("target_user_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_lifecycle_operations"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_lifecycle_operations_tenant_id_id"); + + b.HasIndex("TenantId", "ExportAssetId") + .HasDatabaseName("ix_tenant_lifecycle_operations_tenant_id_export_asset_id"); + + b.HasIndex("TenantId", "OperationType", "Status", "CreatedAt") + .HasDatabaseName("ix_tenant_lifecycle_operations_tenant_id_operation_type_status~"); + + b.ToTable("tenant_lifecycle_operations", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantThemeConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActivePublicAssets") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("active_public_assets") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ActiveTemplateCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("active_template_code"); + + b.Property("ActiveTheme") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("active_theme") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DraftPublicAssets") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("draft_public_assets") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("DraftTemplateCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("draft_template_code"); + + b.Property("DraftTheme") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("draft_theme") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("DraftUpdatedBy") + .HasColumnType("uuid") + .HasColumnName("draft_updated_by"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("PublishedBy") + .HasColumnType("uuid") + .HasColumnName("published_by"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_theme_configs"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_theme_configs_tenant_id_id"); + + b.HasIndex("ActiveTemplateCode") + .HasDatabaseName("ix_tenant_theme_configs_active_template_code"); + + b.HasIndex("DraftTemplateCode") + .HasDatabaseName("ix_tenant_theme_configs_draft_template_code"); + + b.HasIndex("DraftUpdatedBy") + .HasDatabaseName("ix_tenant_theme_configs_draft_updated_by"); + + b.HasIndex("PublishedBy") + .HasDatabaseName("ix_tenant_theme_configs_published_by"); + + b.HasIndex("TenantId") + .IsUnique() + .HasDatabaseName("ix_tenant_theme_configs_tenant_id"); + + b.ToTable("tenant_theme_configs", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantThemeTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PreviewImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("preview_image_url"); + + b.Property("PublicAssets") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("public_assets") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("Theme") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("theme") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_theme_templates"); + + b.HasAlternateKey("Code") + .HasName("ak_tenant_theme_templates_code"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_tenant_theme_templates_code"); + + b.HasIndex("Status", "SortOrder", "Code") + .HasDatabaseName("ix_tenant_theme_templates_status_sort_order_code"); + + b.ToTable("tenant_theme_templates", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.UserBadge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BadgeId") + .HasColumnType("uuid") + .HasColumnName("badge_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("GrantedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("granted_at"); + + b.Property("GrantedBy") + .HasColumnType("uuid") + .HasColumnName("granted_by"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Note") + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_badges"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_user_badges_tenant_id_id"); + + b.HasIndex("GrantedBy") + .HasDatabaseName("ix_user_badges_granted_by"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_badges_user_id"); + + b.HasIndex("TenantId", "BadgeId") + .HasDatabaseName("ix_user_badges_tenant_id_badge_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_user_badges_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "UserId", "BadgeId") + .IsUnique() + .HasDatabaseName("ix_user_badges_tenant_id_user_id_badge_id"); + + b.ToTable("user_badges", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.UserNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActionLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("action_label"); + + b.Property("ActionPath") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("action_path"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("DedupeKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("dedupe_key"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("NotificationType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("notification_type"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("read_at"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("severity"); + + b.Property("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_type"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_notifications"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_user_notifications_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_user_notifications_created_by"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_notifications_user_id"); + + b.HasIndex("TenantId", "SourceType", "SourceId") + .HasDatabaseName("ix_user_notifications_tenant_id_source_type_source_id"); + + b.HasIndex("TenantId", "UserId", "NotificationType", "DedupeKey") + .IsUnique() + .HasDatabaseName("ix_user_notifications_tenant_id_user_id_notification_type_dedu~") + .HasFilter("dedupe_key is not null"); + + b.HasIndex("TenantId", "UserId", "Status", "CreatedAt") + .HasDatabaseName("ix_user_notifications_tenant_id_user_id_status_created_at"); + + b.ToTable("user_notifications", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.WorkerHeartbeat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastError") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("last_error"); + + b.Property("LastHeartbeatAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_at"); + + b.Property("LastIterationCompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_iteration_completed_at"); + + b.Property("LastIterationStartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_iteration_started_at"); + + b.Property("LastSucceededAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_succeeded_at"); + + b.Property("Processor") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("processor"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("WorkerId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("worker_id"); + + b.HasKey("Id") + .HasName("pk_worker_heartbeats"); + + b.HasIndex("Processor", "LastHeartbeatAt") + .HasDatabaseName("ix_worker_heartbeats_processor_last_heartbeat_at"); + + b.HasIndex("WorkerId", "Processor") + .IsUnique() + .HasDatabaseName("ix_worker_heartbeats_worker_id_processor"); + + b.ToTable("worker_heartbeats", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PermissionModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Area") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("area"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RequiredFeatureCode") + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("required_feature_code"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_permission_modules"); + + b.HasAlternateKey("Code") + .HasName("ak_permission_modules_code"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_permission_modules_code"); + + b.HasIndex("RequiredFeatureCode") + .HasDatabaseName("ix_permission_modules_required_feature_code"); + + b.HasIndex("Area", "SortOrder") + .HasDatabaseName("ix_permission_modules_area_sort_order"); + + b.ToTable("permission_modules", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformAuditAlert", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AcknowledgedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("acknowledged_at"); + + b.Property("AcknowledgedBy") + .HasColumnType("uuid") + .HasColumnName("acknowledged_by"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("action"); + + b.Property("AuditLogId") + .HasColumnType("uuid") + .HasColumnName("audit_log_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Details") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("details") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("FirstSeenAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_at") + .HasDefaultValueSql("now()"); + + b.Property("LastSeenAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_at") + .HasDefaultValueSql("now()"); + + b.Property("ResolutionNote") + .HasColumnType("text") + .HasColumnName("resolution_note"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("resolved_at"); + + b.Property("ResolvedBy") + .HasColumnType("uuid") + .HasColumnName("resolved_by"); + + b.Property("RuleId") + .HasColumnType("uuid") + .HasColumnName("rule_id"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("severity"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("Summary") + .HasColumnType("text") + .HasColumnName("summary"); + + b.Property("TargetId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("target_id"); + + b.Property("TargetType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("target_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_audit_alerts"); + + b.HasIndex("AcknowledgedBy") + .HasDatabaseName("ix_platform_audit_alerts_acknowledged_by"); + + b.HasIndex("AuditLogId") + .HasDatabaseName("ix_platform_audit_alerts_audit_log_id"); + + b.HasIndex("ResolvedBy") + .HasDatabaseName("ix_platform_audit_alerts_resolved_by"); + + b.HasIndex("RuleId", "AuditLogId") + .IsUnique() + .HasDatabaseName("ix_platform_audit_alerts_rule_id_audit_log_id"); + + b.HasIndex("Status", "Severity", "CreatedAt") + .HasDatabaseName("ix_platform_audit_alerts_status_severity_created_at"); + + b.HasIndex("TenantId", "Status", "CreatedAt") + .HasDatabaseName("ix_platform_audit_alerts_tenant_id_status_created_at"); + + b.ToTable("platform_audit_alerts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformAuditAlertRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.PrimitiveCollection("ActionPatterns") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text[]") + .HasColumnName("action_patterns") + .HasDefaultValueSql("'{}'::text[]"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("Conditions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("conditions") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("severity"); + + b.PrimitiveCollection("TargetTypes") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text[]") + .HasColumnName("target_types") + .HasDefaultValueSql("'{}'::text[]"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_audit_alert_rules"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_audit_alert_rules_code"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_platform_audit_alert_rules_tenant_id"); + + b.HasIndex("Enabled", "Severity", "Code") + .HasDatabaseName("ix_platform_audit_alert_rules_enabled_severity_code"); + + b.ToTable("platform_audit_alert_rules", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingDunningNotificationChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ChannelCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("channel_code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("MinReminderLevel") + .HasColumnType("integer") + .HasColumnName("min_reminder_level"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("provider"); + + b.PrimitiveCollection("ReminderChannels") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text[]") + .HasColumnName("reminder_channels") + .HasDefaultValueSql("array['internal']::text[]"); + + b.PrimitiveCollection("ReminderTypes") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text[]") + .HasColumnName("reminder_types") + .HasDefaultValueSql("array['overdue', 'final_notice']::text[]"); + + b.Property("SecretRef") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("secret_ref"); + + b.PrimitiveCollection("TenantIds") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("uuid[]") + .HasColumnName("tenant_ids") + .HasDefaultValueSql("'{}'::uuid[]"); + + b.Property("TimeoutSeconds") + .HasColumnType("integer") + .HasColumnName("timeout_seconds"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("WebhookUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("webhook_url"); + + b.HasKey("Id") + .HasName("pk_platform_billing_dunning_notification_channels"); + + b.HasIndex("ChannelCode") + .IsUnique() + .HasDatabaseName("ix_platform_billing_dunning_notification_channels_channel_code"); + + b.HasIndex("Enabled", "MinReminderLevel", "ChannelCode") + .HasDatabaseName("ix_platform_billing_dunning_notification_channels_enabled_min_~"); + + b.ToTable("platform_billing_dunning_notification_channels", null, t => + { + t.HasCheckConstraint("ck_platform_billing_dunning_channels_level", "min_reminder_level between 1 and 20"); + + t.HasCheckConstraint("ck_platform_billing_dunning_channels_timeout", "timeout_seconds between 1 and 60"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingDunningNotificationEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("ChannelId") + .HasColumnType("uuid") + .HasColumnName("channel_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("InvoiceId") + .HasColumnType("uuid") + .HasColumnName("invoice_id"); + + b.Property("LastAttemptAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_at"); + + b.Property("LastError") + .HasColumnType("text") + .HasColumnName("last_error"); + + b.Property("LastHttpCode") + .HasColumnType("integer") + .HasColumnName("last_http_code"); + + b.Property("LastResponseSummary") + .HasColumnType("text") + .HasColumnName("last_response_summary"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("NextAttemptAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("next_attempt_at"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("provider"); + + b.Property("ReminderId") + .HasColumnType("uuid") + .HasColumnName("reminder_id"); + + b.Property("RequestPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("request_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ScheduledAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("scheduled_at") + .HasDefaultValueSql("now()"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("sent_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_billing_dunning_notification_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_billing_dunning_notification_events_tenant_id_id"); + + b.HasIndex("ChannelId") + .HasDatabaseName("ix_platform_billing_dunning_notification_events_channel_id"); + + b.HasIndex("TenantId", "InvoiceId") + .HasDatabaseName("ix_platform_billing_dunning_notification_events_tenant_id_invo~"); + + b.HasIndex("TenantId", "ReminderId") + .HasDatabaseName("ix_platform_billing_dunning_notification_events_tenant_id_remi~"); + + b.HasIndex("InvoiceId", "Status", "CreatedAt") + .HasDatabaseName("ix_platform_billing_dunning_notification_events_invoice_id_sta~"); + + b.HasIndex("ReminderId", "Status", "CreatedAt") + .HasDatabaseName("ix_platform_billing_dunning_notification_events_reminder_id_st~"); + + b.HasIndex("TenantId", "ChannelId", "ReminderId") + .IsUnique() + .HasDatabaseName("ix_platform_billing_dunning_notification_events_tenant_id_chan~"); + + b.HasIndex("TenantId", "Status", "CreatedAt") + .HasDatabaseName("ix_platform_billing_dunning_notification_events_tenant_id_stat~"); + + b.HasIndex("Status", "NextAttemptAt", "ScheduledAt", "CreatedAt") + .HasDatabaseName("ix_platform_billing_dunning_notification_events_status_next_at~"); + + b.ToTable("platform_billing_dunning_notification_events", null, t => + { + t.HasCheckConstraint("ck_platform_billing_dunning_events_attempts", "attempts >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingInvoice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BillingProfileSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("billing_profile_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("currency"); + + b.Property("DueDate") + .HasColumnType("date") + .HasColumnName("due_date"); + + b.Property("InvoiceNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("invoice_no"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("issued_at"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("paid_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalAmountCents") + .HasColumnType("integer") + .HasColumnName("total_amount_cents"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_billing_invoices"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_billing_invoices_tenant_id_id"); + + b.HasIndex("TenantId", "InvoiceNo") + .IsUnique() + .HasDatabaseName("ix_platform_billing_invoices_tenant_id_invoice_no"); + + b.HasIndex("TenantId", "OrderId") + .HasDatabaseName("ix_platform_billing_invoices_tenant_id_order_id"); + + b.HasIndex("TenantId", "Status", "DueDate") + .HasDatabaseName("ix_platform_billing_invoices_tenant_id_status_due_date"); + + b.ToTable("platform_billing_invoices", null, t => + { + t.HasCheckConstraint("ck_platform_billing_invoices_amount", "total_amount_cents >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingInvoiceReminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AcknowledgedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("acknowledged_at"); + + b.Property("BalanceCentsSnapshot") + .HasColumnType("integer") + .HasColumnName("balance_cents_snapshot"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("channel"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("DueDate") + .HasColumnType("date") + .HasColumnName("due_date"); + + b.Property("InvoiceId") + .HasColumnType("uuid") + .HasColumnName("invoice_id"); + + b.Property("Message") + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ReminderDate") + .HasColumnType("date") + .HasColumnName("reminder_date"); + + b.Property("ReminderLevel") + .HasColumnType("integer") + .HasColumnName("reminder_level"); + + b.Property("ReminderType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("reminder_type"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("sent_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_billing_invoice_reminders"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_billing_invoice_reminders_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_platform_billing_invoice_reminders_created_by"); + + b.HasIndex("InvoiceId", "ReminderDate") + .HasDatabaseName("ix_platform_billing_invoice_reminders_invoice_id_reminder_date"); + + b.HasIndex("TenantId", "Status", "ReminderDate") + .HasDatabaseName("ix_platform_billing_invoice_reminders_tenant_id_status_reminde~"); + + b.HasIndex("TenantId", "InvoiceId", "ReminderType", "Channel", "ReminderDate") + .IsUnique() + .HasDatabaseName("ix_platform_billing_invoice_reminders_tenant_id_invoice_id_rem~"); + + b.ToTable("platform_billing_invoice_reminders", null, t => + { + t.HasCheckConstraint("ck_platform_billing_invoice_reminders_balance", "balance_cents_snapshot >= 0"); + + t.HasCheckConstraint("ck_platform_billing_invoice_reminders_level", "reminder_level between 1 and 20"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("cancelled_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("currency"); + + b.Property("DiscountAmountCents") + .HasColumnType("integer") + .HasColumnName("discount_amount_cents"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("order_no"); + + b.Property("OriginalAmountCents") + .HasColumnType("integer") + .HasColumnName("original_amount_cents"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("paid_at"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("purpose"); + + b.Property("QuoteId") + .HasColumnType("uuid") + .HasColumnName("quote_id"); + + b.Property("Snapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalAmountCents") + .HasColumnType("integer") + .HasColumnName("total_amount_cents"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_billing_orders"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_billing_orders_tenant_id_id"); + + b.HasIndex("TenantId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_billing_orders_tenant_id_idempotency_key"); + + b.HasIndex("TenantId", "OrderNo") + .IsUnique() + .HasDatabaseName("ix_platform_billing_orders_tenant_id_order_no"); + + b.HasIndex("TenantId", "QuoteId") + .HasDatabaseName("ix_platform_billing_orders_tenant_id_quote_id"); + + b.HasIndex("TenantId", "Status", "CreatedAt") + .HasDatabaseName("ix_platform_billing_orders_tenant_id_status_created_at"); + + b.ToTable("platform_billing_orders", null, t => + { + t.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"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingOrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("ItemType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("item_type"); + + b.Property("OfferingVersionId") + .HasColumnType("uuid") + .HasColumnName("offering_version_id"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("quantity"); + + b.Property("Snapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UnitAmountCents") + .HasColumnType("integer") + .HasColumnName("unit_amount_cents"); + + b.HasKey("Id") + .HasName("pk_platform_billing_order_items"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_billing_order_items_tenant_id_id"); + + b.HasIndex("OfferingVersionId") + .HasDatabaseName("ix_platform_billing_order_items_offering_version_id"); + + b.HasIndex("TenantId", "OrderId", "OfferingVersionId") + .IsUnique() + .HasDatabaseName("ix_platform_billing_order_items_tenant_id_order_id_offering_ve~"); + + b.ToTable("platform_billing_order_items", null, t => + { + t.HasCheckConstraint("ck_platform_billing_order_items_amounts", "quantity > 0 and unit_amount_cents >= 0 and amount_cents >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingPayment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("ClientPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("client_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("Method") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("method"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("paid_at"); + + b.Property("PaymentNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("payment_no"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("ProviderTradeNo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("provider_trade_no"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_billing_payments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_billing_payments_tenant_id_id"); + + b.HasIndex("TenantId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_billing_payments_tenant_id_idempotency_key"); + + b.HasIndex("TenantId", "PaymentNo") + .IsUnique() + .HasDatabaseName("ix_platform_billing_payments_tenant_id_payment_no"); + + b.HasIndex("TenantId", "OrderId", "Status") + .HasDatabaseName("ix_platform_billing_payments_tenant_id_order_id_status"); + + b.ToTable("platform_billing_payments", null, t => + { + t.HasCheckConstraint("ck_platform_billing_payments_amount", "amount_cents >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingPaymentEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("event_type"); + + b.Property("Payload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PaymentId") + .HasColumnType("uuid") + .HasColumnName("payment_id"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("ProviderEventId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("provider_event_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_platform_billing_payment_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_billing_payment_events_tenant_id_id"); + + b.HasIndex("TenantId", "PaymentId", "CreatedAt") + .HasDatabaseName("ix_platform_billing_payment_events_tenant_id_payment_id_create~"); + + b.HasIndex("TenantId", "Provider", "ProviderEventId") + .IsUnique() + .HasDatabaseName("ix_platform_billing_payment_events_tenant_id_provider_provider~"); + + b.ToTable("platform_billing_payment_events", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingQuote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("currency"); + + b.Property("DiscountAmountCents") + .HasColumnType("integer") + .HasColumnName("discount_amount_cents"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("FeatureSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("feature_snapshot") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("LimitSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("limit_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("OriginalAmountCents") + .HasColumnType("integer") + .HasColumnName("original_amount_cents"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("purpose"); + + b.Property("QuoteNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("quote_no"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalAmountCents") + .HasColumnType("integer") + .HasColumnName("total_amount_cents"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_billing_quotes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_billing_quotes_tenant_id_id"); + + b.HasIndex("TenantId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_billing_quotes_tenant_id_idempotency_key"); + + b.HasIndex("TenantId", "QuoteNo") + .IsUnique() + .HasDatabaseName("ix_platform_billing_quotes_tenant_id_quote_no"); + + b.HasIndex("TenantId", "Status", "ExpiresAt") + .HasDatabaseName("ix_platform_billing_quotes_tenant_id_status_expires_at"); + + b.ToTable("platform_billing_quotes", null, t => + { + t.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"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingQuoteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("ItemType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("item_type"); + + b.Property("OfferingVersionId") + .HasColumnType("uuid") + .HasColumnName("offering_version_id"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("quantity"); + + b.Property("QuoteId") + .HasColumnType("uuid") + .HasColumnName("quote_id"); + + b.Property("Snapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UnitAmountCents") + .HasColumnType("integer") + .HasColumnName("unit_amount_cents"); + + b.HasKey("Id") + .HasName("pk_platform_billing_quote_items"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_billing_quote_items_tenant_id_id"); + + b.HasIndex("OfferingVersionId") + .HasDatabaseName("ix_platform_billing_quote_items_offering_version_id"); + + b.HasIndex("TenantId", "QuoteId", "OfferingVersionId") + .IsUnique() + .HasDatabaseName("ix_platform_billing_quote_items_tenant_id_quote_id_offering_ve~"); + + b.ToTable("platform_billing_quote_items", null, t => + { + t.HasCheckConstraint("ck_platform_billing_quote_items_amounts", "quantity > 0 and unit_amount_cents >= 0 and amount_cents >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingRefund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("PaymentId") + .HasColumnType("uuid") + .HasColumnName("payment_id"); + + b.Property("ProviderRefundNo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("provider_refund_no"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("RefundNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("refund_no"); + + b.Property("RequestedBy") + .HasColumnType("uuid") + .HasColumnName("requested_by"); + + b.Property("ReviewReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("review_reason"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("reviewed_at"); + + b.Property("ReviewedBy") + .HasColumnType("uuid") + .HasColumnName("reviewed_by"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("SubscriptionEffect") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("subscription_effect"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_billing_refunds"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_billing_refunds_tenant_id_id"); + + b.HasIndex("TenantId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_billing_refunds_tenant_id_idempotency_key"); + + b.HasIndex("TenantId", "PaymentId") + .HasDatabaseName("ix_platform_billing_refunds_tenant_id_payment_id"); + + b.HasIndex("TenantId", "RefundNo") + .IsUnique() + .HasDatabaseName("ix_platform_billing_refunds_tenant_id_refund_no"); + + b.HasIndex("TenantId", "OrderId", "Status") + .HasDatabaseName("ix_platform_billing_refunds_tenant_id_order_id_status"); + + b.ToTable("platform_billing_refunds", null, t => + { + t.HasCheckConstraint("ck_platform_billing_refunds_amount", "amount_cents > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformOperationIdempotency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActorUserId") + .HasColumnType("uuid") + .HasColumnName("actor_user_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("RequestHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("request_hash"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("scope"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_operation_idempotencies"); + + b.HasIndex("ActorUserId", "Scope", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_operation_idempotencies_actor_user_id_scope_idempo~"); + + b.ToTable("platform_operation_idempotencies", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformPaymentApp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AppCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("app_code"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("app_name"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SettlementMode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("settlement_mode"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_payment_apps"); + + b.HasIndex("AppCode") + .IsUnique() + .HasDatabaseName("ix_platform_payment_apps_app_code"); + + b.HasIndex("Status", "AppCode") + .HasDatabaseName("ix_platform_payment_apps_status_app_code"); + + b.ToTable("platform_payment_apps", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformPaymentChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AppId") + .HasColumnType("uuid") + .HasColumnName("app_id"); + + b.Property("CallbackPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("callback_path"); + + b.Property("ConfigPublic") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("config_public") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("mode"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)") + .HasColumnName("provider"); + + b.Property("SecretRef") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("secret_ref"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_payment_channels"); + + b.HasIndex("AppId", "Provider") + .IsUnique() + .HasDatabaseName("ix_platform_payment_channels_app_id_provider"); + + b.HasIndex("Status", "Priority", "Provider") + .HasDatabaseName("ix_platform_payment_channels_status_priority_provider"); + + b.ToTable("platform_payment_channels", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.SaasFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("category"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("currency"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("IsCore") + .HasColumnType("boolean") + .HasColumnName("is_core"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("ReferencePriceCents") + .HasColumnType("integer") + .HasColumnName("reference_price_cents"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_saas_features"); + + b.HasAlternateKey("Code") + .HasName("ak_saas_features_code"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_saas_features_code"); + + b.HasIndex("Status", "Category", "SortOrder") + .HasDatabaseName("ix_saas_features_status_category_sort_order"); + + b.ToTable("saas_features", null, t => + { + t.HasCheckConstraint("ck_saas_features_reference_price", "reference_price_cents >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.SaasFeatureLimitDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FeatureCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("feature_code"); + + b.Property("IsHardLimit") + .HasColumnType("boolean") + .HasColumnName("is_hard_limit"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("kind"); + + b.Property("MetricCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("metric_code"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("unit"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("WarningPercent") + .HasColumnType("integer") + .HasColumnName("warning_percent"); + + b.HasKey("Id") + .HasName("pk_saas_feature_limit_definitions"); + + b.HasAlternateKey("MetricCode") + .HasName("ak_saas_feature_limit_definitions_metric_code"); + + b.HasIndex("MetricCode") + .IsUnique() + .HasDatabaseName("ix_saas_feature_limit_definitions_metric_code"); + + b.HasIndex("FeatureCode", "Kind") + .HasDatabaseName("ix_saas_feature_limit_definitions_feature_code_kind"); + + b.ToTable("saas_feature_limit_definitions", null, t => + { + t.HasCheckConstraint("ck_saas_feature_limit_warning", "warning_percent between 1 and 100"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.SaasOffering", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_saas_offerings"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_saas_offerings_code"); + + b.HasIndex("Type", "Status", "SortOrder") + .HasDatabaseName("ix_saas_offerings_type_status_sort_order"); + + b.ToTable("saas_offerings", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.SaasOfferingVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("BillingCycle") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("billing_cycle"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("currency"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("effective_at"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("OfferingId") + .HasColumnType("uuid") + .HasColumnName("offering_id"); + + b.Property("OriginalAmountCents") + .HasColumnType("integer") + .HasColumnName("original_amount_cents"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("RetiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_saas_offering_versions"); + + b.HasIndex("OfferingId", "Version") + .IsUnique() + .HasDatabaseName("ix_saas_offering_versions_offering_id_version"); + + b.HasIndex("OfferingId", "Status", "EffectiveAt") + .HasDatabaseName("ix_saas_offering_versions_offering_id_status_effective_at"); + + b.ToTable("saas_offering_versions", null, t => + { + t.HasCheckConstraint("ck_saas_offering_versions_amount", "original_amount_cents >= 0 and amount_cents >= 0 and amount_cents <= original_amount_cents"); + + t.HasCheckConstraint("ck_saas_offering_versions_version", "version > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.SaasOfferingVersionFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("FeatureCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("feature_code"); + + b.Property("OfferingVersionId") + .HasColumnType("uuid") + .HasColumnName("offering_version_id"); + + b.HasKey("Id") + .HasName("pk_saas_offering_version_features"); + + b.HasIndex("FeatureCode") + .HasDatabaseName("ix_saas_offering_version_features_feature_code"); + + b.HasIndex("OfferingVersionId", "FeatureCode") + .IsUnique() + .HasDatabaseName("ix_saas_offering_version_features_offering_version_id_feature_~"); + + b.ToTable("saas_offering_version_features", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.SaasOfferingVersionLimit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("LimitValue") + .HasColumnType("bigint") + .HasColumnName("limit_value"); + + b.Property("MetricCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("metric_code"); + + b.Property("OfferingVersionId") + .HasColumnType("uuid") + .HasColumnName("offering_version_id"); + + b.HasKey("Id") + .HasName("pk_saas_offering_version_limits"); + + b.HasIndex("MetricCode") + .HasDatabaseName("ix_saas_offering_version_limits_metric_code"); + + b.HasIndex("OfferingVersionId", "MetricCode") + .IsUnique() + .HasDatabaseName("ix_saas_offering_version_limits_offering_version_id_metric_code"); + + b.ToTable("saas_offering_version_limits", null, t => + { + t.HasCheckConstraint("ck_saas_offering_version_limits_value", "limit_value >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantBillingPolicy", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("AutoGenerateRenewal") + .HasColumnType("boolean") + .HasColumnName("auto_generate_renewal"); + + b.Property("CollectionMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("collection_mode"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DefaultPaymentProvider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("default_payment_provider"); + + b.Property("RenewalLeadDays") + .HasColumnType("integer") + .HasColumnName("renewal_lead_days"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("TenantId") + .HasName("pk_tenant_billing_policies"); + + b.ToTable("tenant_billing_policies", null, t => + { + t.HasCheckConstraint("ck_tenant_billing_policies_renewal_lead_days", "renewal_lead_days between 1 and 90"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantBillingProfile", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("BankAccountMasked") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("bank_account_masked"); + + b.Property("BankName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("bank_name"); + + b.Property("BillingAddress") + .HasColumnType("text") + .HasColumnName("billing_address"); + + b.Property("BillingName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("billing_name"); + + b.Property("ContactEmail") + .HasMaxLength(300) + .HasColumnType("citext") + .HasColumnName("contact_email"); + + b.Property("ContactName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("contact_name"); + + b.Property("ContactPhone") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("contact_phone"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("InvoiceTitle") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("invoice_title"); + + b.Property("InvoiceType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("invoice_type"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TaxId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("tax_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("TenantId") + .HasName("pk_tenant_billing_profiles"); + + b.ToTable("tenant_billing_profiles", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantFeatureOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("FeatureCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("feature_code"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("mode"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_feature_overrides"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_feature_overrides_tenant_id_id"); + + b.HasIndex("FeatureCode") + .HasDatabaseName("ix_tenant_feature_overrides_feature_code"); + + b.HasIndex("TenantId", "FeatureCode") + .IsUnique() + .HasDatabaseName("ix_tenant_feature_overrides_tenant_id_feature_code"); + + b.ToTable("tenant_feature_overrides", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantFeatureUsage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LimitValueSnapshot") + .HasColumnType("bigint") + .HasColumnName("limit_value_snapshot"); + + b.Property("MetricCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("metric_code"); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("period_end"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone") + .HasColumnName("period_start"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UsedValue") + .HasColumnType("bigint") + .HasColumnName("used_value"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + + b.Property("WarningIssued") + .HasColumnType("boolean") + .HasColumnName("warning_issued"); + + b.HasKey("Id") + .HasName("pk_tenant_feature_usage"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_feature_usage_tenant_id_id"); + + b.HasIndex("MetricCode") + .HasDatabaseName("ix_tenant_feature_usage_metric_code"); + + b.HasIndex("TenantId", "MetricCode", "PeriodStart", "PeriodEnd") + .IsUnique() + .HasDatabaseName("ix_tenant_feature_usage_tenant_id_metric_code_period_start_per~"); + + b.ToTable("tenant_feature_usage", null, t => + { + t.HasCheckConstraint("ck_tenant_feature_usage_period", "period_end > period_start"); + + t.HasCheckConstraint("ck_tenant_feature_usage_values", "used_value >= 0 and limit_value_snapshot >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantOwnerActivationGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("consumed_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("DomainId") + .HasColumnType("uuid") + .HasColumnName("domain_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("RevocationReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("revocation_reason"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.Property("RevokedBy") + .HasColumnType("uuid") + .HasColumnName("revoked_by"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token_hash"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tenant_owner_activation_grants"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_owner_activation_grants_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_tenant_owner_activation_grants_created_by"); + + b.HasIndex("RevokedBy") + .HasDatabaseName("ix_tenant_owner_activation_grants_revoked_by"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ix_tenant_owner_activation_grants_token_hash") + .HasAnnotation("Tiku:GlobalUnique", true); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tenant_owner_activation_grants_user_id"); + + b.HasIndex("TenantId", "DomainId") + .HasDatabaseName("ix_tenant_owner_activation_grants_tenant_id_domain_id"); + + b.HasIndex("TenantId", "UserId") + .IsUnique() + .HasDatabaseName("ix_tenant_owner_activation_grants_tenant_id_user_id") + .HasFilter("consumed_at is null and revoked_at is null"); + + b.HasIndex("TenantId", "UserId", "ConsumedAt", "ExpiresAt") + .HasDatabaseName("ix_tenant_owner_activation_grants_tenant_id_user_id_consumed_a~"); + + b.ToTable("tenant_owner_activation_grants", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantSaasSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BaseOfferingVersionId") + .HasColumnType("uuid") + .HasColumnName("base_offering_version_id"); + + b.Property("CancelAtPeriodEnd") + .HasColumnType("boolean") + .HasColumnName("cancel_at_period_end"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("cancelled_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CurrentPeriodEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("current_period_end"); + + b.Property("CurrentPeriodStart") + .HasColumnType("timestamp with time zone") + .HasColumnName("current_period_start"); + + b.Property("LifecycleVersion") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("lifecycle_version"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ScheduledBaseOfferingVersionId") + .HasColumnType("uuid") + .HasColumnName("scheduled_base_offering_version_id"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_saas_subscriptions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_saas_subscriptions_tenant_id_id"); + + b.HasIndex("BaseOfferingVersionId") + .HasDatabaseName("ix_tenant_saas_subscriptions_base_offering_version_id"); + + b.HasIndex("ScheduledBaseOfferingVersionId") + .HasDatabaseName("ix_tenant_saas_subscriptions_scheduled_base_offering_version_id"); + + b.HasIndex("TenantId") + .IsUnique() + .HasDatabaseName("ix_tenant_saas_subscriptions_tenant_id") + .HasFilter("status in ('trial', 'active', 'past_due', 'suspended')"); + + b.HasIndex("TenantId", "Status", "CurrentPeriodEnd") + .HasDatabaseName("ix_tenant_saas_subscriptions_tenant_id_status_current_period_e~"); + + b.ToTable("tenant_saas_subscriptions", null, t => + { + t.HasCheckConstraint("ck_tenant_saas_subscriptions_period", "current_period_end > current_period_start and current_period_start >= starts_at"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantSaasSubscriptionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ItemType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("item_type"); + + b.Property("OfferingVersionId") + .HasColumnType("uuid") + .HasColumnName("offering_version_id"); + + b.Property("SourceOrderItemId") + .HasColumnType("uuid") + .HasColumnName("source_order_item_id"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("SubscriptionId") + .HasColumnType("uuid") + .HasColumnName("subscription_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_saas_subscription_items"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_saas_subscription_items_tenant_id_id"); + + b.HasIndex("OfferingVersionId") + .HasDatabaseName("ix_tenant_saas_subscription_items_offering_version_id"); + + b.HasIndex("TenantId", "SourceOrderItemId") + .HasDatabaseName("ix_tenant_saas_subscription_items_tenant_id_source_order_item_~"); + + b.HasIndex("TenantId", "SubscriptionId", "ItemType") + .IsUnique() + .HasDatabaseName("ux_tenant_saas_subscription_items_current_base") + .HasFilter("item_type = 'base_plan' and status = 'active'"); + + b.HasIndex("TenantId", "SubscriptionId", "Status") + .IsUnique() + .HasDatabaseName("ux_tenant_saas_subscription_items_scheduled_base") + .HasFilter("item_type = 'base_plan' and status = 'scheduled'"); + + b.HasIndex("TenantId", "SubscriptionId", "OfferingVersionId", "Status") + .HasDatabaseName("ix_tenant_saas_subscription_items_tenant_id_subscription_id_of~"); + + b.ToTable("tenant_saas_subscription_items", null, t => + { + t.HasCheckConstraint("ck_tenant_saas_subscription_items_period", "ends_at > starts_at"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("ContentNodeId") + .HasColumnType("uuid") + .HasColumnName("content_node_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("EntryId") + .HasColumnType("uuid") + .HasColumnName("entry_id"); + + b.Property("ExamMarkers") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("exam_markers") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("HasVideoExplanation") + .HasColumnType("boolean") + .HasColumnName("has_video_explanation"); + + b.Property("LegacyCategoryId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_category_id"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyNodeId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_node_id"); + + b.Property("LegacySubjectId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_subject_id"); + + b.Property("MediaUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("media_url"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id"); + + b.Property("PrimaryCollectionId") + .HasColumnType("uuid") + .HasColumnName("primary_collection_id"); + + b.Property("QuestionBankId") + .HasColumnType("uuid") + .HasColumnName("question_bank_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("SubjectId") + .HasColumnType("uuid") + .HasColumnName("subject_id"); + + b.Property("Tags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("tags") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("type"); + + b.Property("TypeLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_questions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_questions_tenant_id_id"); + + b.HasIndex("TenantId", "CategoryId") + .HasDatabaseName("ix_questions_tenant_id_category_id"); + + b.HasIndex("TenantId", "ContentNodeId") + .HasDatabaseName("ix_questions_tenant_id_content_node_id"); + + b.HasIndex("TenantId", "EntryId") + .HasDatabaseName("ix_questions_tenant_id_entry_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_questions_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "NodeId") + .HasDatabaseName("ix_questions_tenant_id_node_id"); + + b.HasIndex("TenantId", "PrimaryCollectionId") + .HasDatabaseName("ix_questions_tenant_id_primary_collection_id"); + + b.HasIndex("TenantId", "QuestionBankId") + .HasDatabaseName("ix_questions_tenant_id_question_bank_id"); + + b.HasIndex("TenantId", "SubjectId") + .HasDatabaseName("ix_questions_tenant_id_subject_id"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_questions_tenant_id_id_current_version_id"); + + b.ToTable("questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionBank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentEntryId") + .HasColumnType("uuid") + .HasColumnName("content_entry_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_question_banks"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_banks_tenant_id_id"); + + b.HasIndex("TenantId", "ContentEntryId") + .HasDatabaseName("ix_question_banks_tenant_id_content_entry_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_question_banks_tenant_id_region_id"); + + b.ToTable("question_banks", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerText") + .HasColumnType("text") + .HasColumnName("answer_text"); + + b.Property("CodeLang") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code_lang"); + + b.Property("CodeTemplate") + .HasColumnType("text") + .HasColumnName("code_template"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CorrectOptionIndex") + .HasColumnType("integer") + .HasColumnName("correct_option_index"); + + b.Property("CorrectOptionIndices") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("correct_option_indices") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("Explanation") + .HasColumnType("text") + .HasColumnName("explanation"); + + b.Property("Options") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("options") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("SourceHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_hash"); + + b.Property("SubQuestions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sub_questions") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_question_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "QuestionId", "Id") + .HasName("ak_question_versions_tenant_id_question_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_question_versions_created_by"); + + b.HasIndex("TenantId", "QuestionId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_question_versions_tenant_id_question_id_version_no"); + + b.ToTable("question_versions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.AuthChallenge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("consumed_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("IpAddress") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("ip_address"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("purpose"); + + b.Property("Realm") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("realm"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("security_stamp"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token_hash"); + + b.Property("UserAgent") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("user_agent"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_auth_challenges"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_auth_challenges_tenant_id"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ix_auth_challenges_token_hash"); + + b.HasIndex("UserId", "Purpose", "ExpiresAt") + .HasDatabaseName("ix_auth_challenges_user_id_purpose_expires_at"); + + b.ToTable("auth_challenges", null, t => + { + t.HasCheckConstraint("ck_auth_challenges_realm_tenant", "(realm = 'tenant' and tenant_id is not null) or (realm = 'platform' and tenant_id is null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.AuthLoginEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FailureCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("failure_code"); + + b.Property("Identifier") + .HasMaxLength(320) + .HasColumnType("character varying(320)") + .HasColumnName("identifier"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ip_address"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("Result") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("result"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserAgent") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("user_agent"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_auth_login_events"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_auth_login_events_user_id"); + + b.HasIndex("TenantId", "UserId", "CreatedAt") + .HasDatabaseName("ix_auth_login_events_tenant_id_user_id_created_at"); + + b.ToTable("auth_login_events", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.AuthSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ip_address"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ParentSessionId") + .HasColumnType("uuid") + .HasColumnName("parent_session_id"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("Realm") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("realm"); + + b.Property("ReplacedBySessionId") + .HasColumnType("uuid") + .HasColumnName("replaced_by_session_id"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.Property("RevokedReason") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("revoked_reason"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("security_stamp"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TokenFamilyId") + .HasColumnType("uuid") + .HasColumnName("token_family_id"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("token_hash"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserAgent") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("user_agent"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_auth_sessions"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_auth_sessions_tenant_id"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ix_auth_sessions_token_hash") + .HasAnnotation("Tiku:GlobalUnique", true); + + b.HasIndex("UserId") + .HasDatabaseName("ix_auth_sessions_user_id"); + + b.HasIndex("TokenFamilyId", "RevokedAt") + .HasDatabaseName("ix_auth_sessions_token_family_id_revoked_at"); + + b.HasIndex("Realm", "TenantId", "UserId", "ExpiresAt") + .HasDatabaseName("ix_auth_sessions_realm_tenant_id_user_id_expires_at") + .HasFilter("revoked_at is null"); + + b.ToTable("auth_sessions", null, t => + { + t.HasCheckConstraint("ck_auth_sessions_realm_tenant", "(realm = 'tenant' and tenant_id is not null) or (realm = 'platform' and tenant_id is null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.SmsChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConfigPublic") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("config_public") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("MonthlyQuota") + .HasColumnType("integer") + .HasColumnName("monthly_quota"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)") + .HasColumnName("provider"); + + b.Property("Scene") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("scene"); + + b.Property("SecretRef") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("secret_ref"); + + b.Property("Signature") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("signature"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_sms_channels"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_sms_channels_tenant_id_id"); + + b.HasIndex("TenantId", "Provider", "Scene") + .IsUnique() + .HasDatabaseName("ix_sms_channels_tenant_id_provider_scene"); + + b.HasIndex("TenantId", "Status", "Priority") + .HasDatabaseName("ix_sms_channels_tenant_id_status_priority"); + + b.ToTable("sms_channels", null, t => + { + t.HasCheckConstraint("ck_sms_channels_quota", "monthly_quota >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.SmsSendLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ChannelId") + .HasColumnType("uuid") + .HasColumnName("channel_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ErrorCode") + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("error_code"); + + b.Property("ErrorMessage") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("error_message"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PhoneMasked") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("phone_masked"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)") + .HasColumnName("provider"); + + b.Property("ProviderMessageId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("provider_message_id"); + + b.Property("Scene") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("scene"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("sent_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TemplateId") + .HasColumnType("uuid") + .HasColumnName("template_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_sms_send_logs"); + + b.HasIndex("TenantId", "CreatedAt") + .HasDatabaseName("ix_sms_send_logs_tenant_id_created_at"); + + b.HasIndex("TenantId", "TemplateId") + .HasDatabaseName("ix_sms_send_logs_tenant_id_template_id"); + + b.HasIndex("TenantId", "ChannelId", "CreatedAt") + .HasDatabaseName("ix_sms_send_logs_tenant_id_channel_id_created_at"); + + b.ToTable("sms_send_logs", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.SmsSendRateLimit", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Dimension") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("dimension"); + + b.Property("ScopeHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("scope_hash"); + + b.Property("BucketStart") + .HasColumnType("timestamp with time zone") + .HasColumnName("bucket_start"); + + b.Property("RequestCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("request_count"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("TenantId", "Dimension", "ScopeHash", "BucketStart") + .HasName("pk_sms_send_rate_limits"); + + b.HasIndex("UpdatedAt") + .HasDatabaseName("ix_sms_send_rate_limits_updated_at"); + + b.ToTable("sms_send_rate_limits", null, t => + { + t.HasCheckConstraint("ck_sms_send_rate_limits_request_count", "request_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.SmsTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("approved_at"); + + b.Property("AuditStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("audit_status"); + + b.Property("ChannelId") + .HasColumnType("uuid") + .HasColumnName("channel_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("code"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("content"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DisabledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("disabled_at"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("ProviderTemplateCode") + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("provider_template_code"); + + b.Property("Remark") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("remark"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("submitted_at"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_sms_templates"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_sms_templates_tenant_id_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_sms_templates_tenant_id_code"); + + b.HasIndex("TenantId", "ChannelId", "Status") + .HasDatabaseName("ix_sms_templates_tenant_id_channel_id_status"); + + b.ToTable("sms_templates", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.SmsVerificationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("CodeHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("code_hash"); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("consumed_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ip_address"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("phone"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("purpose"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserAgent") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("user_agent"); + + b.HasKey("Id") + .HasName("pk_sms_verification_codes"); + + b.HasIndex("TenantId", "Phone", "Purpose") + .IsUnique() + .HasDatabaseName("ix_sms_verification_codes_tenant_id_phone_purpose") + .HasFilter("consumed_at is null and status in ('pending', 'sent')"); + + b.HasIndex("TenantId", "Phone", "Purpose", "CreatedAt") + .HasDatabaseName("ix_sms_verification_codes_tenant_id_phone_purpose_created_at") + .HasFilter("consumed_at is null and status in ('pending', 'sent')"); + + b.HasIndex("TenantId", "Phone", "Purpose", "ExpiresAt") + .HasDatabaseName("ix_sms_verification_codes_tenant_id_phone_purpose_expires_at"); + + b.ToTable("sms_verification_codes", null, t => + { + t.HasCheckConstraint("ck_sms_verification_codes_attempts", "attempts >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BillingStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("billing_status"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegalName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("legal_name"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("mode"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("OwnerUserId") + .HasColumnType("uuid") + .HasColumnName("owner_user_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("slug"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.HasIndex("LegacyId") + .IsUnique() + .HasDatabaseName("ix_tenants_legacy_id"); + + b.HasIndex("Mode") + .IsUnique() + .HasDatabaseName("ix_tenants_mode") + .HasFilter("mode = 'platform_owned'"); + + b.HasIndex("OwnerUserId") + .HasDatabaseName("ix_tenants_owner_user_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_tenants_slug"); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantAuthPolicy", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("AllowExternalStudentSelfRegistration") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("allow_external_student_self_registration"); + + b.PrimitiveCollection("AllowedStudentLoginMethods") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text[]") + .HasColumnName("allowed_student_login_methods") + .HasDefaultValueSql("ARRAY['password']::text[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("TenantId") + .HasName("pk_tenant_auth_policies"); + + b.ToTable("tenant_auth_policies", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantBranding", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("BrandName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("brand_name"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FaviconUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("favicon_url"); + + b.Property("LogoUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("logo_url"); + + b.Property("OrganizationName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("organization_name"); + + b.Property("PublicAssets") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("public_assets") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ServiceAccountName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("service_account_name"); + + b.Property("ServiceWechat") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("service_wechat"); + + b.Property("ShortName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("short_name"); + + b.Property("Slogan") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("slogan"); + + b.Property("Theme") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("theme") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("TenantId") + .HasName("pk_tenant_branding"); + + b.ToTable("tenant_branding", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantClass", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.HasKey("Id") + .HasName("pk_tenant_classes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_classes_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_tenant_classes_created_by"); + + b.HasIndex("UpdatedBy") + .HasDatabaseName("ix_tenant_classes_updated_by"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_tenant_classes_tenant_id_code") + .HasFilter("code is not null and code <> ''"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_tenant_classes_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_tenant_classes_tenant_id_region_id") + .HasFilter("region_id is not null"); + + b.HasIndex("TenantId", "Status", "SortOrder", "CreatedAt") + .HasDatabaseName("ix_tenant_classes_tenant_id_status_sort_order_created_at"); + + b.ToTable("tenant_classes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantClassMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ClassId") + .HasColumnType("uuid") + .HasColumnName("class_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("JoinedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_at") + .HasDefaultValueSql("now()"); + + b.Property("LeftAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("left_at"); + + b.Property("MemberType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("member_type"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tenant_class_members"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_class_members_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tenant_class_members_user_id"); + + b.HasIndex("TenantId", "ClassId", "Status", "MemberType") + .HasDatabaseName("ix_tenant_class_members_tenant_id_class_id_status_member_type"); + + b.HasIndex("TenantId", "ClassId", "UserId", "MemberType") + .IsUnique() + .HasDatabaseName("ix_tenant_class_members_tenant_id_class_id_user_id_member_type"); + + b.HasIndex("TenantId", "UserId", "Status", "MemberType") + .HasDatabaseName("ix_tenant_class_members_tenant_id_user_id_status_member_type"); + + b.ToTable("tenant_class_members", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantDomain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DnsVerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("dns_verified_at"); + + b.Property("DomainType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("domain_type"); + + b.Property("Host") + .IsRequired() + .HasMaxLength(253) + .HasColumnType("citext") + .HasColumnName("host"); + + b.Property("IsPrimary") + .HasColumnType("boolean") + .HasColumnName("is_primary"); + + b.Property("LastCheckedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_checked_at"); + + b.Property("LastFailureReason") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_failure_reason"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TlsReadyAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("tls_ready_at"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("VerificationToken") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("verification_token"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_tenant_domains"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_domains_tenant_id_id"); + + b.HasIndex("Host") + .IsUnique() + .HasDatabaseName("ix_tenant_domains_host") + .HasAnnotation("Tiku:GlobalUnique", true); + + b.HasIndex("TenantId", "IsPrimary") + .IsUnique() + .HasDatabaseName("ix_tenant_domains_tenant_id_is_primary") + .HasFilter("is_primary"); + + b.ToTable("tenant_domains", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantExternalProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Capability") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("capability"); + + b.Property("ConfigPublic") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("config_public") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("SecretRef") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("secret_ref"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_external_providers"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_external_providers_tenant_id_id"); + + b.HasIndex("TenantId", "Capability", "Provider") + .IsUnique() + .HasDatabaseName("ix_tenant_external_providers_tenant_id_capability_provider"); + + b.HasIndex("TenantId", "Capability", "Status", "Priority") + .HasDatabaseName("ix_tenant_external_providers_tenant_id_capability_status_prior~"); + + b.ToTable("tenant_external_providers", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantFrontendConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConfigVersion") + .IsConcurrencyToken() + .HasColumnType("integer") + .HasColumnName("config_version"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DraftBranding") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("draft_branding") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("DraftFeatures") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("draft_features") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("DraftHomeModules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("draft_home_modules") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("DraftNavigation") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("draft_navigation") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("DraftTheme") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("draft_theme") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("PublishedBranding") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("published_branding") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PublishedFeatures") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("published_features") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PublishedHomeModules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("published_home_modules") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("PublishedNavigation") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("published_navigation") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("PublishedTheme") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("published_theme") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SchemaVersion") + .HasColumnType("integer") + .HasColumnName("schema_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_frontend_configs"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_frontend_configs_tenant_id_id"); + + b.HasIndex("TenantId") + .IsUnique() + .HasDatabaseName("ix_tenant_frontend_configs_tenant_id"); + + b.ToTable("tenant_frontend_configs", null, t => + { + t.HasCheckConstraint("ck_tenant_frontend_configs_config_version", "config_version > 0"); + + t.HasCheckConstraint("ck_tenant_frontend_configs_schema_version", "schema_version > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyRole") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("legacy_role"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("role"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tenant_memberships"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_memberships_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tenant_memberships_user_id"); + + b.HasIndex("TenantId", "UserId", "Role") + .IsUnique() + .HasDatabaseName("ix_tenant_memberships_tenant_id_user_id_role"); + + b.ToTable("tenant_memberships", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("encrypted_payload"); + + b.Property("EncryptionKeyId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("encryption_key_id"); + + b.Property("EncryptionNonce") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("encryption_nonce"); + + b.Property("EncryptionTag") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("encryption_tag"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)") + .HasColumnName("purpose"); + + b.Property("RotatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("rotated_at"); + + b.Property("SecretKey") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("secret_key"); + + b.Property("SecretRef") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("secret_ref"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_secrets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_secrets_tenant_id_id"); + + b.HasIndex("TenantId", "SecretRef") + .IsUnique() + .HasDatabaseName("ix_tenant_secrets_tenant_id_secret_ref"); + + b.HasIndex("TenantId", "Purpose", "Provider", "SecretKey") + .IsUnique() + .HasDatabaseName("ix_tenant_secrets_tenant_id_purpose_provider_secret_key"); + + b.HasIndex("TenantId", "Purpose", "Provider", "Status") + .HasDatabaseName("ix_tenant_secrets_tenant_id_purpose_provider_status"); + + b.ToTable("tenant_secrets", null, t => + { + t.HasCheckConstraint("ck_tenant_secrets_encryption_envelope", "octet_length(encrypted_payload) > 0 and octet_length(encryption_nonce) = 12 and octet_length(encryption_tag) = 16 and encryption_key_id <> ''"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantSettings", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("AdminFeatureFlags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("admin_feature_flags") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FeatureFlags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("feature_flags") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PublicConfig") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("public_config") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("TenantId") + .HasName("pk_tenant_settings"); + + b.ToTable("tenant_settings", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantStudentFollowup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssignedToUserId") + .HasColumnType("uuid") + .HasColumnName("assigned_to_user_id"); + + b.Property("ClassId") + .HasColumnType("uuid") + .HasColumnName("class_id"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CompletedBy") + .HasColumnType("uuid") + .HasColumnName("completed_by"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("due_at"); + + b.Property("FollowupType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("followup_type"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("priority"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("StudentUserId") + .HasColumnType("uuid") + .HasColumnName("student_user_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.HasKey("Id") + .HasName("pk_tenant_student_followups"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_student_followups_tenant_id_id"); + + b.HasIndex("AssignedToUserId") + .HasDatabaseName("ix_tenant_student_followups_assigned_to_user_id"); + + b.HasIndex("CompletedBy") + .HasDatabaseName("ix_tenant_student_followups_completed_by"); + + b.HasIndex("StudentUserId") + .HasDatabaseName("ix_tenant_student_followups_student_user_id"); + + b.HasIndex("TenantId", "AssignedToUserId", "Status", "DueAt") + .HasDatabaseName("ix_tenant_student_followups_tenant_id_assigned_to_user_id_stat~") + .HasFilter("assigned_to_user_id is not null"); + + b.HasIndex("TenantId", "ClassId", "Status", "DueAt") + .HasDatabaseName("ix_tenant_student_followups_tenant_id_class_id_status_due_at") + .HasFilter("class_id is not null"); + + b.HasIndex("TenantId", "StudentUserId", "Status", "DueAt", "CreatedAt") + .HasDatabaseName("ix_tenant_student_followups_tenant_id_student_user_id_status_d~"); + + b.ToTable("tenant_student_followups", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantStudentNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("NoteType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("note_type"); + + b.Property("StudentUserId") + .HasColumnType("uuid") + .HasColumnName("student_user_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.Property("Visibility") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("visibility"); + + b.HasKey("Id") + .HasName("pk_tenant_student_notes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_student_notes_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_tenant_student_notes_created_by"); + + b.HasIndex("StudentUserId") + .HasDatabaseName("ix_tenant_student_notes_student_user_id"); + + b.HasIndex("UpdatedBy") + .HasDatabaseName("ix_tenant_student_notes_updated_by"); + + b.HasIndex("TenantId", "CreatedBy", "CreatedAt") + .HasDatabaseName("ix_tenant_student_notes_tenant_id_created_by_created_at") + .HasFilter("created_by is not null"); + + b.HasIndex("TenantId", "StudentUserId", "IsPinned", "CreatedAt") + .HasDatabaseName("ix_tenant_student_notes_tenant_id_student_user_id_is_pinned_cr~"); + + b.ToTable("tenant_student_notes", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_claims_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_logins_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_tokens_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Category", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_categories_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.ModuleNode", null) + .WithMany() + .HasForeignKey("TenantId", "NodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_categories_module_nodes_tenant_id_node_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "SubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_categories_subjects_tenant_id_subject_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Major", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_majors_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_majors_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_majors_schools_tenant_id_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.ModuleNode", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_module_nodes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.RegionModule", null) + .WithMany() + .HasForeignKey("TenantId", "ModuleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_module_nodes_region_modules_tenant_id_module_id"); + + b.HasOne("Tiku.Domain.Catalog.ModuleNode", null) + .WithMany() + .HasForeignKey("TenantId", "ParentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_module_nodes_module_nodes_tenant_id_parent_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_module_nodes_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.QuestionTaxonomyAssignment", b => + { + b.HasOne("Tiku.Domain.Catalog.TaxonomyNode", null) + .WithMany() + .HasForeignKey("TaxonomyOwnerTenantId", "TaxonomyNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_taxonomy_assignments_taxonomy_nodes_taxonomy_owner~"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_taxonomy_assignments_questions_tenant_id_question_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Region", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.RegionModule", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_region_modules_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_region_modules_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.School", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_schools_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.RegionModule", null) + .WithMany() + .HasForeignKey("TenantId", "ModuleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_schools_region_modules_tenant_id_module_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_schools_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.ScorelineField", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_scoreline_fields_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_scoreline_fields_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.ScorelineRecord", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_scoreline_records_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Major", null) + .WithMany() + .HasForeignKey("TenantId", "MajorId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_scoreline_records_majors_tenant_id_major_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_scoreline_records_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_scoreline_records_schools_tenant_id_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Subject", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subjects_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Major", null) + .WithMany() + .HasForeignKey("TenantId", "MajorId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_majors_tenant_id_major_id"); + + b.HasOne("Tiku.Domain.Catalog.RegionModule", null) + .WithMany() + .HasForeignKey("TenantId", "ModuleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_region_modules_tenant_id_module_id"); + + b.HasOne("Tiku.Domain.Catalog.ModuleNode", null) + .WithMany() + .HasForeignKey("TenantId", "NodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_module_nodes_tenant_id_node_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_schools_tenant_id_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.TaxonomyNode", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_taxonomy_nodes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.TaxonomyNode", null) + .WithMany() + .HasForeignKey("ParentOwnerTenantId", "ParentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_taxonomy_nodes_taxonomy_nodes_parent_owner_tenant_id_parent~"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.ActivationCode", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("AgentUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_activation_codes_users_agent_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_activation_codes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UsedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_activation_codes_users_used_by"); + + b.HasOne("Tiku.Domain.Commerce.CodeBatch", null) + .WithMany() + .HasForeignKey("TenantId", "BatchId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_activation_codes_code_batches_tenant_id_batch_id"); + + b.HasOne("Tiku.Domain.Commerce.CouponRedemption", null) + .WithMany() + .HasForeignKey("TenantId", "CouponRedemptionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_activation_codes_coupon_redemptions_tenant_id_coupon_redemp~"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "UsedRegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_activation_codes_regions_tenant_id_used_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CodeBatch", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_code_batches_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceAdjustmentVoucher", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_adjustment_vouchers_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ReviewedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_adjustment_vouchers_users_reviewed_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_adjustment_vouchers_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceReconciliationBatch", null) + .WithMany() + .HasForeignKey("TenantId", "BatchId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_adjustment_vouchers_commerce_reconciliation_batche~"); + + b.HasOne("Tiku.Domain.Commerce.CommerceReconciliationIssue", null) + .WithMany() + .HasForeignKey("TenantId", "IssueId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_adjustment_vouchers_commerce_reconciliation_issues~"); + + b.HasOne("Tiku.Domain.Commerce.CommerceReconciliationItem", null) + .WithMany() + .HasForeignKey("TenantId", "ItemId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_adjustment_vouchers_commerce_reconciliation_items_~"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_adjustment_vouchers_orders_tenant_id_order_id"); + + b.HasOne("Tiku.Domain.Commerce.Payment", null) + .WithMany() + .HasForeignKey("TenantId", "PaymentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_adjustment_vouchers_payments_tenant_id_payment_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceRefundRequest", null) + .WithMany() + .HasForeignKey("TenantId", "RefundRequestId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_adjustment_vouchers_commerce_refund_requests_tenan~"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceAdjustmentVoucherEvent", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ActorUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_adjustment_voucher_events_users_actor_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_adjustment_voucher_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceAdjustmentVoucher", null) + .WithMany() + .HasForeignKey("TenantId", "VoucherId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_adjustment_voucher_events_commerce_adjustment_vouc~"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceReconciliationBatch", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_reconciliation_batches_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_reconciliation_batches_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceReconciliationIssue", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("AssignedTo") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_reconciliation_issues_users_assigned_to"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_reconciliation_issues_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ResolvedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_reconciliation_issues_users_resolved_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_reconciliation_issues_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceReconciliationBatch", null) + .WithMany() + .HasForeignKey("TenantId", "BatchId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_issues_commerce_reconciliation_batc~"); + + b.HasOne("Tiku.Domain.Commerce.CommerceReconciliationItem", null) + .WithMany() + .HasForeignKey("TenantId", "ItemId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_issues_commerce_reconciliation_item~"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_issues_orders_tenant_id_order_id"); + + b.HasOne("Tiku.Domain.Commerce.Payment", null) + .WithMany() + .HasForeignKey("TenantId", "PaymentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_issues_payments_tenant_id_payment_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceRefundRequest", null) + .WithMany() + .HasForeignKey("TenantId", "RefundRequestId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_issues_commerce_refund_requests_ten~"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceReconciliationIssueEvent", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ActorUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_reconciliation_issue_events_users_actor_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_reconciliation_issue_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceReconciliationIssue", null) + .WithMany() + .HasForeignKey("TenantId", "IssueId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_reconciliation_issue_events_commerce_reconciliatio~"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceReconciliationItem", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_reconciliation_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceReconciliationBatch", null) + .WithMany() + .HasForeignKey("TenantId", "BatchId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_reconciliation_items_commerce_reconciliation_batch~"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_items_orders_tenant_id_order_id"); + + b.HasOne("Tiku.Domain.Commerce.Payment", null) + .WithMany() + .HasForeignKey("TenantId", "PaymentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_items_payments_tenant_id_payment_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceRefundRequest", null) + .WithMany() + .HasForeignKey("TenantId", "RefundRequestId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_items_commerce_refund_requests_tena~"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceRefundEvent", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ActorUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_refund_events_users_actor_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_refund_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceRefundRequest", null) + .WithMany() + .HasForeignKey("TenantId", "RefundRequestId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_refund_events_commerce_refund_requests_tenant_id_r~"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceRefundRequest", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ProcessedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_refund_requests_users_processed_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RequestedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_refund_requests_users_requested_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ReviewedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_refund_requests_users_reviewed_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_refund_requests_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_refund_requests_orders_tenant_id_order_id"); + + b.HasOne("Tiku.Domain.Commerce.Payment", null) + .WithMany() + .HasForeignKey("TenantId", "PaymentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_refund_requests_payments_tenant_id_payment_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Coupon", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_coupons_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.SvipPlan", null) + .WithMany() + .HasForeignKey("TenantId", "PlanId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_coupons_svip_plans_tenant_id_plan_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CouponRedemption", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_coupon_redemptions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_coupon_redemptions_users_user_id"); + + b.HasOne("Tiku.Domain.Commerce.Coupon", null) + .WithMany() + .HasForeignKey("TenantId", "CouponId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_coupon_redemptions_coupons_tenant_id_coupon_id"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_coupon_redemptions_orders_tenant_id_order_id"); + + b.HasOne("Tiku.Domain.Commerce.SvipPlan", null) + .WithMany() + .HasForeignKey("TenantId", "PlanId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_coupon_redemptions_svip_plans_tenant_id_plan_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_coupon_redemptions_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Entitlement", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RevokedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_entitlements_users_revoked_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_entitlements_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_entitlements_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Order", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_orders_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_orders_users_user_id"); + + b.HasOne("Tiku.Domain.Commerce.SvipPlan", null) + .WithMany() + .HasForeignKey("TenantId", "PlanId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_orders_svip_plans_tenant_id_plan_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_orders_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.OrderItem", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_order_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_order_items_orders_tenant_id_order_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Payment", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_payments_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_payments_orders_tenant_id_order_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PaymentEvent", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_payment_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.Payment", null) + .WithMany() + .HasForeignKey("TenantId", "PaymentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_payment_events_payments_tenant_id_payment_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PointActivityClaim", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_point_activity_claims_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_point_activity_claims_users_user_id"); + + b.HasOne("Tiku.Domain.Commerce.PointActivityTask", null) + .WithMany() + .HasForeignKey("TenantId", "TaskId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_point_activity_claims_point_activity_tasks_tenant_id_task_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PointActivityTask", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_point_activity_tasks_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PointExchangeItem", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_point_exchange_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_point_exchange_items_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PointExchangeOrder", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_point_exchange_orders_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_point_exchange_orders_users_user_id"); + + b.HasOne("Tiku.Domain.Commerce.PointExchangeItem", null) + .WithMany() + .HasForeignKey("TenantId", "ItemId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_point_exchange_orders_point_exchange_items_tenant_id_item_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Product", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_products_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_products_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.SvipPlan", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_svip_plans_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_svip_plans_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AiRecommendationReport", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_ai_recommendation_reports_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_ai_recommendation_reports_users_user_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_ai_recommendation_reports_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AppAsset", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_app_assets_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentAsset", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_content_assets_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_assets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_content_assets_users_updated_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("VerifiedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_content_assets_users_verified_by"); + + b.HasOne("Tiku.Domain.Catalog.Category", null) + .WithMany() + .HasForeignKey("TenantId", "CategoryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_assets_categories_tenant_id_category_id"); + + b.HasOne("Tiku.Domain.Content.ContentNode", null) + .WithMany() + .HasForeignKey("TenantId", "ContentNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_assets_content_nodes_tenant_id_content_node_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_assets_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "SubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_assets_subjects_tenant_id_subject_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentAssetAccessEvent", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_asset_access_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_content_asset_access_events_users_user_id"); + + b.HasOne("Tiku.Domain.Content.ContentAsset", null) + .WithMany() + .HasForeignKey("TenantId", "AssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_asset_access_events_content_assets_tenant_id_asset_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentAssetSecurityScanEvent", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_asset_security_scan_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentAsset", null) + .WithMany() + .HasForeignKey("TenantId", "AssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_asset_security_scan_events_content_assets_tenant_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentEntry", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_content_entries_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_entries_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_content_entries_users_updated_by"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_entries_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentImportIssue", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_import_issues_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentImportItem", null) + .WithMany() + .HasForeignKey("TenantId", "ItemId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_content_import_issues_content_import_items_tenant_id_item_id"); + + b.HasOne("Tiku.Domain.Content.ContentImportJob", null) + .WithMany() + .HasForeignKey("TenantId", "JobId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_import_issues_content_import_jobs_tenant_id_job_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentImportItem", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_import_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentImportJob", null) + .WithMany() + .HasForeignKey("TenantId", "JobId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_import_items_content_import_jobs_tenant_id_job_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentImportJob", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_content_import_jobs_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_import_jobs_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Category", null) + .WithMany() + .HasForeignKey("TenantId", "TargetCategoryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_import_jobs_categories_tenant_id_target_category_id"); + + b.HasOne("Tiku.Domain.Content.ContentNode", null) + .WithMany() + .HasForeignKey("TenantId", "TargetContentNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_import_jobs_content_nodes_tenant_id_target_content_~"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionBank", null) + .WithMany() + .HasForeignKey("TenantId", "TargetQuestionBankId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_import_jobs_question_banks_tenant_id_target_questio~"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "TargetRegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_import_jobs_regions_tenant_id_target_region_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "TargetSubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_import_jobs_subjects_tenant_id_target_subject_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentNode", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_content_nodes_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_nodes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_content_nodes_users_updated_by"); + + b.HasOne("Tiku.Domain.Content.ContentEntry", null) + .WithMany() + .HasForeignKey("TenantId", "EntryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_nodes_content_entries_tenant_id_entry_id"); + + b.HasOne("Tiku.Domain.Content.ContentNode", null) + .WithMany() + .HasForeignKey("TenantId", "ParentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_content_nodes_content_nodes_tenant_id_parent_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_nodes_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.HandbookChapter", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_handbook_chapters_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentNode", null) + .WithMany() + .HasForeignKey("TenantId", "ContentNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_handbook_chapters_content_nodes_tenant_id_content_node_id"); + + b.HasOne("Tiku.Domain.Content.ContentEntry", null) + .WithMany() + .HasForeignKey("TenantId", "EntryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_handbook_chapters_content_entries_tenant_id_entry_id"); + + b.HasOne("Tiku.Domain.Content.HandbookSubject", null) + .WithMany() + .HasForeignKey("TenantId", "SubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_handbook_chapters_handbook_subjects_tenant_id_subject_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.HandbookEntry", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_handbook_entries_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.HandbookChapter", null) + .WithMany() + .HasForeignKey("TenantId", "ChapterId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_handbook_entries_handbook_chapters_tenant_id_chapter_id"); + + b.HasOne("Tiku.Domain.Content.ContentNode", null) + .WithMany() + .HasForeignKey("TenantId", "ContentNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_handbook_entries_content_nodes_tenant_id_content_node_id"); + + b.HasOne("Tiku.Domain.Content.ContentEntry", null) + .WithMany() + .HasForeignKey("TenantId", "EntryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_handbook_entries_content_entries_tenant_id_entry_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.HandbookSubject", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_handbook_subjects_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentNode", null) + .WithMany() + .HasForeignKey("TenantId", "ContentNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_handbook_subjects_content_nodes_tenant_id_content_node_id"); + + b.HasOne("Tiku.Domain.Content.ContentEntry", null) + .WithMany() + .HasForeignKey("TenantId", "EntryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_handbook_subjects_content_entries_tenant_id_entry_id"); + + b.HasOne("Tiku.Domain.Catalog.Major", null) + .WithMany() + .HasForeignKey("TenantId", "MajorId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_handbook_subjects_majors_tenant_id_major_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_handbook_subjects_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_handbook_subjects_schools_tenant_id_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ImageAsset", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_images_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PracticeBlueprint", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_practice_blueprints_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_blueprints_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_practice_blueprints_users_updated_by"); + + b.HasOne("Tiku.Domain.Content.QuestionCollection", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_practice_blueprints_question_collections_tenant_id_collecti~"); + + b.HasOne("Tiku.Domain.Content.ContentEntry", null) + .WithMany() + .HasForeignKey("TenantId", "EntryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_practice_blueprints_content_entries_tenant_id_entry_id"); + + b.HasOne("Tiku.Domain.Content.ContentNode", null) + .WithMany() + .HasForeignKey("TenantId", "NodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_practice_blueprints_content_nodes_tenant_id_node_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_practice_blueprints_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionCollection", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_question_collections_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_collections_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_question_collections_users_updated_by"); + + b.HasOne("Tiku.Domain.Catalog.Category", null) + .WithMany() + .HasForeignKey("TenantId", "CategoryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_collections_categories_tenant_id_category_id"); + + b.HasOne("Tiku.Domain.Content.ContentEntry", null) + .WithMany() + .HasForeignKey("TenantId", "EntryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_collections_content_entries_tenant_id_entry_id"); + + b.HasOne("Tiku.Domain.Content.ContentNode", null) + .WithMany() + .HasForeignKey("TenantId", "NodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_collections_content_nodes_tenant_id_node_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionBank", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionBankId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_collections_question_banks_tenant_id_question_bank~"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_collections_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "SubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_collections_subjects_tenant_id_subject_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionCollectionItem", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_collection_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("QuestionOwnerTenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_collection_items_questions_question_owner_tenant_i~"); + + b.HasOne("Tiku.Domain.Content.QuestionCollection", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_collection_items_question_collections_tenant_id_co~"); + + b.HasOne("Tiku.Domain.Content.TenantQuestionReference", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionReferenceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_collection_items_tenant_question_references_tenant~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionTypeGroup", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_type_groups_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "SubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_question_type_groups_subjects_tenant_id_subject_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionVideo", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_videos_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_question_videos_questions_tenant_id_question_id"); + + b.HasOne("Tiku.Domain.Content.VideoExplanation", null) + .WithMany() + .HasForeignKey("TenantId", "VideoId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_question_videos_video_explanations_tenant_id_video_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.SubjectShare", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subject_shares_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "SourceSubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subject_shares_subjects_tenant_id_source_subject_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "TargetSubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subject_shares_subjects_tenant_id_target_subject_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TenantQuestionBankPreference", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_question_bank_preferences_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_question_bank_preferences_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_question_bank_preferences_users_updated_by"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionBank", null) + .WithMany() + .HasForeignKey("QuestionBankOwnerTenantId", "QuestionBankId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_question_bank_preferences_question_banks_question_ba~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TenantQuestionReference", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_question_references_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_question_references_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("QuestionOwnerTenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_question_references_questions_question_owner_tenant_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.UserWordFavorite", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_word_favorites_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_word_favorites_users_user_id"); + + b.HasOne("Tiku.Domain.Content.VocabularyWord", null) + .WithMany() + .HasForeignKey("TenantId", "WordId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_word_favorites_vocabulary_words_tenant_id_word_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.UserWordProgress", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_word_progress_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_word_progress_users_user_id"); + + b.HasOne("Tiku.Domain.Content.VocabularyWord", null) + .WithMany() + .HasForeignKey("TenantId", "WordId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_word_progress_vocabulary_words_tenant_id_word_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.VideoExplanation", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_video_explanations_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "SubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_video_explanations_subjects_tenant_id_subject_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.VideoPlaybackProgress", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_video_playback_progress_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_video_playback_progress_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_video_playback_progress_questions_tenant_id_question_id"); + + b.HasOne("Tiku.Domain.Content.VideoExplanation", null) + .WithMany() + .HasForeignKey("TenantId", "VideoId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_video_playback_progress_video_explanations_tenant_id_video_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.VocabularyUnit", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vocabulary_units_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentNode", null) + .WithMany() + .HasForeignKey("TenantId", "ContentNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_vocabulary_units_content_nodes_tenant_id_content_node_id"); + + b.HasOne("Tiku.Domain.Content.ContentEntry", null) + .WithMany() + .HasForeignKey("TenantId", "EntryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_vocabulary_units_content_entries_tenant_id_entry_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_vocabulary_units_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.VocabularyWord", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vocabulary_words_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentNode", null) + .WithMany() + .HasForeignKey("TenantId", "ContentNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_vocabulary_words_content_nodes_tenant_id_content_node_id"); + + b.HasOne("Tiku.Domain.Content.ContentEntry", null) + .WithMany() + .HasForeignKey("TenantId", "EntryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_vocabulary_words_content_entries_tenant_id_entry_id"); + + b.HasOne("Tiku.Domain.Content.VocabularyUnit", null) + .WithMany() + .HasForeignKey("TenantId", "UnitId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_vocabulary_words_vocabulary_units_tenant_id_unit_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CommissionSettlement", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("GeneratedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commission_settlements_users_generated_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("PaidBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commission_settlements_users_paid_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ReferrerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commission_settlements_users_referrer_user_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ReviewedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commission_settlements_users_reviewed_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commission_settlements_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CommissionSettlementExportEvent", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ExportedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commission_settlement_export_events_users_exported_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commission_settlement_export_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Growth.CommissionSettlement", null) + .WithMany() + .HasForeignKey("TenantId", "SettlementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commission_settlement_export_events_commission_settlements_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CommissionSettlementItem", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ReferrerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commission_settlement_items_users_referrer_user_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("StudentUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commission_settlement_items_users_student_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commission_settlement_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Growth.CommissionSettlement", null) + .WithMany() + .HasForeignKey("TenantId", "SettlementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commission_settlement_items_commission_settlements_tenant_i~"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CommissionSettlementProof", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ReviewedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commission_settlement_proofs_users_reviewed_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("SubmittedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commission_settlement_proofs_users_submitted_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commission_settlement_proofs_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentAsset", null) + .WithMany() + .HasForeignKey("TenantId", "AssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commission_settlement_proofs_content_assets_tenant_id_asset~"); + + b.HasOne("Tiku.Domain.Growth.CommissionSettlement", null) + .WithMany() + .HasForeignKey("TenantId", "SettlementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commission_settlement_proofs_commission_settlements_tenant_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CrmConfig", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_crm_config_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CrmWebhookLog", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_crm_webhook_log_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.CrmWebhookQueueItem", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_crm_webhook_queue_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.ReferralCode", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_referral_codes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_referral_codes_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.ReferralLead", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("AssignedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_referral_leads_users_assigned_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("AssignedToUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_referral_leads_users_assigned_to_user_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ReferrerUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_referral_leads_users_referrer_user_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("StudentUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_referral_leads_users_student_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_referral_leads_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Growth.ReferralTrack", null) + .WithMany() + .HasForeignKey("TenantId", "FirstTrackId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_referral_leads_referral_tracks_tenant_id_first_track_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.ReferralQrcode", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_referral_qrcodes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_referral_qrcodes_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.ReferralTeamEdge", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("LeaderUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_referral_team_edges_users_leader_user_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("MemberUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_referral_team_edges_users_member_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_referral_team_edges_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.ReferralTrack", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ReferrerUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_referral_tracks_users_referrer_user_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("TargetUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_referral_tracks_users_target_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_referral_tracks_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Growth.ReferralLead", null) + .WithMany() + .HasForeignKey("TenantId", "LeadId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_referral_tracks_referral_leads_tenant_id_lead_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Growth.TenantCommissionSetting", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_commission_settings_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_commission_settings_users_updated_by"); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.StudentProfile", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_profiles_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_profiles_users_user_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_student_profiles_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.Major", null) + .WithMany() + .HasForeignKey("TenantId", "SelectedMajorId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_student_profiles_majors_tenant_id_selected_major_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SelectedSchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_student_profiles_schools_tenant_id_selected_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.UserIdentity", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_identities_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Import.PocketBaseImportIssue", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_pb_import_issues_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Import.PocketBaseImportRun", null) + .WithMany() + .HasForeignKey("TenantId", "RunId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_pb_import_issues_pb_import_runs_tenant_id_run_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Import.PocketBaseImportRun", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_pb_import_runs_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Import.PocketBaseRawRecord", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_pb_raw_records_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Import.PocketBaseImportRun", null) + .WithMany() + .HasForeignKey("TenantId", "RunId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_pb_raw_records_pb_import_runs_tenant_id_run_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.AnswerRecord", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_answer_records_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_answer_records_users_user_id"); + + b.HasOne("Tiku.Domain.Learning.PracticeSessionQuestion", null) + .WithMany() + .HasForeignKey("TenantId", "PracticeSessionId", "SessionQuestionId") + .HasPrincipalKey("TenantId", "PracticeSessionId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_answer_records_practice_session_questions_tenant_id_practic~"); + + b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + .WithMany() + .HasForeignKey("TenantId", "UserId", "PracticeSessionId") + .HasPrincipalKey("TenantId", "UserId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_answer_records_practice_sessions_tenant_id_user_id_practice~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.DashboardDailyStat", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_dashboard_daily_stats_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_dashboard_daily_stats_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ExamDate", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_dates_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_exam_dates_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_exam_dates_schools_tenant_id_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.FavoriteQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_favorite_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_favorite_questions_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("QuestionOwnerTenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_favorite_questions_questions_question_owner_tenant_id_quest~"); + + b.HasOne("Tiku.Domain.Content.TenantQuestionReference", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionReferenceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_favorite_questions_tenant_question_references_tenant_id_que~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningOperationIdempotency", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_operation_idempotencies_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_operation_idempotencies_users_user_id"); + + b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + .WithMany() + .HasForeignKey("TenantId", "UserId", "PracticeSessionId") + .HasPrincipalKey("TenantId", "UserId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_operation_idempotencies_practice_sessions_tenant_i~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeAccessEvent", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_access_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_practice_access_events_users_user_id"); + + b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + .WithMany() + .HasForeignKey("TenantId", "PracticeSessionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_practice_access_events_practice_sessions_tenant_id_practice~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeDailyUsage", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_daily_usage_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_daily_usage_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSession", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_sessions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_sessions_users_user_id"); + + b.HasOne("Tiku.Domain.Content.PracticeBlueprint", null) + .WithMany() + .HasForeignKey("TenantId", "BlueprintId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_practice_sessions_practice_blueprints_tenant_id_blueprint_id"); + + b.HasOne("Tiku.Domain.Content.QuestionCollection", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_practice_sessions_question_collections_tenant_id_collection~"); + + b.HasOne("Tiku.Domain.Content.ContentNode", null) + .WithMany() + .HasForeignKey("TenantId", "ContentNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_practice_sessions_content_nodes_tenant_id_content_node_id"); + + b.HasOne("Tiku.Domain.Content.ContentEntry", null) + .WithMany() + .HasForeignKey("TenantId", "EntryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_practice_sessions_content_entries_tenant_id_entry_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSessionQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + .WithMany() + .HasForeignKey("TenantId", "PracticeSessionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_questions_practice_sessions_tenant_id_prac~"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionVersion", null) + .WithMany() + .HasForeignKey("QuestionOwnerTenantId", "QuestionId", "QuestionVersionId") + .HasPrincipalKey("TenantId", "QuestionId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_practice_session_questions_question_versions_question_owner~"); + + b.HasOne("Tiku.Domain.Content.TenantQuestionReference", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionOwnerTenantId", "QuestionId") + .HasPrincipalKey("TenantId", "QuestionOwnerTenantId", "QuestionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_practice_session_questions_tenant_question_references_tenan~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSessionReport", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_reports_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_reports_users_user_id"); + + b.HasOne("Tiku.Domain.Content.PracticeBlueprint", null) + .WithMany() + .HasForeignKey("TenantId", "BlueprintId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_practice_session_reports_practice_blueprints_tenant_id_blue~"); + + b.HasOne("Tiku.Domain.Content.QuestionCollection", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_practice_session_reports_question_collections_tenant_id_col~"); + + b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + .WithMany() + .HasForeignKey("TenantId", "PracticeSessionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_reports_practice_sessions_tenant_id_practi~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSessionReportSection", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_report_sections_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + .WithMany() + .HasForeignKey("TenantId", "PracticeSessionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_report_sections_practice_sessions_tenant_i~"); + + b.HasOne("Tiku.Domain.Learning.PracticeSessionReport", null) + .WithMany() + .HasForeignKey("TenantId", "ReportId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_report_sections_practice_session_reports_t~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.RecentPractice", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_recent_practices_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_recent_practices_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.Report", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("HandledBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_reports_users_handled_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_reports_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_reports_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_reports_questions_tenant_id_question_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ReportStatusEvent", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ActorUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_report_status_events_users_actor_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_report_status_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.Report", null) + .WithMany() + .HasForeignKey("TenantId", "ReportId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_report_status_events_reports_tenant_id_report_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.RevenueDailyStat", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_revenue_daily_stats_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_revenue_daily_stats_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.UserScoreEvent", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_score_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_score_events_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.WrongQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("QuestionOwnerTenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_wrong_questions_questions_question_owner_tenant_id_question~"); + + b.HasOne("Tiku.Domain.Content.TenantQuestionReference", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionReferenceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_tenant_question_references_tenant_id_questi~"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.Announcement", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_announcements_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.AuditLog", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ActorUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_audit_logs_users_actor_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_audit_logs_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.AuthorizationScopeVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_authorization_scope_versions_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.BackendMenu", b => + { + b.HasOne("Tiku.Domain.Operations.BackendPermission", null) + .WithMany() + .HasForeignKey("PermissionCode") + .HasPrincipalKey("Code") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_backend_menus_backend_permissions_permission_code"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.BackendPermission", b => + { + b.HasOne("Tiku.Domain.Platform.PermissionModule", null) + .WithMany() + .HasForeignKey("PermissionModuleCode") + .HasPrincipalKey("Code") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_backend_permissions_permission_modules_permission_module_co~"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.BackgroundJob", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_background_jobs_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentAsset", null) + .WithMany() + .HasForeignKey("TenantId", "OutputAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_background_jobs_content_assets_tenant_id_output_asset_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.Badge", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_badges_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.Banner", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_banners_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_banners_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.Faq", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_faqs_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_faqs_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRoleMenu", b => + { + b.HasOne("Tiku.Domain.Operations.BackendMenu", null) + .WithMany() + .HasForeignKey("MenuCode") + .HasPrincipalKey("Code") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_backend_role_menus_backend_menus_menu_code"); + + b.HasOne("Tiku.Domain.Operations.PlatformBackendRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_backend_role_menus_platform_backend_roles_role_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRolePermission", b => + { + b.HasOne("Tiku.Domain.Operations.BackendPermission", null) + .WithMany() + .HasForeignKey("PermissionCode") + .HasPrincipalKey("Code") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_backend_role_permissions_backend_permissions_permi~"); + + b.HasOne("Tiku.Domain.Operations.PlatformBackendRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_backend_role_permissions_platform_backend_roles_ro~"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendUserRole", b => + { + b.HasOne("Tiku.Domain.Operations.PlatformBackendRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_backend_user_roles_platform_backend_roles_role_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_backend_user_roles_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRole", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_backend_roles_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRoleMenu", b => + { + b.HasOne("Tiku.Domain.Operations.BackendMenu", null) + .WithMany() + .HasForeignKey("MenuCode") + .HasPrincipalKey("Code") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_backend_role_menus_backend_menus_menu_code"); + + b.HasOne("Tiku.Domain.Operations.TenantBackendRole", null) + .WithMany() + .HasForeignKey("TenantId", "RoleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_backend_role_menus_tenant_backend_roles_tenant_id_ro~"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRolePermission", b => + { + b.HasOne("Tiku.Domain.Operations.BackendPermission", null) + .WithMany() + .HasForeignKey("PermissionCode") + .HasPrincipalKey("Code") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_backend_role_permissions_backend_permissions_permiss~"); + + b.HasOne("Tiku.Domain.Operations.TenantBackendRole", null) + .WithMany() + .HasForeignKey("TenantId", "RoleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_backend_role_permissions_tenant_backend_roles_tenant~"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendUserRole", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_backend_user_roles_users_user_id"); + + b.HasOne("Tiku.Domain.Operations.TenantBackendRole", null) + .WithMany() + .HasForeignKey("TenantId", "RoleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_backend_user_roles_tenant_backend_roles_tenant_id_ro~"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantContentNotification", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_content_notifications_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ReadBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_content_notifications_users_read_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_content_notifications_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionBank", null) + .WithMany() + .HasForeignKey("TenantId", "SourceQuestionBankId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_tenant_content_notifications_question_banks_tenant_id_sourc~"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantLifecycleOperation", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_lifecycle_operations_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentAsset", null) + .WithMany() + .HasForeignKey("TenantId", "ExportAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_lifecycle_operations_content_assets_tenant_id_export~"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.TenantThemeConfig", b => + { + b.HasOne("Tiku.Domain.Operations.TenantThemeTemplate", null) + .WithMany() + .HasForeignKey("ActiveTemplateCode") + .HasPrincipalKey("Code") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_theme_configs_tenant_theme_templates_active_template~"); + + b.HasOne("Tiku.Domain.Operations.TenantThemeTemplate", null) + .WithMany() + .HasForeignKey("DraftTemplateCode") + .HasPrincipalKey("Code") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_theme_configs_tenant_theme_templates_draft_template_~"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("DraftUpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_theme_configs_users_draft_updated_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("PublishedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_theme_configs_users_published_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_theme_configs_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.UserBadge", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("GrantedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_user_badges_users_granted_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_badges_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_user_badges_users_user_id"); + + b.HasOne("Tiku.Domain.Operations.Badge", null) + .WithMany() + .HasForeignKey("TenantId", "BadgeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_user_badges_badges_tenant_id_badge_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Operations.UserNotification", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_user_notifications_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_notifications_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_notifications_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PermissionModule", b => + { + b.HasOne("Tiku.Domain.Platform.SaasFeature", null) + .WithMany() + .HasForeignKey("RequiredFeatureCode") + .HasPrincipalKey("Code") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_permission_modules_saas_features_required_feature_code"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformAuditAlert", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("AcknowledgedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_platform_audit_alerts_users_acknowledged_by"); + + b.HasOne("Tiku.Domain.Operations.AuditLog", null) + .WithMany() + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_audit_alerts_audit_logs_audit_log_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ResolvedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_platform_audit_alerts_users_resolved_by"); + + b.HasOne("Tiku.Domain.Platform.PlatformAuditAlertRule", null) + .WithMany() + .HasForeignKey("RuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_audit_alerts_platform_audit_alert_rules_rule_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_platform_audit_alerts_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformAuditAlertRule", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_platform_audit_alert_rules_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingDunningNotificationEvent", b => + { + b.HasOne("Tiku.Domain.Platform.PlatformBillingDunningNotificationChannel", null) + .WithMany() + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_dunning_events_channel"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_dunning_notification_events_tenants_tenant~"); + + b.HasOne("Tiku.Domain.Platform.PlatformBillingInvoice", null) + .WithMany() + .HasForeignKey("TenantId", "InvoiceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_dunning_events_invoice"); + + b.HasOne("Tiku.Domain.Platform.PlatformBillingInvoiceReminder", null) + .WithMany() + .HasForeignKey("TenantId", "ReminderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_dunning_events_reminder"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingInvoice", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_invoices_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformBillingOrder", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_billing_invoices_platform_billing_orders_tenant_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingInvoiceReminder", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_platform_billing_invoice_reminders_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_invoice_reminders_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformBillingInvoice", null) + .WithMany() + .HasForeignKey("TenantId", "InvoiceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_invoice_reminders_platform_billing_invoice~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingOrder", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_orders_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformBillingQuote", null) + .WithMany() + .HasForeignKey("TenantId", "QuoteId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_billing_orders_platform_billing_quotes_tenant_id_q~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingOrderItem", b => + { + b.HasOne("Tiku.Domain.Platform.SaasOfferingVersion", null) + .WithMany() + .HasForeignKey("OfferingVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_billing_order_items_saas_offering_versions_offerin~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_order_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformBillingOrder", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_order_items_platform_billing_orders_tenant~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingPayment", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_payments_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformBillingOrder", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_payments_platform_billing_orders_tenant_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingPaymentEvent", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_payment_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformBillingPayment", null) + .WithMany() + .HasForeignKey("TenantId", "PaymentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_payment_events_platform_billing_payments_t~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingQuote", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_quotes_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingQuoteItem", b => + { + b.HasOne("Tiku.Domain.Platform.SaasOfferingVersion", null) + .WithMany() + .HasForeignKey("OfferingVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_billing_quote_items_saas_offering_versions_offerin~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_quote_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformBillingQuote", null) + .WithMany() + .HasForeignKey("TenantId", "QuoteId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_quote_items_platform_billing_quotes_tenant~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformBillingRefund", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_billing_refunds_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformBillingOrder", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_billing_refunds_platform_billing_orders_tenant_id_~"); + + b.HasOne("Tiku.Domain.Platform.PlatformBillingPayment", null) + .WithMany() + .HasForeignKey("TenantId", "PaymentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_billing_refunds_platform_billing_payments_tenant_i~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformOperationIdempotency", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ActorUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_operation_idempotencies_users_actor_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformPaymentChannel", b => + { + b.HasOne("Tiku.Domain.Platform.PlatformPaymentApp", null) + .WithMany() + .HasForeignKey("AppId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_payment_channels_platform_payment_apps_app_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.SaasFeatureLimitDefinition", b => + { + b.HasOne("Tiku.Domain.Platform.SaasFeature", null) + .WithMany() + .HasForeignKey("FeatureCode") + .HasPrincipalKey("Code") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_saas_feature_limit_definitions_saas_features_feature_code"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.SaasOfferingVersion", b => + { + b.HasOne("Tiku.Domain.Platform.SaasOffering", null) + .WithMany() + .HasForeignKey("OfferingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_saas_offering_versions_saas_offerings_offering_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.SaasOfferingVersionFeature", b => + { + b.HasOne("Tiku.Domain.Platform.SaasFeature", null) + .WithMany() + .HasForeignKey("FeatureCode") + .HasPrincipalKey("Code") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_saas_offering_version_features_saas_features_feature_code"); + + b.HasOne("Tiku.Domain.Platform.SaasOfferingVersion", null) + .WithMany() + .HasForeignKey("OfferingVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_saas_offering_version_features_saas_offering_versions_offer~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.SaasOfferingVersionLimit", b => + { + b.HasOne("Tiku.Domain.Platform.SaasFeatureLimitDefinition", null) + .WithMany() + .HasForeignKey("MetricCode") + .HasPrincipalKey("MetricCode") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_saas_offering_version_limits_saas_feature_limit_definitions~"); + + b.HasOne("Tiku.Domain.Platform.SaasOfferingVersion", null) + .WithMany() + .HasForeignKey("OfferingVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_saas_offering_version_limits_saas_offering_versions_offerin~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantBillingPolicy", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithOne() + .HasForeignKey("Tiku.Domain.Platform.TenantBillingPolicy", "TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_billing_policies_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantBillingProfile", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithOne() + .HasForeignKey("Tiku.Domain.Platform.TenantBillingProfile", "TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_billing_profiles_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantFeatureOverride", b => + { + b.HasOne("Tiku.Domain.Platform.SaasFeature", null) + .WithMany() + .HasForeignKey("FeatureCode") + .HasPrincipalKey("Code") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_feature_overrides_saas_features_feature_code"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_feature_overrides_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantFeatureUsage", b => + { + b.HasOne("Tiku.Domain.Platform.SaasFeatureLimitDefinition", null) + .WithMany() + .HasForeignKey("MetricCode") + .HasPrincipalKey("MetricCode") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_feature_usage_saas_feature_limit_definitions_metric_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_feature_usage_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantOwnerActivationGrant", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_owner_activation_grants_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RevokedBy") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tenant_owner_activation_grants_users_revoked_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_owner_activation_grants_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_owner_activation_grants_users_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.TenantDomain", null) + .WithMany() + .HasForeignKey("TenantId", "DomainId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tenant_owner_activation_grants_tenant_domains_tenant_id_dom~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantSaasSubscription", b => + { + b.HasOne("Tiku.Domain.Platform.SaasOfferingVersion", null) + .WithMany() + .HasForeignKey("BaseOfferingVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_saas_subscriptions_saas_offering_versions_base_offer~"); + + b.HasOne("Tiku.Domain.Platform.SaasOfferingVersion", null) + .WithMany() + .HasForeignKey("ScheduledBaseOfferingVersionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tenant_saas_subscriptions_saas_offering_versions_scheduled_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_saas_subscriptions_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantSaasSubscriptionItem", b => + { + b.HasOne("Tiku.Domain.Platform.SaasOfferingVersion", null) + .WithMany() + .HasForeignKey("OfferingVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_saas_subscription_items_saas_offering_versions_offer~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_saas_subscription_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformBillingOrderItem", null) + .WithMany() + .HasForeignKey("TenantId", "SourceOrderItemId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tenant_saas_subscription_items_platform_billing_order_items~"); + + b.HasOne("Tiku.Domain.Platform.TenantSaasSubscription", null) + .WithMany() + .HasForeignKey("TenantId", "SubscriptionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_saas_subscription_items_tenant_saas_subscriptions_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.Question", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Category", null) + .WithMany() + .HasForeignKey("TenantId", "CategoryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_categories_tenant_id_category_id"); + + b.HasOne("Tiku.Domain.Content.ContentNode", null) + .WithMany() + .HasForeignKey("TenantId", "ContentNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_content_nodes_tenant_id_content_node_id"); + + b.HasOne("Tiku.Domain.Content.ContentEntry", null) + .WithMany() + .HasForeignKey("TenantId", "EntryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_content_entries_tenant_id_entry_id"); + + b.HasOne("Tiku.Domain.Catalog.ModuleNode", null) + .WithMany() + .HasForeignKey("TenantId", "NodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_module_nodes_tenant_id_node_id"); + + b.HasOne("Tiku.Domain.Content.QuestionCollection", null) + .WithMany() + .HasForeignKey("TenantId", "PrimaryCollectionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_question_collections_tenant_id_primary_collection~"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionBank", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionBankId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_question_banks_tenant_id_question_bank_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "SubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_subjects_tenant_id_subject_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "QuestionId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_question_versions_tenant_id_id_current_version_id"); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionBank", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_banks_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentEntry", null) + .WithMany() + .HasForeignKey("TenantId", "ContentEntryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_banks_content_entries_tenant_id_content_entry_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_banks_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionVersion", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_question_versions_users_created_by"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_versions_questions_tenant_id_question_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.AuthChallenge", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_auth_challenges_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_challenges_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.AuthLoginEvent", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_auth_login_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_auth_login_events_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.AuthSession", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_auth_sessions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_sessions_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.SmsChannel", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_sms_channels_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.SmsSendLog", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_sms_send_logs_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Tenancy.SmsChannel", null) + .WithMany() + .HasForeignKey("TenantId", "ChannelId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_sms_send_logs_sms_channels_tenant_id_channel_id"); + + b.HasOne("Tiku.Domain.Tenancy.SmsTemplate", null) + .WithMany() + .HasForeignKey("TenantId", "TemplateId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_sms_send_logs_sms_templates_tenant_id_template_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.SmsSendRateLimit", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_sms_send_rate_limits_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.SmsTemplate", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_sms_templates_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Tenancy.SmsChannel", null) + .WithMany() + .HasForeignKey("TenantId", "ChannelId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_sms_templates_sms_channels_tenant_id_channel_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.SmsVerificationCode", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_sms_verification_codes_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.Tenant", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenants_users_owner_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantAuthPolicy", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithOne() + .HasForeignKey("Tiku.Domain.Tenancy.TenantAuthPolicy", "TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_auth_policies_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantBranding", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithOne() + .HasForeignKey("Tiku.Domain.Tenancy.TenantBranding", "TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_branding_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantClass", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_classes_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_classes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_classes_users_updated_by"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tenant_classes_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantClassMember", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_class_members_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_class_members_users_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.TenantClass", null) + .WithMany() + .HasForeignKey("TenantId", "ClassId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_class_members_tenant_classes_tenant_id_class_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantDomain", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_domains_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantExternalProvider", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_external_providers_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantFrontendConfig", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_frontend_configs_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantMembership", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_memberships_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_memberships_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantSecret", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_secrets_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantSettings", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithOne() + .HasForeignKey("Tiku.Domain.Tenancy.TenantSettings", "TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_settings_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantStudentFollowup", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("AssignedToUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_student_followups_users_assigned_to_user_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CompletedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_student_followups_users_completed_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("StudentUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_student_followups_users_student_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_student_followups_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Tenancy.TenantClass", null) + .WithMany() + .HasForeignKey("TenantId", "ClassId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_student_followups_tenant_classes_tenant_id_class_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantStudentNote", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_student_notes_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("StudentUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_student_notes_users_student_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_student_notes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_student_notes_users_updated_by"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Tiku.Infrastructure/Persistence/Migrations/20260803013235_TrustedLearningCore.cs b/Tiku.Infrastructure/Persistence/Migrations/20260803013235_TrustedLearningCore.cs new file mode 100644 index 0000000..8984250 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260803013235_TrustedLearningCore.cs @@ -0,0 +1,489 @@ +using System; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Tiku.Infrastructure.Persistence.Migrations +{ + /// + public partial class TrustedLearningCore : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "last_client_sequence", + table: "practice_sessions", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "status", + table: "practice_sessions", + type: "character varying(32)", + maxLength: 32, + nullable: false, + defaultValue: "active"); + + migrationBuilder.AddColumn( + name: "version", + table: "practice_sessions", + type: "bigint", + nullable: false, + defaultValue: 1L); + + migrationBuilder.AddColumn( + name: "is_final", + table: "practice_session_reports", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "pending_review_count", + table: "practice_session_reports", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "scoring_version", + table: "practice_session_reports", + type: "integer", + nullable: false, + defaultValue: 1); + + migrationBuilder.AddColumn( + name: "status", + table: "practice_session_reports", + type: "character varying(32)", + maxLength: 32, + nullable: false, + defaultValue: "final"); + + migrationBuilder.AddColumn( + name: "version", + table: "practice_session_reports", + type: "integer", + nullable: false, + defaultValue: 1); + + migrationBuilder.AddColumn( + name: "answer_text_snapshot", + table: "practice_session_questions", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "content_snapshot", + table: "practice_session_questions", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "correct_option_index_snapshot", + table: "practice_session_questions", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "correct_option_indices_snapshot", + table: "practice_session_questions", + type: "jsonb", + nullable: false, + defaultValueSql: "'[]'::jsonb"); + + migrationBuilder.AddColumn( + name: "difficulty_snapshot", + table: "practice_session_questions", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "explanation_snapshot", + table: "practice_session_questions", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "grading_rules_snapshot", + table: "practice_session_questions", + type: "jsonb", + nullable: false, + defaultValueSql: "'{}'::jsonb"); + + migrationBuilder.AddColumn( + name: "options_snapshot", + table: "practice_session_questions", + type: "jsonb", + nullable: false, + defaultValueSql: "'[]'::jsonb"); + + migrationBuilder.AddColumn( + name: "question_type", + table: "practice_session_questions", + type: "character varying(50)", + maxLength: 50, + nullable: false, + defaultValue: "choice"); + + migrationBuilder.AddColumn( + name: "snapshot_version", + table: "practice_session_questions", + type: "integer", + nullable: false, + defaultValue: 1); + + migrationBuilder.AddColumn( + name: "tags_snapshot", + table: "practice_session_questions", + type: "jsonb", + nullable: false, + defaultValueSql: "'[]'::jsonb"); + + migrationBuilder.AddColumn( + name: "type_label_snapshot", + table: "practice_session_questions", + type: "character varying(100)", + maxLength: 100, + nullable: true); + + migrationBuilder.AddColumn( + name: "awarded_score", + table: "answer_records", + type: "numeric(8,2)", + precision: 8, + scale: 2, + nullable: true); + + migrationBuilder.AddColumn( + name: "client_sequence", + table: "answer_records", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "grading_status", + table: "answer_records", + type: "character varying(32)", + maxLength: 32, + nullable: false, + defaultValue: "pending_review"); + + migrationBuilder.AddColumn( + name: "idempotency_key", + table: "answer_records", + type: "character varying(200)", + maxLength: 200, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "is_current", + table: "answer_records", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "request_hash", + table: "answer_records", + type: "character varying(64)", + maxLength: 64, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "revision", + table: "answer_records", + type: "integer", + nullable: false, + defaultValue: 1); + + migrationBuilder.CreateTable( + name: "learning_operation_idempotencies", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + tenant_id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false), + practice_session_id = table.Column(type: "uuid", nullable: false), + operation_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + idempotency_key = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + request_hash = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + response_snapshot = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + completed_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_learning_operation_idempotencies", x => x.id); + table.UniqueConstraint("ak_learning_operation_idempotencies_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_learning_operation_idempotencies_practice_sessions_tenant_i~", + columns: x => new { x.tenant_id, x.user_id, x.practice_session_id }, + principalTable: "practice_sessions", + principalColumns: new[] { "tenant_id", "user_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_learning_operation_idempotencies_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_learning_operation_idempotencies_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql(""" + UPDATE practice_sessions + SET status = CASE + WHEN finished_at IS NOT NULL THEN 'submitted' + WHEN expires_at IS NOT NULL AND expires_at <= now() THEN 'expired' + ELSE 'active' + END, + version = 1, + last_client_sequence = 0; + + UPDATE practice_session_reports + SET status = 'legacy_unverified', + version = 1, + is_final = false, + pending_review_count = 0, + scoring_version = 0; + + UPDATE practice_session_questions AS session_question + SET question_type = question.type, + type_label_snapshot = question.type_label, + difficulty_snapshot = question.difficulty, + tags_snapshot = question.tags, + content_snapshot = version.content, + options_snapshot = version.options, + correct_option_index_snapshot = version.correct_option_index, + correct_option_indices_snapshot = version.correct_option_indices, + answer_text_snapshot = version.answer_text, + explanation_snapshot = version.explanation, + grading_rules_snapshot = jsonb_build_object( + 'version', 1, + 'legacyBackfill', true), + snapshot_version = 1 + FROM questions AS question + JOIN question_versions AS version + ON version.tenant_id = question.tenant_id + AND version.question_id = question.id + WHERE question.tenant_id = session_question.question_owner_tenant_id + AND question.id = session_question.question_id + AND version.id = session_question.question_version_id; + + WITH ranked AS ( + SELECT id, + row_number() OVER ( + PARTITION BY tenant_id, user_id, practice_session_id + ORDER BY answered_at, created_at, id) AS client_sequence_value, + row_number() OVER ( + PARTITION BY tenant_id, user_id, practice_session_id, session_question_id + ORDER BY answered_at, created_at, id) AS revision_value, + row_number() OVER ( + PARTITION BY tenant_id, user_id, practice_session_id, session_question_id + ORDER BY answered_at DESC, created_at DESC, id DESC) AS current_rank + FROM answer_records + ) + UPDATE answer_records AS answer + SET client_sequence = ranked.client_sequence_value, + revision = ranked.revision_value, + is_current = ranked.current_rank = 1, + grading_status = 'legacy_unverified', + is_correct = NULL, + awarded_score = NULL, + idempotency_key = 'legacy-' || answer.id::text, + request_hash = repeat('0', 64) + FROM ranked + WHERE ranked.id = answer.id; + + UPDATE practice_sessions AS session + SET last_client_sequence = sequence.maximum + FROM ( + SELECT tenant_id, user_id, practice_session_id, max(client_sequence) AS maximum + FROM answer_records + GROUP BY tenant_id, user_id, practice_session_id + ) AS sequence + WHERE sequence.tenant_id = session.tenant_id + AND sequence.user_id = session.user_id + AND sequence.practice_session_id = session.id; + """); + + migrationBuilder.CreateIndex( + name: "ix_practice_sessions_tenant_id_user_id_status_started_at", + table: "practice_sessions", + columns: new[] { "tenant_id", "user_id", "status", "started_at" }); + + migrationBuilder.CreateIndex( + name: "ix_answer_records_tenant_id_user_id_practice_session_id_client~", + table: "answer_records", + columns: new[] { "tenant_id", "user_id", "practice_session_id", "client_sequence" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ux_answer_records_current_session_question", + table: "answer_records", + columns: new[] { "tenant_id", "user_id", "practice_session_id", "session_question_id" }, + unique: true, + filter: "is_current"); + + migrationBuilder.CreateIndex( + name: "ux_answer_records_session_question_revision", + table: "answer_records", + columns: new[] { "tenant_id", "user_id", "practice_session_id", "session_question_id", "revision" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_learning_operation_idempotencies_tenant_id_user_id_practice~", + table: "learning_operation_idempotencies", + columns: new[] { "tenant_id", "user_id", "practice_session_id", "operation_type", "idempotency_key" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_learning_operation_idempotencies_user_id", + table: "learning_operation_idempotencies", + column: "user_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "learning_operation_idempotencies"); + + migrationBuilder.DropIndex( + name: "ix_practice_sessions_tenant_id_user_id_status_started_at", + table: "practice_sessions"); + + migrationBuilder.DropIndex( + name: "ix_answer_records_tenant_id_user_id_practice_session_id_client~", + table: "answer_records"); + + migrationBuilder.DropIndex( + name: "ux_answer_records_current_session_question", + table: "answer_records"); + + migrationBuilder.DropIndex( + name: "ux_answer_records_session_question_revision", + table: "answer_records"); + + migrationBuilder.DropColumn( + name: "last_client_sequence", + table: "practice_sessions"); + + migrationBuilder.DropColumn( + name: "status", + table: "practice_sessions"); + + migrationBuilder.DropColumn( + name: "version", + table: "practice_sessions"); + + migrationBuilder.DropColumn( + name: "is_final", + table: "practice_session_reports"); + + migrationBuilder.DropColumn( + name: "pending_review_count", + table: "practice_session_reports"); + + migrationBuilder.DropColumn( + name: "scoring_version", + table: "practice_session_reports"); + + migrationBuilder.DropColumn( + name: "status", + table: "practice_session_reports"); + + migrationBuilder.DropColumn( + name: "version", + table: "practice_session_reports"); + + migrationBuilder.DropColumn( + name: "answer_text_snapshot", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "content_snapshot", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "correct_option_index_snapshot", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "correct_option_indices_snapshot", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "difficulty_snapshot", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "explanation_snapshot", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "grading_rules_snapshot", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "options_snapshot", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "question_type", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "snapshot_version", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "tags_snapshot", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "type_label_snapshot", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "awarded_score", + table: "answer_records"); + + migrationBuilder.DropColumn( + name: "client_sequence", + table: "answer_records"); + + migrationBuilder.DropColumn( + name: "grading_status", + table: "answer_records"); + + migrationBuilder.DropColumn( + name: "idempotency_key", + table: "answer_records"); + + migrationBuilder.DropColumn( + name: "is_current", + table: "answer_records"); + + migrationBuilder.DropColumn( + name: "request_hash", + table: "answer_records"); + + migrationBuilder.DropColumn( + name: "revision", + table: "answer_records"); + } + } +} diff --git a/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs b/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs index 640ae49..76b2ffa 100644 --- a/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs +++ b/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs @@ -8650,16 +8650,45 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnName("answered_at") .HasDefaultValueSql("now()"); + b.Property("AwardedScore") + .HasPrecision(8, 2) + .HasColumnType("numeric(8,2)") + .HasColumnName("awarded_score"); + + b.Property("ClientSequence") + .HasColumnType("bigint") + .HasColumnName("client_sequence"); + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasColumnName("created_at") .HasDefaultValueSql("now()"); + b.Property("GradingStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("pending_review") + .HasColumnName("grading_status"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + b.Property("IsCorrect") .HasColumnType("boolean") .HasColumnName("is_correct"); + b.Property("IsCurrent") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_current"); + b.Property("LegacyCategoryId") .HasMaxLength(64) .HasColumnType("character varying(64)") @@ -8679,6 +8708,18 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("uuid") .HasColumnName("practice_session_id"); + b.Property("RequestHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("request_hash"); + + b.Property("Revision") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("revision"); + b.Property("SelectedOptions") .ValueGeneratedOnAdd() .HasColumnType("jsonb") @@ -8716,6 +8757,19 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.HasIndex("TenantId", "UserId", "PracticeSessionId") .HasDatabaseName("ix_answer_records_tenant_id_user_id_practice_session_id"); + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "ClientSequence") + .IsUnique() + .HasDatabaseName("ix_answer_records_tenant_id_user_id_practice_session_id_client~"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "SessionQuestionId") + .IsUnique() + .HasDatabaseName("ux_answer_records_current_session_question") + .HasFilter("is_current"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "SessionQuestionId", "Revision") + .IsUnique() + .HasDatabaseName("ux_answer_records_session_question_revision"); + b.ToTable("answer_records", (string)null); }); @@ -8945,6 +8999,76 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.ToTable("favorite_questions", (string)null); }); + modelBuilder.Entity("Tiku.Domain.Learning.LearningOperationIdempotency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("OperationType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("operation_type"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("RequestHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("request_hash"); + + b.Property("ResponseSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("response_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_learning_operation_idempotencies"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_operation_idempotencies_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_learning_operation_idempotencies_user_id"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "OperationType", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_learning_operation_idempotencies_tenant_id_user_id_practice~"); + + b.ToTable("learning_operation_idempotencies", (string)null); + }); + modelBuilder.Entity("Tiku.Domain.Learning.PracticeAccessEvent", b => { b.Property("Id") @@ -9186,6 +9310,10 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("finished_at"); + b.Property("LastClientSequence") + .HasColumnType("bigint") + .HasColumnName("last_client_sequence"); + b.Property("Metadata") .ValueGeneratedOnAdd() .HasColumnType("jsonb") @@ -9208,6 +9336,14 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnName("started_at") .HasDefaultValueSql("now()"); + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("active") + .HasColumnName("status"); + b.Property("TargetId") .HasColumnType("uuid") .HasColumnName("target_id"); @@ -9230,6 +9366,13 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("uuid") .HasColumnName("user_id"); + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + b.HasKey("Id") .HasName("pk_practice_sessions"); @@ -9257,6 +9400,9 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.HasIndex("TenantId", "UserId", "StartedAt") .HasDatabaseName("ix_practice_sessions_tenant_id_user_id_started_at"); + b.HasIndex("TenantId", "UserId", "Status", "StartedAt") + .HasDatabaseName("ix_practice_sessions_tenant_id_user_id_status_started_at"); + b.ToTable("practice_sessions", null, t => { t.HasCheckConstraint("ck_practice_sessions_consumed_free_quota", "consumed_free_quota >= 0"); @@ -9271,6 +9417,44 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnName("id") .HasDefaultValueSql("gen_random_uuid()"); + b.Property("AnswerTextSnapshot") + .HasColumnType("text") + .HasColumnName("answer_text_snapshot"); + + b.Property("ContentSnapshot") + .HasColumnType("text") + .HasColumnName("content_snapshot"); + + b.Property("CorrectOptionIndexSnapshot") + .HasColumnType("integer") + .HasColumnName("correct_option_index_snapshot"); + + b.Property("CorrectOptionIndicesSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("correct_option_indices_snapshot") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("DifficultySnapshot") + .HasColumnType("integer") + .HasColumnName("difficulty_snapshot"); + + b.Property("ExplanationSnapshot") + .HasColumnType("text") + .HasColumnName("explanation_snapshot"); + + b.Property("GradingRulesSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("grading_rules_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("OptionsSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("options_snapshot") + .HasDefaultValueSql("'[]'::jsonb"); + b.Property("Position") .HasColumnType("integer") .HasColumnName("position"); @@ -9291,6 +9475,14 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("uuid") .HasColumnName("question_reference_id"); + b.Property("QuestionType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("choice") + .HasColumnName("question_type"); + b.Property("QuestionVersionId") .HasColumnType("uuid") .HasColumnName("question_version_id"); @@ -9300,10 +9492,27 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("numeric(8,2)") .HasColumnName("score"); + b.Property("SnapshotVersion") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("snapshot_version"); + + b.Property("TagsSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("tags_snapshot") + .HasDefaultValueSql("'[]'::jsonb"); + b.Property("TenantId") .HasColumnType("uuid") .HasColumnName("tenant_id"); + b.Property("TypeLabelSnapshot") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label_snapshot"); + b.HasKey("Id") .HasName("pk_practice_session_questions"); @@ -9365,6 +9574,12 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("integer") .HasColumnName("duration_seconds"); + b.Property("IsFinal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_final"); + b.Property("Metadata") .ValueGeneratedOnAdd() .HasColumnType("jsonb") @@ -9377,6 +9592,10 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("character varying(50)") .HasColumnName("mode"); + b.Property("PendingReviewCount") + .HasColumnType("integer") + .HasColumnName("pending_review_count"); + b.Property("PracticeSessionId") .HasColumnType("uuid") .HasColumnName("practice_session_id"); @@ -9392,6 +9611,12 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("numeric(10,2)") .HasColumnName("score"); + b.Property("ScoringVersion") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("scoring_version"); + b.Property("SectionStats") .ValueGeneratedOnAdd() .HasColumnType("jsonb") @@ -9402,6 +9627,14 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("started_at"); + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("final") + .HasColumnName("status"); + b.Property("SubmittedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -9435,6 +9668,12 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("uuid") .HasColumnName("user_id"); + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("version"); + b.Property("WrongCount") .HasColumnType("integer") .HasColumnName("wrong_count"); @@ -18845,6 +19084,31 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_favorite_questions_tenant_question_references_tenant_id_que~"); }); + modelBuilder.Entity("Tiku.Domain.Learning.LearningOperationIdempotency", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_operation_idempotencies_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_operation_idempotencies_users_user_id"); + + b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + .WithMany() + .HasForeignKey("TenantId", "UserId", "PracticeSessionId") + .HasPrincipalKey("TenantId", "UserId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_operation_idempotencies_practice_sessions_tenant_i~"); + }); + modelBuilder.Entity("Tiku.Domain.Learning.PracticeAccessEvent", b => { b.HasOne("Tiku.Domain.Tenancy.Tenant", null) diff --git a/Tiku.Infrastructure/Persistence/TikuDbContext.cs b/Tiku.Infrastructure/Persistence/TikuDbContext.cs index 84c13af..3d5acd3 100644 --- a/Tiku.Infrastructure/Persistence/TikuDbContext.cs +++ b/Tiku.Infrastructure/Persistence/TikuDbContext.cs @@ -110,6 +110,7 @@ public sealed class TikuDbContext( public DbSet PracticeSessions => Set(); public DbSet PracticeSessionQuestions => Set(); public DbSet AnswerRecords => Set(); + public DbSet LearningOperationIdempotencies => Set(); public DbSet FavoriteQuestions => Set(); public DbSet WrongQuestions => Set(); public DbSet RecentPractices => Set(); diff --git a/Tiku.Infrastructure/PlatformAdmin/PlatformQuestionBankService.cs b/Tiku.Infrastructure/PlatformAdmin/PlatformQuestionBankService.cs index bdd1b48..b75cf50 100644 --- a/Tiku.Infrastructure/PlatformAdmin/PlatformQuestionBankService.cs +++ b/Tiku.Infrastructure/PlatformAdmin/PlatformQuestionBankService.cs @@ -4,6 +4,7 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Tiku.Application.Assets; +using Tiku.Application.Learning; using Tiku.Application.PlatformAdmin; using Tiku.Application.Security; using Tiku.Domain.Common; @@ -620,6 +621,15 @@ internal sealed class PlatformQuestionBankService( var type = string.IsNullOrWhiteSpace(command.Type) ? "choice" : command.Type.Trim(); if (!SupportedQuestionTypes.Contains(type)) throw Error("题型不受支持。", "question_type_invalid"); if (command.Difficulty is < 1 or > 5) throw Error("难度必须在 1 到 5 之间。", "question_difficulty_invalid"); + if (ParseQuestionStatus(command.Status) == QuestionStatus.Published && + !QuestionGrader.HasValidAuthoritativeAnswer( + type, + command.CorrectOptionIndex, + command.CorrectOptionIndices, + command.AnswerText)) + { + throw Error("发布题目必须提供有效的标准答案。", "question_grading_rule_invalid"); + } } private static void ApplyQuestion(Question question, UpsertPlatformQuestionCommand command, Guid entryId, Guid nodeId) diff --git a/Tiku.IntegrationTests/Api/LearningEndpointTests.cs b/Tiku.IntegrationTests/Api/LearningEndpointTests.cs index 95282ee..b1a4913 100644 --- a/Tiku.IntegrationTests/Api/LearningEndpointTests.cs +++ b/Tiku.IntegrationTests/Api/LearningEndpointTests.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Http.Json; using System.Text.Json; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Tiku.Api.Contracts; using Tiku.Domain.Common; @@ -33,7 +34,7 @@ public sealed class LearningEndpointTests await using var factory = new ApiTestFactory(); var seed = await SeedLearningUserAsync(factory); var questionId = Guid.NewGuid(); - var sessionQuestionId = await SeedAnswerableQuestionAsync(factory, seed, questionId); + var answerable = await SeedAnswerableQuestionAsync(factory, seed, questionId); using var client = factory.CreateClient(); await LoginAsync(client, seed); @@ -41,17 +42,28 @@ public sealed class LearningEndpointTests "/api/learning/answers", new SubmitAnswerDto { - SessionQuestionId = sessionQuestionId, - SelectedOptions = ["A"], - SelfJudgedCorrect = false + SessionQuestionId = answerable.SessionQuestionId, + ExpectedSessionVersion = 1, + ClientSequence = 1, + IdempotencyKey = "wrong-answer-1", + SelectedOptionIndices = [1] + }); + using var submitResponse = await client.PostAsJsonAsync( + "/api/learning/practice-sessions/submit", + new SubmitPracticeSessionDto + { + PracticeSessionId = answerable.SessionId, + ExpectedSessionVersion = 2, + IdempotencyKey = "wrong-submit-1" }); using var wrongResponse = await client.GetAsync("/api/learning/wrong-questions"); var answer = await ReadJsonAsync(response); var wrongItems = await ReadItemsAsync(wrongResponse); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(sessionQuestionId, answer.RootElement.GetProperty("sessionQuestionId").GetGuid()); - Assert.False(answer.RootElement.GetProperty("isCorrect").GetBoolean()); + Assert.Equal(answerable.SessionQuestionId, answer.RootElement.GetProperty("sessionQuestionId").GetGuid()); + Assert.False(answer.RootElement.TryGetProperty("isCorrect", out _)); + Assert.Equal(HttpStatusCode.OK, submitResponse.StatusCode); var wrong = Assert.Single(wrongItems); Assert.Equal(questionId, wrong.GetProperty("locator").GetProperty("questionId").GetGuid()); Assert.Equal(1, wrong.GetProperty("wrongCount").GetInt32()); @@ -81,7 +93,8 @@ public sealed class LearningEndpointTests TenantId = seed.TenantId, QuestionId = firstQuestionId, VersionNo = 1, - Content = "first" + Content = "first", + CorrectOptionIndex = 0 }); await factory.SeedQuestionWithVersionAsync( new Question @@ -97,7 +110,8 @@ public sealed class LearningEndpointTests TenantId = seed.TenantId, QuestionId = secondQuestionId, VersionNo = 1, - Content = "second" + Content = "second", + CorrectOptionIndex = 0 }); var firstReferenceId = Guid.NewGuid(); var secondReferenceId = Guid.NewGuid(); @@ -167,25 +181,34 @@ public sealed class LearningEndpointTests var secondSessionQuestionId = detailQuestions.Single(item => item.GetProperty("questionId").GetGuid() == secondQuestionId).GetProperty("sessionQuestionId").GetGuid(); - await client.PostAsJsonAsync( + var firstAnswerResponse = await client.PostAsJsonAsync( "/api/learning/answers", new SubmitAnswerDto { SessionQuestionId = firstSessionQuestionId, - SelectedOptions = ["A"], - SelfJudgedCorrect = true + ExpectedSessionVersion = 1, + ClientSequence = 1, + IdempotencyKey = "practice-first-answer", + SelectedOptionIndices = [0] }); - await client.PostAsJsonAsync( + var secondAnswerResponse = await client.PostAsJsonAsync( "/api/learning/answers", new SubmitAnswerDto { SessionQuestionId = secondSessionQuestionId, - SelectedOptions = ["B"], - SelfJudgedCorrect = false + ExpectedSessionVersion = 2, + ClientSequence = 2, + IdempotencyKey = "practice-second-answer", + SelectedOptionIndices = [1] }); var submitResponse = await client.PostAsJsonAsync( "/api/learning/practice-sessions/submit", - new SubmitPracticeSessionDto { PracticeSessionId = practiceSessionId }); + new SubmitPracticeSessionDto + { + PracticeSessionId = practiceSessionId, + ExpectedSessionVersion = 3, + IdempotencyKey = "practice-submit" + }); var report = await ReadJsonAsync(submitResponse); var reportResponse = await client.GetAsync($"/api/learning/practice-sessions/report?practiceSessionId={practiceSessionId}"); var reportsResponse = await client.GetAsync("/api/learning/practice-reports"); @@ -195,9 +218,11 @@ public sealed class LearningEndpointTests Assert.Equal(HttpStatusCode.OK, createResponse.StatusCode); Assert.Equal(2, created.RootElement.GetProperty("questionCount").GetInt32()); - Assert.Equal("active", created.RootElement.GetProperty("status").GetString()); + Assert.Equal("Active", created.RootElement.GetProperty("status").GetString()); Assert.Equal(HttpStatusCode.OK, detailResponse.StatusCode); Assert.Equal(2, detail.RootElement.GetProperty("questions").GetArrayLength()); + Assert.True(firstAnswerResponse.IsSuccessStatusCode, await firstAnswerResponse.Content.ReadAsStringAsync()); + Assert.True(secondAnswerResponse.IsSuccessStatusCode, await secondAnswerResponse.Content.ReadAsStringAsync()); Assert.Equal(HttpStatusCode.OK, submitResponse.StatusCode); Assert.Equal(2, report.RootElement.GetProperty("totalQuestions").GetInt32()); Assert.Equal(1, report.RootElement.GetProperty("correctCount").GetInt32()); @@ -346,7 +371,7 @@ public sealed class LearningEndpointTests var seed = await SeedLearningUserAsync(factory); var questionId = Guid.NewGuid(); var wordId = Guid.NewGuid(); - var sessionQuestionId = await SeedAnswerableQuestionAsync(factory, seed, questionId); + var answerable = await SeedAnswerableQuestionAsync(factory, seed, questionId); await factory.SeedAsync( new VocabularyWord { @@ -362,9 +387,19 @@ public sealed class LearningEndpointTests "/api/learning/answers", new SubmitAnswerDto { - SessionQuestionId = sessionQuestionId, - SelectedOptions = ["A"], - SelfJudgedCorrect = false + SessionQuestionId = answerable.SessionQuestionId, + ExpectedSessionVersion = 1, + ClientSequence = 1, + IdempotencyKey = "stats-wrong-answer", + SelectedOptionIndices = [1] + }); + await client.PostAsJsonAsync( + "/api/learning/practice-sessions/submit", + new SubmitPracticeSessionDto + { + PracticeSessionId = answerable.SessionId, + ExpectedSessionVersion = 2, + IdempotencyKey = "stats-wrong-submit" }); await client.PostAsJsonAsync( "/api/learning/vocabulary/progress", @@ -405,6 +440,231 @@ public sealed class LearningEndpointTests Assert.Equal(1, wordStats.RootElement.GetProperty("total").GetInt32()); } + [Fact] + public async Task Answers_are_idempotent_versioned_and_keep_revision_history() + { + await using var factory = new ApiTestFactory(); + var seed = await SeedLearningUserAsync(factory); + var answerable = await SeedAnswerableQuestionAsync(factory, seed, Guid.NewGuid()); + using var client = factory.CreateClient(); + await LoginAsync(client, seed); + var firstRequest = new SubmitAnswerDto + { + SessionQuestionId = answerable.SessionQuestionId, + ExpectedSessionVersion = 1, + ClientSequence = 1, + IdempotencyKey = "revision-answer-1", + SelectedOptionIndices = [1] + }; + + var first = await client.PostAsJsonAsync("/api/learning/answers", firstRequest); + var replay = await client.PostAsJsonAsync("/api/learning/answers", firstRequest); + var conflict = await client.PostAsJsonAsync( + "/api/learning/answers", + new SubmitAnswerDto + { + SessionQuestionId = answerable.SessionQuestionId, + ExpectedSessionVersion = 1, + ClientSequence = 1, + IdempotencyKey = "revision-answer-1", + SelectedOptionIndices = [0] + }); + var revision = await client.PostAsJsonAsync( + "/api/learning/answers", + new SubmitAnswerDto + { + SessionQuestionId = answerable.SessionQuestionId, + ExpectedSessionVersion = 2, + ClientSequence = 2, + IdempotencyKey = "revision-answer-2", + SelectedOptionIndices = [0] + }); + var stale = await client.PostAsJsonAsync( + "/api/learning/answers", + new SubmitAnswerDto + { + SessionQuestionId = answerable.SessionQuestionId, + ExpectedSessionVersion = 1, + ClientSequence = 3, + IdempotencyKey = "revision-answer-stale", + SelectedOptionIndices = [0] + }); + + Assert.Equal(HttpStatusCode.OK, first.StatusCode); + Assert.Equal(HttpStatusCode.OK, replay.StatusCode); + Assert.Equal(await first.Content.ReadAsStringAsync(), await replay.Content.ReadAsStringAsync()); + Assert.Equal(HttpStatusCode.Conflict, conflict.StatusCode); + Assert.Equal("idempotency_conflict", await ReadProblemCodeAsync(conflict)); + Assert.Equal(HttpStatusCode.OK, revision.StatusCode); + Assert.Equal(HttpStatusCode.Conflict, stale.StatusCode); + Assert.Equal("practice_session_version_conflict", await ReadProblemCodeAsync(stale)); + + using var scope = factory.CreateSystemScope("Verify immutable answer revisions"); + var records = await scope.ServiceProvider.GetRequiredService().AnswerRecords + .Where(item => item.PracticeSessionId == answerable.SessionId) + .OrderBy(item => item.Revision) + .ToArrayAsync(); + Assert.Equal(2, records.Length); + Assert.False(records[0].IsCurrent); + Assert.True(records[1].IsCurrent); + Assert.Equal(AnswerGradingStatus.Correct, records[1].GradingStatus); + } + + [Fact] + public async Task Session_detail_uses_immutable_safe_snapshot() + { + await using var factory = new ApiTestFactory(); + var seed = await SeedLearningUserAsync(factory); + var answerable = await SeedAnswerableQuestionAsync(factory, seed, Guid.NewGuid()); + using var client = factory.CreateClient(); + await LoginAsync(client, seed); + + var before = await client.GetAsync($"/api/learning/practice-sessions/detail?practiceSessionId={answerable.SessionId}"); + using (var scope = factory.CreateSystemScope("Mutate source question version after session creation")) + { + var db = scope.ServiceProvider.GetRequiredService(); + var version = await db.QuestionVersions.SingleAsync(item => item.Id == answerable.VersionId); + version.Content = "mutated source content"; + version.Explanation = "must remain hidden"; + version.CorrectOptionIndex = 1; + await db.SaveChangesAsync(); + } + var after = await client.GetAsync($"/api/learning/practice-sessions/detail?practiceSessionId={answerable.SessionId}"); + var beforeQuestion = (await ReadJsonAsync(before)).RootElement.GetProperty("questions")[0]; + var afterQuestion = (await ReadJsonAsync(after)).RootElement.GetProperty("questions")[0]; + + Assert.Equal("answerable question", beforeQuestion.GetProperty("content").GetString()); + Assert.Equal("answerable question", afterQuestion.GetProperty("content").GetString()); + Assert.False(beforeQuestion.TryGetProperty("explanation", out _)); + Assert.False(afterQuestion.TryGetProperty("correctOptionIndex", out _)); + } + + [Fact] + public async Task Subjective_answer_produces_non_final_pending_review_report() + { + await using var factory = new ApiTestFactory(); + var seed = await SeedLearningUserAsync(factory); + var answerable = await SeedAnswerableQuestionAsync(factory, seed, Guid.NewGuid(), "short_answer"); + using var client = factory.CreateClient(); + await LoginAsync(client, seed); + + var answer = await client.PostAsJsonAsync( + "/api/learning/answers", + new SubmitAnswerDto + { + SessionQuestionId = answerable.SessionQuestionId, + ExpectedSessionVersion = 1, + ClientSequence = 1, + IdempotencyKey = "subjective-answer", + AnswerText = "student response" + }); + var submit = await client.PostAsJsonAsync( + "/api/learning/practice-sessions/submit", + new SubmitPracticeSessionDto + { + PracticeSessionId = answerable.SessionId, + ExpectedSessionVersion = 2, + IdempotencyKey = "subjective-submit" + }); + var answerBody = await ReadJsonAsync(answer); + var report = await ReadJsonAsync(submit); + var result = report.RootElement.GetProperty("questionResults")[0]; + + Assert.Equal(HttpStatusCode.OK, answer.StatusCode); + Assert.Equal("pending_review", answerBody.RootElement.GetProperty("status").GetString()); + Assert.Equal(HttpStatusCode.OK, submit.StatusCode); + Assert.Equal("PendingReview", report.RootElement.GetProperty("status").GetString()); + Assert.False(report.RootElement.GetProperty("isFinal").GetBoolean()); + Assert.Equal(1, report.RootElement.GetProperty("pendingReviewCount").GetInt32()); + Assert.Equal(JsonValueKind.Null, result.GetProperty("isCorrect").ValueKind); + Assert.Equal(JsonValueKind.Null, result.GetProperty("explanation").ValueKind); + } + + [Fact] + public async Task Concurrent_submission_creates_only_one_authoritative_report() + { + await using var factory = new ApiTestFactory(); + var seed = await SeedLearningUserAsync(factory); + var answerable = await SeedAnswerableQuestionAsync(factory, seed, Guid.NewGuid()); + using var client = factory.CreateClient(); + await LoginAsync(client, seed); + await client.PostAsJsonAsync( + "/api/learning/answers", + new SubmitAnswerDto + { + SessionQuestionId = answerable.SessionQuestionId, + ExpectedSessionVersion = 1, + ClientSequence = 1, + IdempotencyKey = "concurrent-answer", + SelectedOptionIndices = [0] + }); + + var submissions = await Task.WhenAll( + client.PostAsJsonAsync( + "/api/learning/practice-sessions/submit", + new SubmitPracticeSessionDto + { + PracticeSessionId = answerable.SessionId, + ExpectedSessionVersion = 2, + IdempotencyKey = "concurrent-submit-a" + }), + client.PostAsJsonAsync( + "/api/learning/practice-sessions/submit", + new SubmitPracticeSessionDto + { + PracticeSessionId = answerable.SessionId, + ExpectedSessionVersion = 2, + IdempotencyKey = "concurrent-submit-b" + })); + + Assert.Single(submissions, response => response.StatusCode == HttpStatusCode.OK); + Assert.Single(submissions, response => response.StatusCode == HttpStatusCode.Conflict); + using var scope = factory.CreateSystemScope("Verify unique authoritative report"); + Assert.Equal(1, await scope.ServiceProvider.GetRequiredService().PracticeSessionReports + .CountAsync(item => item.PracticeSessionId == answerable.SessionId)); + } + + [Fact] + public async Task Concurrent_devices_cannot_silently_overwrite_an_answer() + { + await using var factory = new ApiTestFactory(); + var seed = await SeedLearningUserAsync(factory); + var answerable = await SeedAnswerableQuestionAsync(factory, seed, Guid.NewGuid()); + using var client = factory.CreateClient(); + await LoginAsync(client, seed); + + var responses = await Task.WhenAll( + client.PostAsJsonAsync( + "/api/learning/answers", + new SubmitAnswerDto + { + SessionQuestionId = answerable.SessionQuestionId, + ExpectedSessionVersion = 1, + ClientSequence = 1, + IdempotencyKey = "device-a-answer", + SelectedOptionIndices = [0] + }), + client.PostAsJsonAsync( + "/api/learning/answers", + new SubmitAnswerDto + { + SessionQuestionId = answerable.SessionQuestionId, + ExpectedSessionVersion = 1, + ClientSequence = 2, + IdempotencyKey = "device-b-answer", + SelectedOptionIndices = [1] + })); + + Assert.Single(responses, response => response.StatusCode == HttpStatusCode.OK); + Assert.Single(responses, response => response.StatusCode == HttpStatusCode.Conflict); + using var scope = factory.CreateSystemScope("Verify concurrent answer winner"); + var records = await scope.ServiceProvider.GetRequiredService().AnswerRecords + .Where(item => item.PracticeSessionId == answerable.SessionId) + .ToArrayAsync(); + Assert.Single(records); + Assert.True(records[0].IsCurrent); + } + private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedLearningUserAsync( ApiTestFactory factory) { @@ -435,10 +695,11 @@ public sealed class LearningEndpointTests return (tenantId, userId, phone); } - private static async Task SeedAnswerableQuestionAsync( + private static async Task<(Guid SessionQuestionId, Guid SessionId, Guid VersionId)> SeedAnswerableQuestionAsync( ApiTestFactory factory, (Guid TenantId, Guid UserId, string Phone) seed, - Guid questionId) + Guid questionId, + string questionType = "choice") { var versionId = Guid.NewGuid(); await factory.SeedQuestionWithVersionAsync( @@ -446,7 +707,7 @@ public sealed class LearningEndpointTests { Id = questionId, TenantId = seed.TenantId, - Type = "choice", + Type = questionType, Status = QuestionStatus.Published }, new QuestionVersion @@ -455,7 +716,8 @@ public sealed class LearningEndpointTests TenantId = seed.TenantId, QuestionId = questionId, VersionNo = 1, - Content = "answerable question" + Content = "answerable question", + CorrectOptionIndex = questionType == "choice" ? 0 : null }); var referenceId = Guid.NewGuid(); @@ -488,9 +750,13 @@ public sealed class LearningEndpointTests QuestionOwnerTenantId = seed.TenantId, QuestionId = questionId, QuestionVersionId = versionId, - Position = 0 + Position = 0, + QuestionType = questionType, + ContentSnapshot = "answerable question", + CorrectOptionIndexSnapshot = questionType == "choice" ? 0 : null, + Score = 1 }); - return sessionQuestionId; + return (sessionQuestionId, sessionId, versionId); } private static async Task LoginAsync( @@ -516,4 +782,10 @@ public sealed class LearningEndpointTests .ToArray(); } + private static async Task ReadProblemCodeAsync(HttpResponseMessage response) + { + using var body = await ReadJsonAsync(response); + return body.RootElement.TryGetProperty("code", out var code) ? code.GetString() : null; + } + } diff --git a/Tiku.UnitTests/Learning/QuestionGraderTests.cs b/Tiku.UnitTests/Learning/QuestionGraderTests.cs new file mode 100644 index 0000000..015aa7d --- /dev/null +++ b/Tiku.UnitTests/Learning/QuestionGraderTests.cs @@ -0,0 +1,81 @@ +using System.Text.Json; +using Tiku.Application.Learning; +using Tiku.Domain.Common; +using Tiku.Domain.Learning; + +namespace Tiku.UnitTests.Learning; + +public sealed class QuestionGraderTests +{ + [Fact] + public void Choice_requires_exactly_one_matching_index() + { + var result = Grade("choice", correctIndex: 1, selected: [1]); + + Assert.Equal(AnswerGradingStatus.Correct, result.Status); + Assert.True(result.IsCorrect); + Assert.Equal(5, result.AwardedScore); + } + + [Fact] + public void Multiple_choice_compares_deduplicated_unordered_sets() + { + var result = Grade( + "multiple_choice", + correctIndices: JsonSerializer.SerializeToElement(new[] { 2, 0 }), + selected: [0, 2, 2]); + + Assert.Equal(AnswerGradingStatus.Correct, result.Status); + } + + [Theory] + [InlineData("对", "true")] + [InlineData("false", "错")] + public void True_false_normalizes_supported_boolean_values(string expected, string actual) + { + var result = Grade("true_false", correctText: expected, answerText: actual); + + Assert.Equal(AnswerGradingStatus.Correct, result.Status); + } + + [Fact] + public void Fill_blank_normalizes_unicode_case_and_whitespace() + { + var rules = JsonSerializer.SerializeToElement(new { acceptedAnswers = new[] { "Entity Framework" } }); + + var result = Grade("fill_blank", correctText: "EF Core", answerText: " ENTITY FRAMEWORK ", rules: rules); + + Assert.Equal(AnswerGradingStatus.Correct, result.Status); + } + + [Theory] + [InlineData("short_answer")] + [InlineData("reading")] + [InlineData("programming")] + public void Subjective_questions_remain_pending_review(string type) + { + var result = Grade(type, answerText: "student answer"); + + Assert.Equal(AnswerGradingStatus.PendingReview, result.Status); + Assert.Null(result.IsCorrect); + Assert.Null(result.AwardedScore); + } + + private static QuestionGradingResult Grade( + string type, + int? correctIndex = null, + JsonElement? correctIndices = null, + string? correctText = null, + IReadOnlyCollection? selected = null, + string? answerText = null, + JsonElement? rules = null) => + QuestionGrader.Grade(new QuestionGradingInput( + type, + correctIndex, + correctIndices ?? JsonDefaults.Array(), + correctText, + rules ?? JsonDefaults.Object(), + selected, + answerText, + 5)); +}