diff --git a/Tiku.Api/Contracts/ContentManagementDtos.cs b/Tiku.Api/Contracts/ContentManagementDtos.cs deleted file mode 100644 index 73bbc35..0000000 --- a/Tiku.Api/Contracts/ContentManagementDtos.cs +++ /dev/null @@ -1,665 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json; -using Tiku.Application.Content; -using Tiku.Application.QuestionBanks; -using Tiku.Domain.Common; -using Tiku.Domain.Content; - -namespace Tiku.Api.Contracts; - -/// -/// 内容管理查询参数。 -/// -public sealed class ContentManagementQueryDto -{ - /// - /// 地区 ID。 - /// - public Guid? RegionId { get; set; } - - /// - /// 内容入口 ID。 - /// - public Guid? EntryId { get; set; } - - /// - /// 节点 ID。 - /// - public Guid? NodeId { get; set; } - - /// - /// 题集 ID。 - /// - public Guid? CollectionId { get; set; } - - /// - /// 父节点 ID;传 root 表示根节点。 - /// - [StringLength(64)] - [RegularExpression("^(root|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$")] - public string? ParentId { get; set; } - - /// - /// 入口类型。 - /// - [StringLength(50)] - public string? EntryType { get; set; } - - /// - /// 题集类型。 - /// - [StringLength(50)] - public string? CollectionType { get; set; } - - /// - /// 模式。 - /// - [StringLength(50)] - public string? Mode { get; set; } - - /// - /// 标记类型。 - /// - [StringLength(50)] - public string? MarkerType { get; set; } - - /// - /// 关键字。 - /// - [StringLength(100)] - public string? Keyword { get; set; } - - /// - /// 是否包含停用数据。 - /// - public bool IncludeInactive { get; set; } - - /// - /// 返回数量上限。 - /// - [Range(1, 1000)] - public int? Limit { get; set; } - - public ContentManagementFilter ToFilter() - { - return new ContentManagementFilter( - RegionId, - EntryId, - NodeId, - CollectionId, - ParentId, - EntryType, - CollectionType, - Mode, - MarkerType, - Keyword, - IncludeInactive, - Limit); - } -} - -/// -/// 新增或更新内容条目请求 DTO。 -/// -public sealed class UpsertContentEntryDto -{ - /// - /// ID。 - /// - public Guid? Id { get; set; } - - /// - /// 地区 ID。 - /// - public Guid? RegionId { get; set; } - - /// - /// 历史系统 ID。 - /// - [StringLength(64)] - public string? LegacyId { get; set; } - - /// - /// 入口键。 - /// - [StringLength(100)] - public string? EntryKey { get; set; } - - /// - /// 名称。 - /// - [Required] - [StringLength(300)] - public string Name { get; set; } = string.Empty; - - /// - /// 入口类型。 - /// - [StringLength(50)] - public string? EntryType { get; set; } - - /// - /// 图标。 - /// - [StringLength(100)] - public string? Icon { get; set; } - - /// - /// 路由地址。 - /// - [StringLength(500)] - public string? Route { get; set; } - - /// - /// 说明。 - /// - [StringLength(2000)] - public string? Description { get; set; } - - /// - /// 可见性。 - /// - [StringLength(50)] - public string? Visibility { get; set; } - - /// - /// 访问规则。 - /// - public JsonElement AccessRules { get; set; } = JsonDefaults.Object(); - - /// - /// 布局配置。 - /// - public JsonElement LayoutConfig { get; set; } = JsonDefaults.Object(); - - /// - /// 显示顺序。 - /// - public int? Order { get; set; } - - /// - /// 是否启用。 - /// - public bool? IsActive { get; set; } - - public UpsertContentEntryCommand ToCommand() - { - return new UpsertContentEntryCommand( - Id, - RegionId, - LegacyId, - EntryKey, - Name, - EntryType, - Icon, - Route, - Description, - Visibility, - AccessRules, - LayoutConfig, - Order, - IsActive); - } -} - -/// -/// 新增或更新内容节点请求 DTO。 -/// -public sealed class UpsertContentNodeDto -{ - /// - /// ID。 - /// - public Guid? Id { get; set; } - - /// - /// 内容入口 ID。 - /// - [Required] - public Guid EntryId { get; set; } - - /// - /// 地区 ID。 - /// - public Guid? RegionId { get; set; } - - /// - /// 父节点 ID。 - /// - public Guid? ParentId { get; set; } - - /// - /// 历史系统 ID。 - /// - [StringLength(64)] - public string? LegacyId { get; set; } - - /// - /// 节点键。 - /// - [StringLength(100)] - public string? NodeKey { get; set; } - - /// - /// 名称。 - /// - [Required] - [StringLength(300)] - public string Name { get; set; } = string.Empty; - - /// - /// 节点Type。 - /// - [StringLength(50)] - public string? NodeType { get; set; } - - /// - /// 标记类型。 - /// - [StringLength(50)] - public string? MarkerType { get; set; } - - /// - /// 标记配置。 - /// - public JsonElement MarkerConfig { get; set; } = JsonDefaults.Object(); - - /// - /// 显示顺序。 - /// - public int? Order { get; set; } - - /// - /// 是否启用。 - /// - public bool? IsActive { get; set; } - - /// - /// 是否可选择。 - /// - public bool? IsSelectable { get; set; } - - /// - /// 是否叶子节点。 - /// - public bool? IsLeaf { get; set; } - - /// - /// 访问规则。 - /// - public JsonElement AccessRules { get; set; } = JsonDefaults.Object(); - - /// - /// 扩展元数据。 - /// - public JsonElement Metadata { get; set; } = JsonDefaults.Object(); - - public UpsertContentNodeCommand ToCommand() - { - return new UpsertContentNodeCommand( - Id, - EntryId, - RegionId, - ParentId, - LegacyId, - NodeKey, - Name, - NodeType, - MarkerType, - MarkerConfig, - Order, - IsActive, - IsSelectable, - IsLeaf, - AccessRules, - Metadata); - } -} - -/// -/// 新增或更新题目题集请求 DTO。 -/// -public sealed class UpsertQuestionCollectionDto -{ - /// - /// ID。 - /// - public Guid? Id { get; set; } - - /// - /// 地区 ID。 - /// - public Guid? RegionId { get; set; } - - /// - /// 内容入口 ID。 - /// - public Guid? EntryId { get; set; } - - /// - /// 节点 ID。 - /// - public Guid? NodeId { get; set; } - - /// - /// 科目 ID。 - /// - public Guid? SubjectId { get; set; } - - /// - /// 分类 ID。 - /// - public Guid? CategoryId { get; set; } - - /// - /// 题库 ID。 - /// - public Guid? QuestionBankId { get; set; } - - /// - /// 历史系统 ID。 - /// - [StringLength(64)] - public string? LegacyId { get; set; } - - /// - /// 名称。 - /// - [Required] - [StringLength(300)] - public string Name { get; set; } = string.Empty; - - /// - /// 题集类型。 - /// - [StringLength(50)] - public string? CollectionType { get; set; } - - /// - /// 来源类型。 - /// - [StringLength(50)] - public string? SourceType { get; set; } - - /// - /// 筛选条件。 - /// - public JsonElement Filters { get; set; } = JsonDefaults.Object(); - - /// - /// 总分。 - /// - public decimal? TotalScore { get; set; } - - /// - /// 时长,单位为分钟。 - /// - public int? DurationMinutes { get; set; } - - /// - /// 状态。 - /// - [StringLength(50)] - public string? Status { get; set; } - - /// - /// 显示顺序。 - /// - public int? Order { get; set; } - - /// - /// 访问规则。 - /// - public JsonElement AccessRules { get; set; } = JsonDefaults.Object(); - - /// - /// 扩展元数据。 - /// - public JsonElement Metadata { get; set; } = JsonDefaults.Object(); - - public UpsertQuestionCollectionCommand ToCommand() - { - return new UpsertQuestionCollectionCommand( - Id, - RegionId, - EntryId, - NodeId, - SubjectId, - CategoryId, - QuestionBankId, - LegacyId, - Name, - CollectionType, - SourceType, - Filters, - TotalScore, - DurationMinutes, - Status, - Order, - AccessRules, - Metadata); - } -} - -/// -/// 题集题目请求 DTO。 -/// -public sealed class CollectionQuestionDto -{ - /// - /// 题目 ID。 - /// - [Required] - public Guid QuestionId { get; set; } - - /// - /// 来源。 - /// - [Required] - public QuestionSource Source { get; set; } = QuestionSource.Tenant; - - /// - /// 分段键。 - /// - [StringLength(100)] - public string? SectionKey { get; set; } - - /// - /// 显示顺序。 - /// - public int? Order { get; set; } - - /// - /// 分数。 - /// - public decimal? Score { get; set; } - - /// - /// 是否必填。 - /// - public bool? Required { get; set; } - - /// - /// 扩展元数据。 - /// - public JsonElement Metadata { get; set; } = JsonDefaults.Object(); - - public CollectionQuestionCommand ToCommand() - { - return new CollectionQuestionCommand( - new QuestionLocator(Source, QuestionId), - SectionKey, - Order, - Score, - Required, - Metadata); - } -} - -/// -/// 替换题集Items请求 DTO。 -/// -public sealed class ReplaceCollectionItemsDto -{ - /// - /// 题集 ID。 - /// - [Required] - public Guid CollectionId { get; set; } - - /// - /// 题目列表。 - /// - public IReadOnlyCollection Questions { get; set; } = []; - - public ReplaceCollectionItemsCommand ToCommand() - { - return new ReplaceCollectionItemsCommand( - CollectionId, - Questions.Select(question => question.ToCommand()).ToArray()); - } -} - -/// -/// 新增或更新练习Blueprint请求 DTO。 -/// -public sealed class UpsertPracticeBlueprintDto -{ - /// - /// ID。 - /// - public Guid? Id { get; set; } - - /// - /// 地区 ID。 - /// - public Guid? RegionId { get; set; } - - /// - /// 内容入口 ID。 - /// - public Guid? EntryId { get; set; } - - /// - /// 节点 ID。 - /// - public Guid? NodeId { get; set; } - - /// - /// 题集 ID。 - /// - public Guid? CollectionId { get; set; } - - /// - /// 历史系统 ID。 - /// - [StringLength(64)] - public string? LegacyId { get; set; } - - /// - /// 名称。 - /// - [Required] - [StringLength(300)] - public string Name { get; set; } = string.Empty; - - /// - /// 模式。 - /// - [StringLength(50)] - public string? Mode { get; set; } - - /// - /// 组卷方式。 - /// - [StringLength(50)] - public string? AssemblyType { get; set; } - - /// - /// 题目数量上限。 - /// - public int? QuestionLimit { get; set; } - - /// - /// 时长,单位为分钟。 - /// - public int? DurationMinutes { get; set; } - - /// - /// 总分。 - /// - public decimal? TotalScore { get; set; } - - /// - /// 及格分。 - /// - public decimal? PassScore { get; set; } - - /// - /// 分段配置。 - /// - public JsonElement Sections { get; set; } = JsonDefaults.Array(); - - /// - /// 规则配置。 - /// - public JsonElement Rules { get; set; } = JsonDefaults.Object(); - - /// - /// 访问规则。 - /// - public JsonElement AccessRules { get; set; } = JsonDefaults.Object(); - - /// - /// 状态。 - /// - [StringLength(50)] - public string? Status { get; set; } - - /// - /// 显示顺序。 - /// - public int? Order { get; set; } - - public UpsertPracticeBlueprintCommand ToCommand() - { - return new UpsertPracticeBlueprintCommand( - Id, - RegionId, - EntryId, - NodeId, - CollectionId, - LegacyId, - Name, - Mode, - AssemblyType, - QuestionLimit, - DurationMinutes, - TotalScore, - PassScore, - Sections, - Rules, - AccessRules, - Status, - Order); - } -} - -/// -/// 导入Template查询参数。 -/// -public sealed class ImportTemplateQueryDto -{ - /// - /// 导入Type。 - /// - [Required] - [StringLength(50)] - public string ImportType { get; set; } = string.Empty; - - /// - /// 导出格式。 - /// - [StringLength(10)] - public string? Format { get; set; } -} \ No newline at end of file diff --git a/Tiku.Api/Contracts/ContentNavigationDtos.cs b/Tiku.Api/Contracts/ContentNavigationDtos.cs deleted file mode 100644 index e7aa9f2..0000000 --- a/Tiku.Api/Contracts/ContentNavigationDtos.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Tiku.Application.Content; - -namespace Tiku.Api.Contracts; - -/// -/// 内容Navigation查询参数。 -/// -public sealed class ContentNavigationQueryDto -{ - /// - /// 租户编码。 - /// - [StringLength(100)] - public string? TenantCode { get; set; } - - /// - /// 地区 ID。 - /// - public Guid? RegionId { get; set; } - - /// - /// 内容入口 ID。 - /// - public Guid? EntryId { get; set; } - - /// - /// 节点 ID。 - /// - public Guid? NodeId { get; set; } - - /// - /// 题集 ID。 - /// - public Guid? CollectionId { get; set; } - - /// - /// 父节点 ID;传 root 表示根节点。 - /// - [StringLength(64)] - [RegularExpression("^(root|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$")] - public string? ParentId { get; set; } - - /// - /// 入口类型。 - /// - [StringLength(50)] - public string? EntryType { get; set; } - - /// - /// 题集类型。 - /// - [StringLength(50)] - public string? CollectionType { get; set; } - - /// - /// 模式。 - /// - [StringLength(50)] - public string? Mode { get; set; } - - /// - /// 标记类型。 - /// - [StringLength(50)] - public string? MarkerType { get; set; } - - /// - /// 关键字。 - /// - [StringLength(100)] - public string? Keyword { get; set; } - - /// - /// 是否包含隐藏数据。 - /// - public bool IncludeHidden { get; set; } - - /// - /// 是否包含停用数据。 - /// - public bool IncludeInactive { get; set; } - - /// - /// 返回数量上限。 - /// - [Range(1, 1000)] - public int? Limit { get; set; } - - public ContentNavigationFilter ToFilter(Guid tenantId) - { - var parentWasSpecified = ParentId is not null; - var parentIsRoot = string.Equals(ParentId, "root", StringComparison.OrdinalIgnoreCase); - Guid? parentId = parentIsRoot || string.IsNullOrWhiteSpace(ParentId) - ? null - : Guid.Parse(ParentId); - - return new ContentNavigationFilter( - tenantId, - RegionId, - EntryId, - NodeId, - CollectionId, - parentId, - parentWasSpecified, - parentIsRoot, - EntryType, - CollectionType, - Mode, - MarkerType, - Keyword, - IncludeHidden, - IncludeInactive, - Limit); - } -} \ No newline at end of file diff --git a/Tiku.Api/Contracts/ContentV2Dtos.cs b/Tiku.Api/Contracts/ContentV2Dtos.cs new file mode 100644 index 0000000..ad5afd4 --- /dev/null +++ b/Tiku.Api/Contracts/ContentV2Dtos.cs @@ -0,0 +1,138 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Tiku.Application.Content; +using Tiku.Domain.Common; +using Tiku.Domain.Content; + +namespace Tiku.Api.Contracts; + +public sealed class QuestionRevisionDraftDto +{ + [Required][StringLength(50)] public string QuestionType { get; set; } = "choice"; + [StringLength(100)] public string? TypeLabel { get; set; } + [Range(1, 5)] public int? Difficulty { get; set; } + public string? Content { get; set; } + public JsonElement Options { get; set; } = JsonDefaults.Array(); + public int? CorrectOptionIndex { get; set; } + public JsonElement CorrectOptionIndices { get; set; } = JsonDefaults.Array(); + public string? AnswerText { get; set; } + public string? Explanation { get; set; } + public JsonElement SubQuestions { get; set; } = JsonDefaults.Array(); + [StringLength(50)] public string? CodeLang { get; set; } + public string? CodeTemplate { get; set; } + + public QuestionRevisionDraft ToCommand() => new( + QuestionType, + TypeLabel, + Difficulty, + Content, + Options, + CorrectOptionIndex, + CorrectOptionIndices, + AnswerText, + Explanation, + SubQuestions, + CodeLang, + CodeTemplate); +} + +public sealed class QuestionDuplicateSearchDto +{ + [Required] public QuestionRevisionDraftDto Draft { get; set; } = new(); + [Range(1, 100)] public int Limit { get; set; } = 20; +} + +public sealed class ExecuteQuestionAuthoringDto +{ + [Required] public QuestionImportDecision Decision { get; set; } + [Required] public QuestionRevisionDraftDto Draft { get; set; } = new(); + public Guid? ReuseQuestionAssetId { get; set; } + public Guid? FamilyId { get; set; } + public bool PublishAsset { get; set; } + + public ExecuteQuestionAuthoringCommand ToCommand() => new( + Decision, + Draft.ToCommand(), + ReuseQuestionAssetId, + FamilyId, + PublishAsset); +} + +public sealed class CreateQuestionRevisionV2Dto +{ + [Required] public QuestionRevisionDraftDto Draft { get; set; } = new(); + public bool PublishAsset { get; set; } +} + +public sealed class PlacementConditionDto +{ + [Required] public Guid TargetDimensionDefinitionId { get; set; } + [Required] public Guid TargetNodeId { get; set; } + [Required] public TargetRuleOperator Operator { get; set; } + + public PlacementConditionCommand ToCommand() => + new(TargetDimensionDefinitionId, TargetNodeId, Operator); +} + +public sealed class PlacementRuleGroupDto +{ + [Range(0, 1000)] public int GroupOrder { get; set; } + [MinLength(1)] public IReadOnlyCollection Conditions { get; set; } = []; + + public PlacementRuleGroupCommand ToCommand() => + new(GroupOrder, Conditions.Select(item => item.ToCommand()).ToArray()); +} + +public sealed class CreateQuestionPlacementDto +{ + [Required] public Guid QuestionAssetOwnerTenantId { get; set; } + [Required] public Guid QuestionAssetId { get; set; } + [Required] public Guid CurriculumVersionId { get; set; } + [Required] public Guid CurriculumNodeId { get; set; } + [Required] public Guid AssessmentPolicyVersionId { get; set; } + public IReadOnlyCollection KnowledgeConceptIds { get; set; } = []; + public IReadOnlyCollection RuleGroups { get; set; } = []; + [Range(1, 5)] public int? DifficultyOverride { get; set; } + public bool Activate { get; set; } + + public CreateQuestionPlacementCommand ToCommand() => new( + QuestionAssetOwnerTenantId, + QuestionAssetId, + CurriculumVersionId, + CurriculumNodeId, + AssessmentPolicyVersionId, + KnowledgeConceptIds, + RuleGroups.Select(item => item.ToCommand()).ToArray(), + DifficultyOverride, + Activate); +} + +public sealed class PublishContentReleaseDto +{ + [Required] public Guid CurriculumVersionId { get; set; } + [Required][StringLength(300, MinimumLength = 1)] public string Name { get; set; } = string.Empty; +} + +public sealed class QuestionImportPreviewRowDto +{ + [Required][StringLength(100, MinimumLength = 1)] public string RowKey { get; set; } = string.Empty; + [Required] public QuestionRevisionDraftDto Draft { get; set; } = new(); +} + +public sealed class PreviewQuestionImportV2Dto +{ + [Required][MinLength(1)][MaxLength(2000)] + public IReadOnlyCollection Rows { get; set; } = []; +} + +public sealed class ExecuteQuestionImportRowDto +{ + [Required][StringLength(100, MinimumLength = 1)] public string RowKey { get; set; } = string.Empty; + [Required] public ExecuteQuestionAuthoringDto Command { get; set; } = new(); +} + +public sealed class ExecuteQuestionImportV2Dto +{ + [Required][MinLength(1)][MaxLength(2000)] + public IReadOnlyCollection Rows { get; set; } = []; +} diff --git a/Tiku.Api/Contracts/LearningAccessDtos.cs b/Tiku.Api/Contracts/LearningAccessDtos.cs deleted file mode 100644 index f8850ab..0000000 --- a/Tiku.Api/Contracts/LearningAccessDtos.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Tiku.Application.Learning; -using Tiku.Domain.Learning; - -namespace Tiku.Api.Contracts; - -public sealed class ChangeStudentTargetRegionDto -{ - public Guid MarketRegionId { get; set; } - - public ChangeStudentTargetRegionCommand ToCommand() => new(MarketRegionId); -} - -public sealed class OverrideStudentTargetRegionDto -{ - public Guid MarketRegionId { get; set; } - - [Required] - [StringLength(1000, MinimumLength = 1)] - public string Reason { get; set; } = string.Empty; - - public OverrideStudentTargetRegionCommand ToCommand(Guid userId, Guid changedBy) => - new(userId, MarketRegionId, changedBy, Reason); -} - -public sealed class UpsertClassContentAssignmentDto -{ - public Guid? Id { get; set; } - public Guid ClassId { get; set; } - public Guid ContentSliceId { get; set; } - public LearningContentResourceType ResourceType { get; set; } - public Guid ResourceId { get; set; } - public DateTimeOffset? StartsAt { get; set; } - public DateTimeOffset? EndsAt { get; set; } - - public UpsertClassContentAssignmentCommand ToCommand() => new( - Id, ClassId, ContentSliceId, ResourceType, ResourceId, StartsAt, EndsAt); -} - -public sealed class RevokeClassContentAssignmentDto -{ - [StringLength(1000)] - public string? Reason { get; set; } -} diff --git a/Tiku.Api/Contracts/LearningAccessV2Dtos.cs b/Tiku.Api/Contracts/LearningAccessV2Dtos.cs new file mode 100644 index 0000000..dd1adce --- /dev/null +++ b/Tiku.Api/Contracts/LearningAccessV2Dtos.cs @@ -0,0 +1,45 @@ +using System.ComponentModel.DataAnnotations; +using Tiku.Application.Learning; +using Tiku.Domain.Learning; + +namespace Tiku.Api.Contracts; + +public sealed class UpsertClassAssignmentGrantDto +{ + public Guid? Id { get; set; } + public Guid ProductAccessManifestVersionId { get; set; } + public AccessResourceType ResourceType { get; set; } + public Guid ResourceId { get; set; } + public DateTimeOffset? StartsAt { get; set; } + public DateTimeOffset? EndsAt { get; set; } + + public UpsertClassAssignmentGrantCommand ToCommand(Guid classId) => new( + Id, + classId, + ProductAccessManifestVersionId, + ResourceType, + ResourceId, + StartsAt, + EndsAt); +} + +public sealed class RevokeClassAssignmentGrantDto +{ + [Required] + [StringLength(1000, MinimumLength = 1)] + public string Reason { get; set; } = string.Empty; +} + +public sealed class OverrideStudentLearningTargetsDto +{ + public Guid PrimaryProfileVersionId { get; set; } + public IReadOnlyCollection AlternateProfileVersionIds { get; set; } = []; + + [Required] + [StringLength(1000, MinimumLength = 1)] + public string Reason { get; set; } = string.Empty; + + public ChangeLearningTargetsCommand ToCommand() => new( + PrimaryProfileVersionId, + AlternateProfileVersionIds); +} diff --git a/Tiku.Api/Contracts/LearningDtos.cs b/Tiku.Api/Contracts/LearningDtos.cs index 75923a9..466275e 100644 --- a/Tiku.Api/Contracts/LearningDtos.cs +++ b/Tiku.Api/Contracts/LearningDtos.cs @@ -5,6 +5,7 @@ using Tiku.Application.Learning; using Tiku.Application.QuestionBanks; using Tiku.Domain.Common; using Tiku.Domain.Content; +using Tiku.Domain.Learning; namespace Tiku.Api.Contracts; @@ -92,73 +93,20 @@ public sealed class CreatePracticeSessionDto public string? Mode { get; set; } /// - /// 目标类型。 + /// 目标资源类型。租户、Release、Segment 和业务由服务端推导。 /// - [StringLength(50)] - public string? TargetType { get; set; } + [Required] + public AccessResourceType? ResourceType { get; set; } /// - /// 目标 ID。 + /// 目标资源 ID。 /// - public Guid? TargetId { get; set; } - - /// - /// 练习蓝图 ID。 - /// - public Guid? BlueprintId { get; set; } - - /// - /// 题集 ID。 - /// - public Guid? CollectionId { get; set; } - - /// - /// 内容入口 ID。 - /// - public Guid? EntryId { get; set; } - - /// - /// 内容节点 ID。 - /// - public Guid? ContentNodeId { get; set; } - - /// - /// 题目数量上限。 - /// - [Range(1, 500)] - public int? QuestionLimit { get; set; } - - /// - /// 时长,单位为分钟。 - /// - [Range(1, 1440)] - public int? DurationMinutes { get; set; } - - /// - /// 总分。 - /// - [Range(typeof(decimal), "0", "99999")] - public decimal? TotalScore { get; set; } - - /// - /// 扩展元数据。 - /// - public JsonElement Metadata { get; set; } = JsonDefaults.Object(); + [Required] + public Guid? ResourceId { get; set; } public PracticeSessionCommand ToCommand() { - return new PracticeSessionCommand( - Mode, - TargetType, - TargetId, - BlueprintId, - CollectionId, - EntryId, - ContentNodeId, - QuestionLimit, - DurationMinutes, - TotalScore, - Metadata); + return new PracticeSessionCommand(Mode, ResourceType!.Value, ResourceId!.Value); } } @@ -173,15 +121,30 @@ public sealed class SubmitPracticeSessionDto [Required] public Guid PracticeSessionId { get; set; } - [Required][Range(1, long.MaxValue)] public long ExpectedSessionVersion { get; set; } - [Required] [StringLength(200, MinimumLength = 1)] public string IdempotencyKey { get; set; } = string.Empty; public SubmitPracticeSessionCommand ToCommand() { - return new SubmitPracticeSessionCommand(PracticeSessionId, ExpectedSessionVersion, IdempotencyKey); + return new SubmitPracticeSessionCommand(PracticeSessionId, IdempotencyKey); + } +} + +/// +/// 学生变更当前业务的主目标和备选目标。租户、业务许可和产品范围均由服务端校验。 +/// +public sealed class ChangeLearningTargetsDto +{ + [Required] + public Guid PrimaryProfileVersionId { get; set; } + + [MaxLength(15)] + public IReadOnlyCollection AlternateProfileVersionIds { get; set; } = []; + + public ChangeLearningTargetsCommand ToCommand() + { + return new ChangeLearningTargetsCommand(PrimaryProfileVersionId, AlternateProfileVersionIds); } } diff --git a/Tiku.Api/Contracts/TaxonomyDtos.cs b/Tiku.Api/Contracts/TaxonomyDtos.cs deleted file mode 100644 index 76053ad..0000000 --- a/Tiku.Api/Contracts/TaxonomyDtos.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json; -using Tiku.Application.Catalog; -using Tiku.Domain.Catalog; -using Tiku.Domain.Common; -using Tiku.Domain.Content; - -namespace Tiku.Api.Contracts; - -/// -/// 创建分类节点请求 DTO。 -/// -public sealed class CreateTaxonomyNodeDto -{ - /// - /// 父节点 ID。 - /// - public Guid? ParentId { get; set; } - - /// - /// 父级来源。 - /// - public QuestionSource? ParentSource { get; set; } - - /// - /// 节点Type。 - /// - [Required] - public TaxonomyNodeType NodeType { get; set; } - - /// - /// 编码。 - /// - [Required] - [StringLength(100)] - public string Code { get; set; } = string.Empty; - - /// - /// 名称。 - /// - [Required] - [StringLength(300)] - public string Name { get; set; } = string.Empty; - - /// - /// 排序值。 - /// - public int SortOrder { get; set; } - - /// - /// 扩展元数据。 - /// - public JsonElement Metadata { get; set; } = JsonDefaults.Object(); - - public CreateTaxonomyNodeCommand ToCommand() - { - return new CreateTaxonomyNodeCommand( - ParentId, - ParentSource, - NodeType, - Code, - Name, - SortOrder, - Metadata); - } -} \ No newline at end of file diff --git a/Tiku.Api/Controllers/CatalogController.cs b/Tiku.Api/Controllers/CatalogController.cs deleted file mode 100644 index 3728c80..0000000 --- a/Tiku.Api/Controllers/CatalogController.cs +++ /dev/null @@ -1,562 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Tiku.Api.Contracts; -using Tiku.Api.Security; -using Tiku.Application.Assets; -using Tiku.Application.Catalog; -using Tiku.Application.Content; -using Tiku.Application.Learning; -using Tiku.Application.QuestionBanks; -using Tiku.Application.Security; -using Tiku.Application.StudyContent; - -namespace Tiku.Api.Controllers; - -[ApiController] -[Tags("学生端-认证目录")] -[Authorize(Policy = TikuPolicies.CurrentTenantMember)] -[Produces("application/json")] -[Route("api/student/catalog")] -public sealed class CatalogController( - ICatalogQueryService catalogQueryService, - IContentNavigationQueryService contentNavigationQueryService, - IQuestionBankQueryService questionBankQueryService, - IStudyContentQueryService studyContentQueryService, - IAssetQueryService assetQueryService, - ITenantContext currentTenant, - ILearningAccessService learningAccessService, - LearningActorResolver learningActorResolver) : ControllerBase -{ - [HttpGet("regions")] - [EndpointSummary("查询可用地区")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetRegions( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetRegionsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("region-modules")] - [EndpointSummary("查询地区功能模块")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetRegionModules( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetRegionModulesAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("module-nodes")] - [EndpointSummary("查询模块导航节点")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetModuleNodes( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetModuleNodesAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("schools")] - [EndpointSummary("查询院校目录")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetSchools( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetSchoolsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("majors")] - [EndpointSummary("查询专业目录")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetMajors( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetMajorsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("subjects")] - [EndpointSummary("查询科目目录")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetSubjects( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetSubjectsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("categories")] - [HttpGet("question-categories")] - [EndpointSummary("查询题目分类")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetCategories( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetCategoriesAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("content-entries")] - [EndpointSummary("查询内容入口")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetContentEntries( - [FromQuery] ContentNavigationQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await contentNavigationQueryService.GetContentEntriesAsync( - await ToAuthorizedFilterAsync(query, cancellationToken), - cancellationToken)); - } - - [HttpGet("content-nodes")] - [EndpointSummary("查询内容导航节点")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetContentNodes( - [FromQuery] ContentNavigationQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await contentNavigationQueryService.GetContentNodesAsync( - await ToAuthorizedFilterAsync(query, cancellationToken), - cancellationToken)); - } - - [HttpGet("question-collections")] - [RequireSaasFeature(SaasFeatureCatalog.Practice)] - [EndpointSummary("查询可用题集")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetQuestionCollections( - [FromQuery] ContentNavigationQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await contentNavigationQueryService.GetQuestionCollectionsAsync( - await ToAuthorizedFilterAsync(query, cancellationToken), - cancellationToken)); - } - - [HttpGet("question-collections/questions")] - [RequireSaasFeature(SaasFeatureCatalog.Practice)] - [EndpointSummary("查询题集内题目")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetCollectionQuestions( - [FromQuery] ContentNavigationQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await contentNavigationQueryService.GetCollectionQuestionsAsync( - await ToAuthorizedFilterAsync(query, cancellationToken), - cancellationToken)); - } - - [HttpGet("practice-blueprints")] - [RequireSaasFeature(SaasFeatureCatalog.Practice)] - [EndpointSummary("查询练习蓝图")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetPracticeBlueprints( - [FromQuery] ContentNavigationQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await contentNavigationQueryService.GetPracticeBlueprintsAsync( - await ToAuthorizedFilterAsync(query, cancellationToken), - cancellationToken)); - } - - [HttpGet("question-banks")] - [RequireSaasFeature(SaasFeatureCatalog.Practice)] - [EndpointSummary("查询题库列表")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetQuestionBanks( - [FromQuery] QuestionBankQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await questionBankQueryService.GetQuestionBanksAsync( - await ToAuthorizedFilterAsync(query, null, cancellationToken), - cancellationToken)); - } - - [HttpGet("questions")] - [RequireSaasFeature(SaasFeatureCatalog.Practice)] - [EndpointSummary("查询已发布题目")] - [EndpointDescription("支持按题库、科目、分类、模块节点、内容入口、内容节点、题集或题目 ID 列表筛选。")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetQuestions( - [FromQuery] QuestionBankQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await questionBankQueryService.GetQuestionsAsync( - await ToAuthorizedFilterAsync(query, null, cancellationToken), - cancellationToken)); - } - - [HttpGet("questions/{questionId:guid}")] - [RequireSaasFeature(SaasFeatureCatalog.Practice)] - [EndpointSummary("查询题目详情")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetQuestion( - Guid questionId, - [FromQuery] QuestionBankQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await questionBankQueryService.GetQuestionAsync( - await ToAuthorizedFilterAsync(query, questionId, cancellationToken), - cancellationToken)); - } - - [HttpGet("vocabulary-units")] - [RequireSaasFeature(SaasFeatureCatalog.Vocabulary)] - [EndpointSummary("查询词汇单元")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetVocabularyUnits( - [FromQuery] StudyContentQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await studyContentQueryService.GetVocabularyUnitsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("vocabulary-words")] - [RequireSaasFeature(SaasFeatureCatalog.Vocabulary)] - [EndpointSummary("查询词汇单词")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetVocabularyWords( - [FromQuery] StudyContentQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await studyContentQueryService.GetVocabularyWordsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("handbook-subjects")] - [RequireSaasFeature(SaasFeatureCatalog.Handbook)] - [EndpointSummary("查询知识手册科目")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetHandbookSubjects( - [FromQuery] StudyContentQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await studyContentQueryService.GetHandbookSubjectsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("handbook-chapters")] - [RequireSaasFeature(SaasFeatureCatalog.Handbook)] - [EndpointSummary("查询知识手册章节")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetHandbookChapters( - [FromQuery] StudyContentQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await studyContentQueryService.GetHandbookChaptersAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("handbook-entries")] - [RequireSaasFeature(SaasFeatureCatalog.Handbook)] - [EndpointSummary("查询知识手册条目")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetHandbookEntries( - [FromQuery] StudyContentQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await studyContentQueryService.GetHandbookEntriesAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("content-assets")] - [EndpointSummary("查询内容资源")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetContentAssets( - [FromQuery] AssetQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await assetQueryService.GetContentAssetsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("images")] - [EndpointSummary("查询图片资源")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetImages( - [FromQuery] AssetQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await assetQueryService.GetImagesAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("app-assets")] - [EndpointSummary("查询应用资源")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetAppAssets( - [FromQuery] AssetQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await assetQueryService.GetAppAssetsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("video-explanations")] - [RequireSaasFeature(SaasFeatureCatalog.Video)] - [EndpointSummary("查询视频讲解")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetVideoExplanations( - [FromQuery] AssetQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await assetQueryService.GetVideoExplanationsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("question-videos")] - [RequireSaasFeature(SaasFeatureCatalog.Video)] - [EndpointSummary("查询题目关联视频")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetQuestionVideos( - [FromQuery] AssetQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await assetQueryService.GetQuestionVideosAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("banners")] - [RequireSaasFeature(SaasFeatureCatalog.SiteContent)] - [EndpointSummary("查询首页横幅")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetBanners( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetBannersAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("faqs")] - [RequireSaasFeature(SaasFeatureCatalog.SiteContent)] - [EndpointSummary("查询常见问题")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetFaqs( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetFaqsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("announcements")] - [RequireSaasFeature(SaasFeatureCatalog.SiteContent)] - [EndpointSummary("查询公告")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetAnnouncements( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetAnnouncementsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("exam-dates")] - [RequireSaasFeature(SaasFeatureCatalog.SiteContent)] - [EndpointSummary("查询考试日期")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetExamDates( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetExamDatesAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("products")] - [RequireSaasFeature(SaasFeatureCatalog.StudentStore)] - [EndpointSummary("查询可购买产品")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetProducts( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetProductsAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - [HttpGet("svip-plans")] - [RequireSaasFeature(SaasFeatureCatalog.StudentStore)] - [EndpointSummary("查询 SVIP 套餐")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetSvipPlans( - [FromQuery] CatalogQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await catalogQueryService.GetSvipPlansAsync( - query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), - cancellationToken)); - } - - private async Task ResolveTenantIdAsync( - CatalogQueryDto query, - CancellationToken cancellationToken) - { - var actor = learningActorResolver.Resolve(); - var snapshot = await learningAccessService.GetSnapshotAsync(actor, cancellationToken); - query.RegionId = ResolveAuthorizedRegion(query.RegionId, snapshot); - query.AllowedRegionIds = snapshot.LicensedRegionIds.ToArray(); - return currentTenant.TenantId ?? throw new TenantNotFoundException(); - } - - private async Task ToAuthorizedFilterAsync( - ContentNavigationQueryDto query, - CancellationToken cancellationToken) - { - var actor = learningActorResolver.Resolve(); - var snapshot = await learningAccessService.GetSnapshotAsync(actor, cancellationToken); - return query.ToFilter(actor.TenantId) with - { - RegionId = ResolveAuthorizedRegion(query.RegionId, snapshot), - AllowedContentSliceIds = snapshot.ContentSliceIds.ToArray(), - AllowedRegionIds = snapshot.LicensedRegionIds.ToArray() - }; - } - - private async Task ToAuthorizedFilterAsync( - QuestionBankQueryDto query, - Guid? questionId, - CancellationToken cancellationToken) - { - var actor = learningActorResolver.Resolve(); - var snapshot = await learningAccessService.GetSnapshotAsync(actor, cancellationToken); - return query.ToFilter(actor.TenantId, questionId) with - { - RegionId = ResolveAuthorizedRegion(query.RegionId, snapshot), - AllowedContentSliceIds = snapshot.ContentSliceIds.ToArray() - }; - } - - private static Guid? ResolveAuthorizedRegion(Guid? requestedRegionId, LearningAccessSnapshot snapshot) - { - if (requestedRegionId.HasValue && - requestedRegionId != snapshot.TargetRegionId && - !snapshot.LicensedRegionIds.Contains(requestedRegionId.Value)) - throw new LearningAccessException( - "catalog_region_not_entitled", - "The requested catalog region is outside the current learning grant."); - - return requestedRegionId ?? snapshot.TargetRegionId ?? - (snapshot.LicensedRegionIds.Count == 1 ? snapshot.LicensedRegionIds.Single() : null); - } - - private Task ResolveTenantIdAsync( - ContentNavigationQueryDto query, - CancellationToken cancellationToken) - { - return ResolveTenantIdAsync( - new CatalogQueryDto - { - TenantCode = query.TenantCode - }, - cancellationToken); - } - - private Task ResolveTenantIdAsync( - QuestionBankQueryDto query, - CancellationToken cancellationToken) - { - return ResolveTenantIdAsync( - new CatalogQueryDto - { - TenantCode = query.TenantCode - }, - cancellationToken); - } - - private async Task ResolveTenantIdAsync( - StudyContentQueryDto query, - CancellationToken cancellationToken) - { - var actor = learningActorResolver.Resolve(); - var snapshot = await learningAccessService.GetSnapshotAsync(actor, cancellationToken); - query.RegionId = ResolveAuthorizedRegion(query.RegionId, snapshot); - query.AllowedRegionIds = snapshot.LicensedRegionIds.ToArray(); - return currentTenant.TenantId ?? throw new TenantNotFoundException(); - } - - private async Task ResolveTenantIdAsync( - AssetQueryDto query, - CancellationToken cancellationToken) - { - var actor = learningActorResolver.Resolve(); - var snapshot = await learningAccessService.GetSnapshotAsync(actor, cancellationToken); - query.RegionId = ResolveAuthorizedRegion(query.RegionId, snapshot); - query.AllowedRegionIds = snapshot.LicensedRegionIds.ToArray(); - return currentTenant.TenantId ?? throw new TenantNotFoundException(); - } -} - -public sealed class TenantNotFoundException : Exception -{ - public TenantNotFoundException() - : base("Tenant was not found.") - { - } -} diff --git a/Tiku.Api/Controllers/ContentV2AuthoringController.cs b/Tiku.Api/Controllers/ContentV2AuthoringController.cs new file mode 100644 index 0000000..2f1edcc --- /dev/null +++ b/Tiku.Api/Controllers/ContentV2AuthoringController.cs @@ -0,0 +1,107 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tiku.Api.Contracts; +using Tiku.Api.Security; +using Tiku.Application.Content; +using Tiku.Application.Security; + +namespace Tiku.Api.Controllers; + +[ApiController] +[Tags("租户端-内容 V2 录题")] +[Authorize(Policy = BackendPermissions.TenantContentManage)] +[Authorize(Policy = TikuPolicies.TenantAllDataScope)] +[RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)] +[Produces("application/json")] +[Route("api/tenant/content-v2")] +public sealed class ContentV2AuthoringController( + IContentV2AuthoringService service, + IContentReleaseCompiler releaseCompiler, + DirectContentActorResolver actorResolver) : ControllerBase +{ + [HttpPost("questions/duplicate-search")] + [EndpointSummary("查询当前租户和平台公共范围内的精确/相似题")] + public Task SearchDuplicates( + QuestionDuplicateSearchDto request, + CancellationToken cancellationToken) + { + return service.SearchDuplicatesAsync(Actor(), request.Draft.ToCommand(), request.Limit, cancellationToken); + } + + [HttpPost("questions/execute")] + [EndpointSummary("按复用、新建、变体、跳过或人工复核决策执行录题")] + public Task Execute( + ExecuteQuestionAuthoringDto request, + CancellationToken cancellationToken) + { + return service.ExecuteAsync(Actor(), request.ToCommand(), cancellationToken); + } + + [HttpPost("questions/{questionAssetId:guid}/revisions")] + [EndpointSummary("为租户题目资产创建不可变 Revision")] + public Task CreateRevision( + Guid questionAssetId, + CreateQuestionRevisionV2Dto request, + CancellationToken cancellationToken) + { + return service.CreateRevisionAsync( + Actor(), questionAssetId, request.Draft.ToCommand(), request.PublishAsset, cancellationToken); + } + + [HttpPost("questions/placements")] + [EndpointSummary("把租户私题或平台公共题放置到大纲并配置适用规则")] + public Task CreatePlacement( + CreateQuestionPlacementDto request, + CancellationToken cancellationToken) + { + return service.CreatePlacementAsync(Actor(), request.ToCommand(), cancellationToken); + } + + [HttpPost("imports/questions/preview")] + [EndpointSummary("预览 Excel/Word 解析后的题目行并给出精确复用或人工复核建议")] + public Task PreviewImport( + PreviewQuestionImportV2Dto request, + CancellationToken cancellationToken) + { + return service.PreviewImportAsync( + Actor(), + request.Rows.Select(row => (row.RowKey, row.Draft.ToCommand())).ToArray(), + cancellationToken); + } + + [HttpPost("imports/questions/execute")] + [EndpointSummary("按逐行复用、新建、变体、跳过或人工复核决策执行导入")] + public Task ExecuteImport( + ExecuteQuestionImportV2Dto request, + CancellationToken cancellationToken) + { + return service.ExecuteImportAsync( + Actor(), + request.Rows.Select(row => new ExecuteQuestionImportRow( + row.RowKey, + row.Command.ToCommand())).ToArray(), + cancellationToken); + } + + [HttpPost("releases/publish")] + [EndpointSummary("校验并发布不可变内容 Release")] + public Task PublishRelease( + PublishContentReleaseDto request, + CancellationToken cancellationToken) + { + var actor = Actor(); + return releaseCompiler.PublishAsync( + new PublishContentReleaseCommand( + actor.TenantId, + actor.UserId, + request.CurriculumVersionId, + request.Name), + cancellationToken); + } + + private ContentAuthorActor Actor() + { + var actor = actorResolver.Resolve(); + return new ContentAuthorActor(actor.TenantId, actor.UserId); + } +} diff --git a/Tiku.Api/Controllers/LearningAccessController.cs b/Tiku.Api/Controllers/LearningAccessController.cs deleted file mode 100644 index 946983c..0000000 --- a/Tiku.Api/Controllers/LearningAccessController.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Tiku.Api.Contracts; -using Tiku.Api.Security; -using Tiku.Application.Learning; -using Tiku.Application.Security; - -namespace Tiku.Api.Controllers; - -[ApiController] -[Tags("学生端-学习授权")] -[Authorize(Policy = TikuPolicies.CurrentTenantMember)] -[RequireSaasFeature(SaasFeatureCatalog.Practice)] -[Produces("application/json")] -[Route("api/student/learning/access")] -public sealed class LearningAccessController( - ILearningAccessService accessService, - ILearningAccessAdministrationService administrationService, - LearningActorResolver actorResolver) : ControllerBase -{ - [HttpGet] - [EndpointSummary("查询当前学习授权快照")] - public Task Get(CancellationToken cancellationToken) - { - return accessService.GetSnapshotAsync(actorResolver.Resolve(), cancellationToken); - } - - [HttpGet("target-region")] - [EndpointSummary("查询当前个人目标地区")] - public Task GetTargetRegion(CancellationToken cancellationToken) - { - return administrationService.GetTargetRegionAsync(actorResolver.Resolve(), cancellationToken); - } - - [HttpPut("target-region")] - [EndpointSummary("变更个人目标地区")] - public Task ChangeTargetRegion( - ChangeStudentTargetRegionDto request, - CancellationToken cancellationToken) - { - return administrationService.ChangeTargetRegionAsync( - actorResolver.Resolve(), request.ToCommand(), cancellationToken); - } -} diff --git a/Tiku.Api/Controllers/PlatformContentV2AuthoringController.cs b/Tiku.Api/Controllers/PlatformContentV2AuthoringController.cs new file mode 100644 index 0000000..51cb094 --- /dev/null +++ b/Tiku.Api/Controllers/PlatformContentV2AuthoringController.cs @@ -0,0 +1,112 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tiku.Api.Contracts; +using Tiku.Api.Security; +using Tiku.Application.Content; +using Tiku.Application.Security; + +namespace Tiku.Api.Controllers; + +[ApiController] +[Tags("平台端-公共内容 V2")] +[Authorize(Policy = TikuPolicies.PlatformBackofficeBootstrap)] +[Authorize(Policy = BackendPermissions.PlatformQuestionBankManage)] +[Produces("application/json")] +[Route("api/platform/content-v2")] +public sealed class PlatformContentV2AuthoringController( + IContentV2AuthoringService service, + IContentReleaseCompiler releaseCompiler, + IPlatformContentActorResolver actorResolver, + ICurrentUser currentUser) : ControllerBase +{ + [HttpPost("questions/duplicate-search")] + [EndpointSummary("查询平台公共题范围内的精确和相似题")] + public async Task SearchDuplicates( + QuestionDuplicateSearchDto request, + CancellationToken cancellationToken) + { + return await service.SearchDuplicatesAsync( + await ActorAsync(cancellationToken), + request.Draft.ToCommand(), + request.Limit, + cancellationToken); + } + + [HttpPost("questions/execute")] + [EndpointSummary("按审核决策录入平台公共题")] + public async Task Execute( + ExecuteQuestionAuthoringDto request, + CancellationToken cancellationToken) + { + return await service.ExecuteAsync( + await ActorAsync(cancellationToken), request.ToCommand(), cancellationToken); + } + + [HttpPost("questions/{questionAssetId:guid}/revisions")] + [EndpointSummary("为平台公共题创建不可变 Revision")] + public async Task CreateRevision( + Guid questionAssetId, + CreateQuestionRevisionV2Dto request, + CancellationToken cancellationToken) + { + return await service.CreateRevisionAsync( + await ActorAsync(cancellationToken), + questionAssetId, + request.Draft.ToCommand(), + request.PublishAsset, + cancellationToken); + } + + [HttpPost("questions/placements")] + [EndpointSummary("把平台公共题放置到大纲并配置目标规则")] + public async Task CreatePlacement( + CreateQuestionPlacementDto request, + CancellationToken cancellationToken) + { + return await service.CreatePlacementAsync( + await ActorAsync(cancellationToken), request.ToCommand(), cancellationToken); + } + + [HttpPost("imports/questions/preview")] + [EndpointSummary("预览平台公共题批量导入")] + public async Task PreviewImport( + PreviewQuestionImportV2Dto request, + CancellationToken cancellationToken) + { + return await service.PreviewImportAsync( + await ActorAsync(cancellationToken), + request.Rows.Select(row => (row.RowKey, row.Draft.ToCommand())).ToArray(), + cancellationToken); + } + + [HttpPost("imports/questions/execute")] + [EndpointSummary("执行平台公共题批量导入决策")] + public async Task ExecuteImport( + ExecuteQuestionImportV2Dto request, + CancellationToken cancellationToken) + { + return await service.ExecuteImportAsync( + await ActorAsync(cancellationToken), + request.Rows.Select(row => new ExecuteQuestionImportRow(row.RowKey, row.Command.ToCommand())).ToArray(), + cancellationToken); + } + + [HttpPost("releases/publish")] + [EndpointSummary("校验并发布平台公共不可变 Release")] + public async Task PublishRelease( + PublishContentReleaseDto request, + CancellationToken cancellationToken) + { + var actor = await ActorAsync(cancellationToken); + return await releaseCompiler.PublishAsync( + new PublishContentReleaseCommand(actor.TenantId, actor.UserId, request.CurriculumVersionId, request.Name), + cancellationToken); + } + + private Task ActorAsync(CancellationToken cancellationToken) + { + return currentUser.UserId is { } userId + ? actorResolver.ResolveAsync(userId, cancellationToken) + : throw new ContentV2Exception("platform_access_denied", "The platform actor was not resolved."); + } +} diff --git a/Tiku.Api/Controllers/PlatformQuestionBanksController.cs b/Tiku.Api/Controllers/PlatformQuestionBanksController.cs deleted file mode 100644 index 8acf2d1..0000000 --- a/Tiku.Api/Controllers/PlatformQuestionBanksController.cs +++ /dev/null @@ -1,170 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Tiku.Api.Contracts; -using Tiku.Application.Assets; -using Tiku.Application.PlatformAdmin; -using Tiku.Application.Security; - -namespace Tiku.Api.Controllers; - -[ApiController] -[Tags("平台端-公共题库")] -[Authorize(Policy = BackendPermissions.PlatformQuestionBankManage)] -[Produces("application/json")] -[Route("api/platform/question-banks")] -public sealed class PlatformQuestionBanksController( - IPlatformQuestionBankCatalogService catalogService, - IPlatformQuestionBankNodeService nodeService, - IPlatformQuestionAdministrationService questionService, - IPlatformQuestionImportService importService, - IPlatformQuestionAssetService assetService, - ICurrentUser currentUser) : ControllerBase -{ - [HttpGet] - [EndpointSummary("查询平台公共题库")] - public async Task>> GetBanks( - [FromQuery] string? keyword, - [FromQuery] string? status, - CancellationToken cancellationToken) - { - return Ok(await catalogService.GetBanksAsync(ResolveActor(), - new PlatformQuestionBankFilter(Keyword: keyword, Status: status), cancellationToken)); - } - - [HttpPut] - [EndpointSummary("新增或更新平台公共题库")] - public async Task> UpsertBank( - UpsertPlatformQuestionBankCommand request, - CancellationToken cancellationToken) - { - return Ok(await catalogService.UpsertBankAsync(ResolveActor(), request, cancellationToken)); - } - - [HttpPost("{bankId:guid}/archive")] - [EndpointSummary("归档平台公共题库")] - public async Task> ArchiveBank(Guid bankId, - CancellationToken cancellationToken) - { - return Ok(await catalogService.ArchiveBankAsync(ResolveActor(), bankId, cancellationToken)); - } - - [HttpGet("{bankId:guid}/nodes")] - [EndpointSummary("查询公共题库内容结构")] - public async Task>> GetNodes(Guid bankId, - CancellationToken cancellationToken) - { - return Ok(await nodeService.GetNodesAsync(ResolveActor(), bankId, cancellationToken)); - } - - [HttpPut("nodes")] - [EndpointSummary("新增或更新公共题库内容节点")] - public async Task> UpsertNode( - UpsertPlatformQuestionBankNodeCommand request, - CancellationToken cancellationToken) - { - return Ok(await nodeService.UpsertNodeAsync(ResolveActor(), request, cancellationToken)); - } - - [HttpPost("nodes/batch")] - [EndpointSummary("批量创建公共题库章节或试卷")] - public async Task>> BatchCreateNodes( - BatchCreatePlatformQuestionBankNodesCommand request, - CancellationToken cancellationToken) - { - return Ok(await nodeService.BatchCreateNodesAsync(ResolveActor(), request, cancellationToken)); - } - - [HttpPost("nodes/{nodeId:guid}/archive")] - [EndpointSummary("归档公共题库内容节点")] - public async Task> ArchiveNode(Guid nodeId, - CancellationToken cancellationToken) - { - return Ok(await nodeService.ArchiveNodeAsync(ResolveActor(), nodeId, cancellationToken)); - } - - [HttpGet("questions")] - [EndpointSummary("分页查询公共题库题目")] - public async Task> GetQuestions( - [FromQuery] Guid questionBankId, - [FromQuery] Guid? contentNodeId, - [FromQuery] string? keyword, - [FromQuery] string? type, - [FromQuery] int? difficulty, - [FromQuery] string? status, - [FromQuery] int page = 1, - [FromQuery] int pageSize = 20, - CancellationToken cancellationToken = default) - { - return Ok(await questionService.GetQuestionsAsync(ResolveActor(), new PlatformQuestionBankFilter( - questionBankId, contentNodeId, keyword, type, difficulty, status, page, pageSize), cancellationToken)); - } - - [HttpPut("questions")] - [EndpointSummary("新增或更新公共题库题目并保留版本")] - public async Task> UpsertQuestion( - UpsertPlatformQuestionCommand request, - CancellationToken cancellationToken) - { - return Ok(await questionService.UpsertQuestionAsync(ResolveActor(), request, cancellationToken)); - } - - [HttpPost("questions/archive")] - [EndpointSummary("批量归档公共题库题目")] - public async Task> ArchiveQuestions( - ArchivePlatformQuestionsCommand request, - CancellationToken cancellationToken) - { - return Ok(await questionService.ArchiveQuestionsAsync(ResolveActor(), request, cancellationToken)); - } - - [HttpPost("imports/preview")] - [EndpointSummary("预检公共题库 JSON 导入")] - public async Task> PreviewImport( - PlatformQuestionImportCommand request, - CancellationToken cancellationToken) - { - return Ok(await importService.PreviewImportAsync(ResolveActor(), request, cancellationToken)); - } - - [HttpPost("imports")] - [EndpointSummary("执行公共题库 JSON 导入")] - public async Task> ExecuteImport( - PlatformQuestionImportCommand request, - CancellationToken cancellationToken) - { - return Ok(await importService.ExecuteImportAsync(ResolveActor(), request, cancellationToken)); - } - - [HttpGet("imports/{jobId:guid}")] - [EndpointSummary("查询公共题库导入结果")] - public async Task> GetImport(Guid jobId, CancellationToken cancellationToken) - { - return Ok(await importService.GetImportAsync(ResolveActor(), jobId, cancellationToken)); - } - - [HttpPost("assets/upload-sign")] - [EndpointSummary("签发公共题库图片上传地址")] - public async Task> SignAssetUpload( - AssetUploadSignDto request, - CancellationToken cancellationToken) - { - return Ok(await assetService.SignQuestionAssetUploadAsync(ResolveActor(), request.ToCommand(), cancellationToken)); - } - - [HttpPost("assets/upload-confirm")] - [EndpointSummary("确认公共题库图片上传结果")] - public async Task> ConfirmAssetUpload( - AssetUploadConfirmDto request, - CancellationToken cancellationToken) - { - return Ok(await assetService.ConfirmQuestionAssetUploadAsync(ResolveActor(), request.ToCommand(), - cancellationToken)); - } - - private PlatformAdminActor ResolveActor() - { - return currentUser.UserId is { } userId - ? new PlatformAdminActor(userId) - : throw new PlatformAdminException("无法识别当前平台员工。", "platform_access_denied"); - } -} diff --git a/Tiku.Api/Controllers/QuestionManagementController.cs b/Tiku.Api/Controllers/QuestionManagementController.cs deleted file mode 100644 index 072ef24..0000000 --- a/Tiku.Api/Controllers/QuestionManagementController.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Text.Json; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Tiku.Api.Contracts; -using Tiku.Api.Security; -using Tiku.Application.Assets; -using Tiku.Application.Catalog; -using Tiku.Application.Content; -using Tiku.Application.Jobs; -using Tiku.Application.Security; -using Tiku.Domain.Catalog; -using Tiku.Domain.Content; - -namespace Tiku.Api.Controllers; - -[ApiController] -[Tags("租户端-内容直接管理")] -[Produces("application/json")] -[Route("api/tenant/content")] -public sealed class QuestionManagementController( - IQuestionManagementService service, - DirectContentActorResolver actorResolver) : ControllerBase -{ - [HttpPost("questions")] - [Authorize(Policy = BackendPermissions.TenantContentManage)] - [Authorize(Policy = TikuPolicies.TenantAllDataScope)] - [RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)] - [EndpointSummary("创建题目及首个版本")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task>> CreateQuestion( - DirectQuestionWriteDto request, - CancellationToken cancellationToken) - { - return Ok(await service.CreateQuestionAsync(actorResolver.Resolve(), request.ToCommand(true), - cancellationToken)); - } - - [HttpPatch("questions")] - [Authorize(Policy = BackendPermissions.TenantContentManage)] - [Authorize(Policy = TikuPolicies.TenantAllDataScope)] - [RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)] - [EndpointSummary("更新题目并可选择创建新版本")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task>> UpdateQuestion( - DirectQuestionWriteDto request, - CancellationToken cancellationToken) - { - return Ok(await service.UpdateQuestionAsync(actorResolver.Resolve(), request.ToCommand(false), - cancellationToken)); - } -} diff --git a/Tiku.Api/Controllers/StudentLearningContextController.cs b/Tiku.Api/Controllers/StudentLearningContextController.cs new file mode 100644 index 0000000..6a70fe7 --- /dev/null +++ b/Tiku.Api/Controllers/StudentLearningContextController.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tiku.Api.Contracts; +using Tiku.Api.Security; +using Tiku.Application.Learning; +using Tiku.Application.Security; + +namespace Tiku.Api.Controllers; + +[ApiController] +[Tags("学生端-学习上下文")] +[Authorize(Policy = TikuPolicies.CurrentTenantMember)] +[RequireSaasFeature(SaasFeatureCatalog.Practice)] +[Produces("application/json")] +[Route("api/student")] +public sealed class StudentLearningContextController( + IStudentLearningTargetService service, + IStudentLearningCatalogService catalog, + LearningActorResolver actorResolver) : ControllerBase +{ + [HttpGet("learning-context")] + [EndpointSummary("查询学生可用业务与当前学习目标")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task GetContext(CancellationToken cancellationToken) + { + return service.GetContextAsync(actorResolver.Resolve(), cancellationToken); + } + + [HttpGet("learning-targets")] + [EndpointSummary("查询当前业务可选目标")] + [ProducesResponseType>(StatusCodes.Status200OK)] + public Task> GetTargets( + [FromQuery] Guid businessLineId, + CancellationToken cancellationToken) + { + return service.GetTargetsAsync(actorResolver.Resolve(), businessLineId, cancellationToken); + } + + [HttpGet("catalog/resources")] + [EndpointSummary("查询由当前套餐、目标和班级授权编译出的认证学习目录")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task GetCatalog( + [FromQuery] Guid businessLineId, + CancellationToken cancellationToken) + { + return catalog.GetAsync(actorResolver.Resolve(), businessLineId, cancellationToken); + } + + [HttpPut("learning-targets/{businessLineId:guid}")] + [EndpointSummary("变更当前业务的主目标和备选目标")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public Task ChangeTargets( + Guid businessLineId, + ChangeLearningTargetsDto request, + CancellationToken cancellationToken) + { + return service.ChangeTargetsAsync( + actorResolver.Resolve(), + businessLineId, + request.ToCommand(), + cancellationToken); + } +} diff --git a/Tiku.Api/Controllers/TaxonomyController.cs b/Tiku.Api/Controllers/TaxonomyController.cs deleted file mode 100644 index 0befad4..0000000 --- a/Tiku.Api/Controllers/TaxonomyController.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Tiku.Api.Contracts; -using Tiku.Api.Security; -using Tiku.Application.Catalog; -using Tiku.Application.Security; - -namespace Tiku.Api.Controllers; - -[ApiController] -[Tags("租户端-分类管理")] -[Authorize(Policy = TikuPolicies.CurrentTenantMember)] -[Produces("application/json")] -[Route("api/tenant/taxonomy/nodes")] -public sealed class TaxonomyController( - ITenantContext tenantContext, - ITaxonomyService taxonomyService) : ControllerBase -{ - [HttpGet] - [RequireSaasFeature(SaasFeatureCatalog.Practice)] - [EndpointSummary("查询租户分类节点")] - public Task> List(CancellationToken cancellationToken) - { - return taxonomyService.ListAsync(RequireTenantId(), cancellationToken); - } - - [HttpPost] - [RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)] - [Authorize(Policy = BackendPermissions.TenantContentManage)] - [EndpointSummary("创建租户分类节点")] - public Task Create( - CreateTaxonomyNodeDto request, - CancellationToken cancellationToken) - { - return taxonomyService.CreateAsync(RequireTenantId(), request.ToCommand(), cancellationToken); - } - - private Guid RequireTenantId() - { - return tenantContext.TenantId ?? throw new InvalidOperationException("Tenant was not resolved."); - } -} \ No newline at end of file diff --git a/Tiku.Api/Controllers/TenantClassLearningAssignmentController.cs b/Tiku.Api/Controllers/TenantClassLearningAssignmentController.cs index 0b203aa..2c2bcc3 100644 --- a/Tiku.Api/Controllers/TenantClassLearningAssignmentController.cs +++ b/Tiku.Api/Controllers/TenantClassLearningAssignmentController.cs @@ -7,68 +7,82 @@ using Tiku.Application.Security; namespace Tiku.Api.Controllers; [ApiController] -[Tags("租户端-班级学习授权")] +[Tags("租户端-班级学习授权 V2")] [Authorize(Policy = BackendPermissions.TenantStudentManage)] [Produces("application/json")] -[Route("api/tenant/classes/learning-assignments")] +[Route("api/tenant/classes/{classId:guid}/learning-assignments")] public sealed class TenantClassLearningAssignmentController( - ILearningAccessAdministrationService service, + IV2LearningAccessAdministrationService service, TenantAdminActorResolver actorResolver) : ControllerBase { [HttpGet] - [EndpointSummary("查询班级题集和练习蓝图授权")] - public Task> Get( - [FromQuery] Guid classId, + [EndpointSummary("查询班级的 V2 题集和蓝图授权")] + public Task> Get( + Guid classId, CancellationToken cancellationToken) { var actor = actorResolver.Resolve(); - return service.GetClassAssignmentsAsync(actor.TenantId, classId, cancellationToken); + return service.GetClassGrantsAsync(actor.TenantId, classId, cancellationToken); } [HttpPut] - [EndpointSummary("新增或更新班级题集和练习蓝图授权")] - public Task Upsert( - UpsertClassContentAssignmentDto request, + [EndpointSummary("新增或更新班级的 V2 题集和蓝图授权")] + public Task Upsert( + Guid classId, + UpsertClassAssignmentGrantDto request, CancellationToken cancellationToken) { var actor = actorResolver.Resolve(); - return service.UpsertClassAssignmentAsync( - actor.TenantId, actor.UserId, request.ToCommand(), cancellationToken); + return service.UpsertClassGrantAsync( + actor.TenantId, + actor.UserId, + request.ToCommand(classId), + cancellationToken); } - [HttpPost("{assignmentId:guid}/revoke")] - [EndpointSummary("撤回班级学习授权并强制撤销活动会话")] - public Task Revoke( - Guid assignmentId, - RevokeClassContentAssignmentDto request, + [HttpPost("{grantId:guid}/revoke")] + [EndpointSummary("撤回班级 V2 学习授权;已开始会话继续使用锁定快照")] + public Task Revoke( + Guid classId, + Guid grantId, + RevokeClassAssignmentGrantDto request, CancellationToken cancellationToken) { var actor = actorResolver.Resolve(); - return service.RevokeClassAssignmentAsync( - actor.TenantId, actor.UserId, assignmentId, request.Reason, cancellationToken); + return service.RevokeClassGrantAsync( + actor.TenantId, + actor.UserId, + classId, + grantId, + request.Reason, + cancellationToken); } } [ApiController] -[Tags("租户端-学生学习授权")] +[Tags("租户端-学生学习目标 V2")] [Authorize(Policy = BackendPermissions.TenantStudentManage)] [Produces("application/json")] -[Route("api/tenant/students/{userId:guid}/learning/target-region")] +[Route("api/tenant/students/{userId:guid}/learning-targets/{businessLineId:guid}")] public sealed class TenantStudentLearningAccessController( - ILearningAccessAdministrationService service, + IStudentLearningTargetService service, TenantAdminActorResolver actorResolver) : ControllerBase { [HttpPut] - [EndpointSummary("管理员强制调整学生目标地区")] - public Task Override( + [EndpointSummary("管理员带原因强制调整学生复合考试目标")] + public Task Override( Guid userId, - OverrideStudentTargetRegionDto request, + Guid businessLineId, + OverrideStudentLearningTargetsDto request, CancellationToken cancellationToken) { var actor = actorResolver.Resolve(); - return service.OverrideTargetRegionAsync( - actor.TenantId, - request.ToCommand(userId, actor.UserId), + return service.OverrideTargetsAsync( + new LearningActor(actor.TenantId, userId), + businessLineId, + request.ToCommand(), + actor.UserId, + request.Reason, cancellationToken); } } diff --git a/Tiku.Api/Controllers/TenantContentCapabilitySet.cs b/Tiku.Api/Controllers/TenantContentCapabilitySet.cs index 36989c7..15c4a00 100644 --- a/Tiku.Api/Controllers/TenantContentCapabilitySet.cs +++ b/Tiku.Api/Controllers/TenantContentCapabilitySet.cs @@ -4,20 +4,12 @@ using Tiku.Application.Content; namespace Tiku.Api.Controllers; public sealed class TenantContentCapabilitySet( - IContentEntryManagementService contentEntries, - IContentNodeManagementService contentNodes, - IQuestionCollectionManagementService questionCollections, - IPracticeBlueprintManagementService practiceBlueprints, IAssetCatalogManagementService assetCatalog, IAssetUploadManagementService assetUploads, IAssetLifecycleManagementService assetLifecycle, IAssetAuditQueryService assetAudit, IAssetImportJobQueryService assetImports) { - internal IContentEntryManagementService ContentEntries { get; } = contentEntries; - internal IContentNodeManagementService ContentNodes { get; } = contentNodes; - internal IQuestionCollectionManagementService QuestionCollections { get; } = questionCollections; - internal IPracticeBlueprintManagementService PracticeBlueprints { get; } = practiceBlueprints; internal IAssetCatalogManagementService AssetCatalog { get; } = assetCatalog; internal IAssetUploadManagementService AssetUploads { get; } = assetUploads; internal IAssetLifecycleManagementService AssetLifecycle { get; } = assetLifecycle; diff --git a/Tiku.Api/Controllers/TenantContentController.cs b/Tiku.Api/Controllers/TenantContentController.cs index 3deadfb..3098c51 100644 --- a/Tiku.Api/Controllers/TenantContentController.cs +++ b/Tiku.Api/Controllers/TenantContentController.cs @@ -18,141 +18,6 @@ public sealed class TenantContentController( ICurrentUser currentUser, ITenantContext currentTenant) : ControllerBase { - [HttpGet("entries")] - [EndpointSummary("查询租户内容入口")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task>> GetEntries( - [FromQuery] ContentManagementQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await capabilities.ContentEntries.GetEntriesAsync( - ResolveContentActor(), - query.ToFilter(), - cancellationToken)); - } - - [HttpPost("entries")] - [EndpointSummary("创建或更新内容入口")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task>> UpsertEntry( - UpsertContentEntryDto request, - CancellationToken cancellationToken) - { - return Ok(await capabilities.ContentEntries.UpsertEntryAsync( - ResolveContentActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("nodes")] - [EndpointSummary("查询租户内容节点")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task>> GetNodes( - [FromQuery] ContentManagementQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await capabilities.ContentNodes.GetNodesAsync( - ResolveContentActor(), - query.ToFilter(), - cancellationToken)); - } - - [HttpPost("nodes")] - [EndpointSummary("创建或更新内容节点")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task>> UpsertNode( - UpsertContentNodeDto request, - CancellationToken cancellationToken) - { - return Ok(await capabilities.ContentNodes.UpsertNodeAsync( - ResolveContentActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("question-collections")] - [EndpointSummary("查询租户题集")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task>> GetQuestionCollections( - [FromQuery] ContentManagementQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await capabilities.QuestionCollections.GetCollectionsAsync( - ResolveContentActor(), - query.ToFilter(), - cancellationToken)); - } - - [HttpPost("question-collections")] - [EndpointSummary("创建或更新题集")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task>> UpsertQuestionCollection( - UpsertQuestionCollectionDto request, - CancellationToken cancellationToken) - { - return Ok(await capabilities.QuestionCollections.UpsertCollectionAsync( - ResolveContentActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpPost("question-collections/items/replace")] - [EndpointSummary("替换题集题目")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> ReplaceQuestionCollectionItems( - ReplaceCollectionItemsDto request, - CancellationToken cancellationToken) - { - return Ok(await capabilities.QuestionCollections.ReplaceCollectionItemsAsync( - ResolveContentActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("practice-blueprints")] - [EndpointSummary("查询练习蓝图")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task>> GetPracticeBlueprints( - [FromQuery] ContentManagementQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await capabilities.PracticeBlueprints.GetPracticeBlueprintsAsync( - ResolveContentActor(), - query.ToFilter(), - cancellationToken)); - } - - [HttpPost("practice-blueprints")] - [EndpointSummary("创建或更新练习蓝图")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task>> UpsertPracticeBlueprint( - UpsertPracticeBlueprintDto request, - CancellationToken cancellationToken) - { - return Ok(await capabilities.PracticeBlueprints.UpsertPracticeBlueprintAsync( - ResolveContentActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("imports/field-mapping")] - [EndpointSummary("查询导入字段映射")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetImportFieldMapping([FromQuery] ImportTemplateQueryDto query) - { - _ = ResolveContentActor(); - return Ok(capabilities.PracticeBlueprints.GetImportFieldMapping(query.ImportType)); - } - - [HttpGet("imports/templates")] - [EndpointSummary("获取内容导入模板")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetImportTemplate([FromQuery] ImportTemplateQueryDto query) - { - _ = ResolveContentActor(); - return Ok(capabilities.PracticeBlueprints.GetImportTemplate(query.ImportType, query.Format)); - } - [HttpGet("assets")] [EndpointSummary("查询租户内容资产")] [ProducesResponseType>(StatusCodes.Status200OK)] @@ -311,12 +176,4 @@ public sealed class TenantContentController( return new AssetManagementActor(currentTenant.TenantId.Value, currentUser.UserId.Value); } - private ContentManagementActor ResolveContentActor() - { - if (currentTenant.TenantId is null || currentUser.UserId is null) - throw new ContentManagementException("Tenant content actor was not resolved.", - "tenant_content_access_denied"); - - return new ContentManagementActor(currentTenant.TenantId.Value, currentUser.UserId.Value); - } } diff --git a/Tiku.Api/Controllers/TenantNotFoundException.cs b/Tiku.Api/Controllers/TenantNotFoundException.cs new file mode 100644 index 0000000..5716349 --- /dev/null +++ b/Tiku.Api/Controllers/TenantNotFoundException.cs @@ -0,0 +1,8 @@ +namespace Tiku.Api.Controllers; + +public sealed class TenantNotFoundException : Exception +{ + public TenantNotFoundException() : base("Tenant not found.") + { + } +} diff --git a/Tiku.Api/Modules/Student/Content/Errors/ContentExceptionProblemDetailsMapper.cs b/Tiku.Api/Modules/Student/Content/Errors/ContentExceptionProblemDetailsMapper.cs index d8073b3..5e9eb6b 100644 --- a/Tiku.Api/Modules/Student/Content/Errors/ContentExceptionProblemDetailsMapper.cs +++ b/Tiku.Api/Modules/Student/Content/Errors/ContentExceptionProblemDetailsMapper.cs @@ -15,7 +15,7 @@ internal sealed class ContentExceptionProblemDetailsMapper : ExceptionProblemDet typeof(ContentNavigationNotFoundException), typeof(QuestionBankRequiredFieldException), typeof(QuestionBankNotFoundException), typeof(AssetAccessException), typeof(AssetManagementException), typeof(VideoPlaybackException), typeof(ContentManagementException), typeof(ScorelineQueryException), - typeof(ProfileException) + typeof(ProfileException), typeof(ContentV2Exception) ]; public override bool TryMap(Exception exception, out ExceptionProblemDetailsMapping mapping) @@ -38,6 +38,7 @@ internal sealed class ContentExceptionProblemDetailsMapper : ExceptionProblemDet AssetManagementException asset => Mapped(asset, AssetManagementStatus(asset.Code), asset.Code, out mapping), VideoPlaybackException video => Mapped(video, VideoStatus(video.Code), video.Code, out mapping), ContentManagementException content => Mapped(content, ContentStatus(content.Code), content.Code, out mapping), + ContentV2Exception content => Mapped(content, ContentV2Status(content.Code), content.Code, out mapping), ScorelineQueryException scoreline => Mapped(scoreline, StatusCodes.Status400BadRequest, scoreline.Code, out mapping), ProfileException profile => Mapped(profile, ProfileStatus(profile.Code), profile.Code, out mapping), @@ -85,4 +86,13 @@ internal sealed class ContentExceptionProblemDetailsMapper : ExceptionProblemDet "major_not_found" => StatusCodes.Status404NotFound, _ => StatusCodes.Status400BadRequest }; + + private static int ContentV2Status(string code) => code switch + { + "question_asset_owner_forbidden" => StatusCodes.Status403Forbidden, + "question_exact_duplicate_exists" => StatusCodes.Status409Conflict, + _ when code.EndsWith("_not_found", StringComparison.Ordinal) || + code.EndsWith("_missing", StringComparison.Ordinal) => StatusCodes.Status404NotFound, + _ => StatusCodes.Status400BadRequest + }; } diff --git a/Tiku.Api/Modules/Student/Learning/Errors/LearningExceptionProblemDetailsMapper.cs b/Tiku.Api/Modules/Student/Learning/Errors/LearningExceptionProblemDetailsMapper.cs index a0f6722..1555339 100644 --- a/Tiku.Api/Modules/Student/Learning/Errors/LearningExceptionProblemDetailsMapper.cs +++ b/Tiku.Api/Modules/Student/Learning/Errors/LearningExceptionProblemDetailsMapper.cs @@ -20,7 +20,7 @@ internal sealed class LearningExceptionProblemDetailsMapper : ExceptionProblemDe validation.Code, out mapping), LearningResourceNotFoundException notFound => Mapped(notFound, StatusCodes.Status404NotFound, notFound.Code, out mapping), - LearningAccessException access => Mapped(access, StatusCodes.Status403Forbidden, + LearningAccessException access => Mapped(access, AccessStatus(access.Code), access.Code, out mapping), LearningAccessDeniedException => Mapped(exception, StatusCodes.Status403Forbidden, "learning_access_denied", out mapping), @@ -36,4 +36,11 @@ internal sealed class LearningExceptionProblemDetailsMapper : ExceptionProblemDe "practice_session_expired" => StatusCodes.Status409Conflict, _ => StatusCodes.Status400BadRequest }; + + private static int AccessStatus(string code) => code switch + { + "student_target_change_cooldown" or "student_target_policy_limit_exceeded" or + "student_target_product_limit_exceeded" => StatusCodes.Status409Conflict, + _ => StatusCodes.Status403Forbidden + }; } diff --git a/Tiku.Application/Catalog/TaxonomyModels.cs b/Tiku.Application/Catalog/TaxonomyModels.cs deleted file mode 100644 index 0274014..0000000 --- a/Tiku.Application/Catalog/TaxonomyModels.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text.Json; -using Tiku.Domain.Catalog; -using Tiku.Domain.Content; - -namespace Tiku.Application.Catalog; - -public sealed record TaxonomyNodeItem( - Guid Id, - QuestionSource Source, - Guid? ParentId, - QuestionSource? ParentSource, - TaxonomyNodeType NodeType, - string Code, - string Name, - string? Path, - int Depth, - int SortOrder, - JsonElement Metadata); - -public sealed record CreateTaxonomyNodeCommand( - Guid? ParentId, - QuestionSource? ParentSource, - TaxonomyNodeType NodeType, - string Code, - string Name, - int SortOrder, - JsonElement Metadata); - -public interface ITaxonomyService -{ - Task> ListAsync(Guid tenantId, CancellationToken cancellationToken = default); - - Task CreateAsync( - Guid tenantId, - CreateTaxonomyNodeCommand command, - CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/Tiku.Application/Content/ContentManagementException.cs b/Tiku.Application/Content/ContentManagementException.cs new file mode 100644 index 0000000..b8ce3dc --- /dev/null +++ b/Tiku.Application/Content/ContentManagementException.cs @@ -0,0 +1,8 @@ +namespace Tiku.Application.Content; + +public sealed record ContentManagementResult(TItem Item); + +public sealed class ContentManagementException(string message, string code) : Exception(message) +{ + public string Code { get; } = code; +} diff --git a/Tiku.Application/Content/ContentManagementModels.cs b/Tiku.Application/Content/ContentManagementModels.cs deleted file mode 100644 index 11b99c2..0000000 --- a/Tiku.Application/Content/ContentManagementModels.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System.Text.Json; -using Tiku.Application.QuestionBanks; -using Tiku.Domain.Content; - -namespace Tiku.Application.Content; - -public sealed record ContentManagementActor(Guid TenantId, Guid UserId); - -public sealed record ContentManagementFilter( - Guid? RegionId = null, - Guid? EntryId = null, - Guid? NodeId = null, - Guid? CollectionId = null, - string? ParentId = null, - string? EntryType = null, - string? CollectionType = null, - string? Mode = null, - string? MarkerType = null, - string? Keyword = null, - bool IncludeInactive = false, - int? Limit = null); - -public sealed record UpsertContentEntryCommand( - Guid? Id, - Guid? RegionId, - string? LegacyId, - string? EntryKey, - string Name, - string? EntryType, - string? Icon, - string? Route, - string? Description, - string? Visibility, - JsonElement AccessRules, - JsonElement LayoutConfig, - int? Order, - bool? IsActive); - -public sealed record UpsertContentNodeCommand( - Guid? Id, - Guid EntryId, - Guid? RegionId, - Guid? ParentId, - string? LegacyId, - string? NodeKey, - string Name, - string? NodeType, - string? MarkerType, - JsonElement MarkerConfig, - int? Order, - bool? IsActive, - bool? IsSelectable, - bool? IsLeaf, - JsonElement AccessRules, - JsonElement Metadata); - -public sealed record UpsertQuestionCollectionCommand( - Guid? Id, - Guid? RegionId, - Guid? EntryId, - Guid? NodeId, - Guid? SubjectId, - Guid? CategoryId, - Guid? QuestionBankId, - string? LegacyId, - string Name, - string? CollectionType, - string? SourceType, - JsonElement Filters, - decimal? TotalScore, - int? DurationMinutes, - string? Status, - int? Order, - JsonElement AccessRules, - JsonElement Metadata); - -public sealed record ReplaceCollectionItemsCommand( - Guid CollectionId, - IReadOnlyCollection Questions); - -public sealed record CollectionQuestionCommand( - QuestionLocator Locator, - string? SectionKey, - int? Order, - decimal? Score, - bool? Required, - JsonElement Metadata); - -public sealed record UpsertPracticeBlueprintCommand( - Guid? Id, - Guid? RegionId, - Guid? EntryId, - Guid? NodeId, - Guid? CollectionId, - string? LegacyId, - string Name, - string? Mode, - string? AssemblyType, - int? QuestionLimit, - int? DurationMinutes, - decimal? TotalScore, - decimal? PassScore, - JsonElement Sections, - JsonElement Rules, - JsonElement AccessRules, - string? Status, - int? Order); - -public sealed record ContentEntryManagementItem( - Guid Id, - Guid? RegionId, - string? LegacyId, - string EntryKey, - string Name, - ContentEntryType EntryType, - string? Icon, - string? Route, - string? Description, - ContentVisibility Visibility, - JsonElement AccessRules, - JsonElement LayoutConfig, - int Order, - bool IsActive, - DateTimeOffset CreatedAt, - DateTimeOffset? UpdatedAt); - -public sealed record ContentNodeManagementItem( - Guid Id, - Guid EntryId, - Guid? RegionId, - Guid? ParentId, - string? LegacyId, - string? NodeKey, - string Name, - ContentNodeType NodeType, - ContentMarkerType? MarkerType, - JsonElement MarkerConfig, - string? Path, - int Depth, - int Order, - bool IsActive, - bool IsSelectable, - bool IsLeaf, - JsonElement AccessRules, - JsonElement Metadata, - DateTimeOffset CreatedAt, - DateTimeOffset? UpdatedAt); - -public sealed record QuestionCollectionManagementItem( - Guid Id, - Guid? RegionId, - Guid? EntryId, - Guid? NodeId, - Guid? SubjectId, - Guid? CategoryId, - Guid? QuestionBankId, - string? LegacyId, - string Name, - QuestionCollectionType CollectionType, - QuestionCollectionSourceType SourceType, - JsonElement Filters, - int QuestionCount, - decimal? TotalScore, - int? DurationMinutes, - ContentStatus Status, - int Order, - JsonElement AccessRules, - JsonElement Metadata, - DateTimeOffset CreatedAt, - DateTimeOffset? UpdatedAt); - -public sealed record QuestionCollectionItemManagementItem( - Guid Id, - Guid CollectionId, - Guid QuestionId, - QuestionLocator Locator, - string? SectionKey, - int Order, - decimal? Score, - bool Required, - JsonElement Metadata); - -public sealed record PracticeBlueprintManagementItem( - Guid Id, - Guid? RegionId, - Guid? EntryId, - Guid? NodeId, - Guid? CollectionId, - string? LegacyId, - string Name, - PracticeMode Mode, - PracticeAssemblyType AssemblyType, - int? QuestionLimit, - int? DurationMinutes, - decimal? TotalScore, - decimal? PassScore, - JsonElement Sections, - JsonElement Rules, - JsonElement AccessRules, - ContentStatus Status, - int Order, - DateTimeOffset CreatedAt, - DateTimeOffset? UpdatedAt); - -public sealed record ContentManagementResult(TItem Item); - -public sealed record CollectionItemsReplaceResult( - Guid CollectionId, - int QuestionCount, - IReadOnlyCollection Items); - -public sealed record ImportFieldSpec( - string Field, - string Label, - bool Required, - IReadOnlyCollection Aliases, - string Description, - JsonElement Example); - -public sealed record ImportFieldMappingItem( - string ImportType, - string Title, - string Description, - IReadOnlyCollection Fields, - IReadOnlyCollection RequiredFields); - -public sealed record ImportTemplateItem( - string ImportType, - string Format, - string FileName, - string MimeType, - string ContentBase64, - string ContentPreview, - IReadOnlyCollection Fields); - -public sealed class ContentManagementException(string message, string code) : Exception(message) -{ - public string Code { get; } = code; -} \ No newline at end of file diff --git a/Tiku.Application/Content/ContentNavigationQueryModels.cs b/Tiku.Application/Content/ContentNavigationQueryModels.cs deleted file mode 100644 index 2a8c44d..0000000 --- a/Tiku.Application/Content/ContentNavigationQueryModels.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System.Text.Json; -using Tiku.Domain.Content; - -namespace Tiku.Application.Content; - -public sealed record ContentNavigationFilter( - Guid TenantId, - Guid? RegionId = null, - Guid? EntryId = null, - Guid? NodeId = null, - Guid? CollectionId = null, - Guid? ParentId = null, - bool ParentWasSpecified = false, - bool ParentIsRoot = false, - string? EntryType = null, - string? CollectionType = null, - string? Mode = null, - string? MarkerType = null, - string? Keyword = null, - bool IncludeHidden = false, - bool IncludeInactive = false, - int? Limit = null, - IReadOnlyCollection? AllowedContentSliceIds = null, - IReadOnlyCollection? AllowedRegionIds = null); - -public sealed record ContentEntryCatalogItem( - Guid Id, - Guid? RegionId, - string? LegacyId, - string EntryKey, - string Name, - ContentEntryType EntryType, - string? Icon, - string? Route, - string? Description, - ContentVisibility Visibility, - JsonElement AccessRules, - JsonElement LayoutConfig, - int Order); - -public sealed record ContentNodeCatalogItem( - Guid Id, - Guid EntryId, - Guid? RegionId, - Guid? ParentId, - string? LegacyId, - string? NodeKey, - string Name, - ContentNodeType NodeType, - ContentMarkerType? MarkerType, - JsonElement MarkerConfig, - string? Path, - int Depth, - int Order, - bool IsSelectable, - bool IsLeaf, - JsonElement AccessRules, - JsonElement Metadata); - -public sealed record QuestionCollectionCatalogItem( - Guid Id, - Guid? RegionId, - Guid? EntryId, - Guid? NodeId, - Guid? SubjectId, - Guid? CategoryId, - Guid? QuestionBankId, - string? LegacyId, - string Name, - QuestionCollectionType CollectionType, - QuestionCollectionSourceType SourceType, - JsonElement Filters, - int QuestionCount, - decimal? TotalScore, - int? DurationMinutes, - ContentStatus Status, - int Order, - JsonElement AccessRules, - JsonElement Metadata); - -public sealed record PracticeBlueprintCatalogItem( - Guid Id, - Guid? RegionId, - Guid? EntryId, - Guid? NodeId, - Guid? CollectionId, - string? LegacyId, - string Name, - PracticeMode Mode, - PracticeAssemblyType AssemblyType, - int? QuestionLimit, - int? DurationMinutes, - decimal? TotalScore, - decimal? PassScore, - JsonElement Sections, - JsonElement Rules, - JsonElement AccessRules, - ContentStatus Status, - int Order); - -public sealed record CollectionQuestionCatalogItem( - Guid Id, - string? LegacyId, - Guid? EntryId, - Guid? ContentNodeId, - Guid? PrimaryCollectionId, - Guid? SubjectId, - Guid? CategoryId, - Guid? NodeId, - string Type, - string? TypeLabel, - int? Difficulty, - JsonElement Tags, - JsonElement ExamMarkers, - string? MediaUrl, - bool HasVideoExplanation, - string? SectionKey, - decimal? Score, - int Order, - Guid? VersionId, - string? Content, - JsonElement Options, - string? CodeLang, - string? CodeTemplate); diff --git a/Tiku.Application/Content/ContentV2AuthoringModels.cs b/Tiku.Application/Content/ContentV2AuthoringModels.cs new file mode 100644 index 0000000..d5185f1 --- /dev/null +++ b/Tiku.Application/Content/ContentV2AuthoringModels.cs @@ -0,0 +1,152 @@ +using System.Text.Json; +using Tiku.Domain.Content; + +namespace Tiku.Application.Content; + +public sealed record ContentAuthorActor(Guid TenantId, Guid UserId); + +public interface IPlatformContentActorResolver +{ + Task ResolveAsync(Guid userId, CancellationToken cancellationToken = default); +} + +public sealed record QuestionRevisionDraft( + string QuestionType, + string? TypeLabel, + int? Difficulty, + string? Content, + JsonElement Options, + int? CorrectOptionIndex, + JsonElement CorrectOptionIndices, + string? AnswerText, + string? Explanation, + JsonElement SubQuestions, + string? CodeLang, + string? CodeTemplate); + +public sealed record QuestionDuplicateCandidate( + Guid QuestionAssetOwnerTenantId, + Guid QuestionAssetId, + Guid QuestionRevisionId, + string Source, + bool ExactDeliveryMatch, + double PromptSimilarity, + string QuestionType, + string? Content, + int? Difficulty); + +public sealed record QuestionDuplicateSearchResult( + string DeliveryFingerprint, + IReadOnlyCollection Candidates); + +public enum QuestionImportDecision +{ + ReusePlatform, + ReuseTenant, + CreateNew, + CreateVariant, + Skip, + ManualReview +} + +public sealed record ExecuteQuestionAuthoringCommand( + QuestionImportDecision Decision, + QuestionRevisionDraft Draft, + Guid? ReuseQuestionAssetId = null, + Guid? FamilyId = null, + bool PublishAsset = false); + +public sealed record QuestionAuthoringResult( + QuestionImportDecision Decision, + Guid? QuestionAssetOwnerTenantId, + Guid? QuestionAssetId, + Guid? QuestionRevisionId, + string DeliveryFingerprint, + string Status); + +public sealed record QuestionImportPreviewRow( + string RowKey, + string DeliveryFingerprint, + QuestionImportDecision SuggestedDecision, + IReadOnlyCollection Candidates); + +public sealed record QuestionImportPreviewResult( + int RowCount, + IReadOnlyCollection Rows); + +public sealed record ExecuteQuestionImportRow( + string RowKey, + ExecuteQuestionAuthoringCommand Command); + +public sealed record QuestionImportExecutionRow( + string RowKey, + QuestionAuthoringResult Result); + +public sealed record QuestionImportExecutionResult( + int RowCount, + IReadOnlyCollection Rows); + +public sealed record PlacementConditionCommand( + Guid TargetDimensionDefinitionId, + Guid TargetNodeId, + TargetRuleOperator Operator); + +public sealed record PlacementRuleGroupCommand( + int GroupOrder, + IReadOnlyCollection Conditions); + +public sealed record CreateQuestionPlacementCommand( + Guid QuestionAssetOwnerTenantId, + Guid QuestionAssetId, + Guid CurriculumVersionId, + Guid CurriculumNodeId, + Guid AssessmentPolicyVersionId, + IReadOnlyCollection KnowledgeConceptIds, + IReadOnlyCollection RuleGroups, + int? DifficultyOverride = null, + bool Activate = false); + +public sealed record QuestionPlacementItem( + Guid Id, + Guid QuestionAssetOwnerTenantId, + Guid QuestionAssetId, + Guid CurriculumVersionId, + Guid CurriculumNodeId, + Guid AssessmentPolicyVersionId, + string Status); + +public interface IContentV2AuthoringService +{ + Task SearchDuplicatesAsync( + ContentAuthorActor actor, + QuestionRevisionDraft draft, + int limit = 20, + CancellationToken cancellationToken = default); + + Task ExecuteAsync( + ContentAuthorActor actor, + ExecuteQuestionAuthoringCommand command, + CancellationToken cancellationToken = default); + + Task CreateRevisionAsync( + ContentAuthorActor actor, + Guid questionAssetId, + QuestionRevisionDraft draft, + bool publishAsset, + CancellationToken cancellationToken = default); + + Task CreatePlacementAsync( + ContentAuthorActor actor, + CreateQuestionPlacementCommand command, + CancellationToken cancellationToken = default); + + Task PreviewImportAsync( + ContentAuthorActor actor, + IReadOnlyCollection<(string RowKey, QuestionRevisionDraft Draft)> rows, + CancellationToken cancellationToken = default); + + Task ExecuteImportAsync( + ContentAuthorActor actor, + IReadOnlyCollection rows, + CancellationToken cancellationToken = default); +} diff --git a/Tiku.Application/Content/ContentV2Models.cs b/Tiku.Application/Content/ContentV2Models.cs new file mode 100644 index 0000000..0cd44ca --- /dev/null +++ b/Tiku.Application/Content/ContentV2Models.cs @@ -0,0 +1,93 @@ +using Tiku.Domain.Content; + +namespace Tiku.Application.Content; + +public sealed record PublishContentReleaseCommand( + Guid TenantId, + Guid ActorUserId, + Guid CurriculumVersionId, + string Name); + +public sealed record ContentReleaseCompilationResult( + Guid ContentReleaseId, + int ReleaseNo, + int PlacementCount, + int QuestionCount, + int AudienceSegmentCount, + int TargetProfileCount, + string SourceFingerprint, + DateTimeOffset PublishedAt); + +public interface IContentReleaseCompiler +{ + Task PublishAsync( + PublishContentReleaseCommand command, + CancellationToken cancellationToken = default); +} + +public sealed record ContentCandidateQuery( + Guid ContentOwnerTenantId, + IReadOnlyCollection ContentReleaseIds, + IReadOnlyCollection AudienceSegmentIds, + Guid? CurriculumNodeId, + IReadOnlyCollection? CollectionReleaseIds, + int Limit, + int Seed, + string? QuestionType = null, + int? Difficulty = null); + +public sealed record ContentCandidateItem( + Guid ContentReleaseQuestionId, + Guid ContentOwnerTenantId, + Guid ContentReleaseId, + Guid AudienceSegmentId, + Guid CurriculumNodeId, + Guid QuestionPlacementId, + Guid QuestionAssetOwnerTenantId, + Guid QuestionAssetId, + Guid QuestionRevisionId, + Guid AssessmentPolicyVersionId, + int Ordinal, + int? Difficulty, + string QuestionType); + +public interface IContentCandidateReader +{ + Task> GetCandidatesAsync( + ContentCandidateQuery query, + CancellationToken cancellationToken = default); +} + +public sealed record ApplyPlatformContentPackageCommand( + Guid PackageOwnerTenantId, + Guid PlatformContentPackageVersionId, + string PackageCode, + string CellId, + long EventSequence, + string ContentHash); + +public enum CellPackageApplyStatus +{ + Applied, + Duplicate, + IgnoredOutOfOrder +} + +public sealed record CellPackageApplyResult( + CellPackageApplyStatus Status, + Guid PlatformContentPackageVersionId, + string CellId, + long EventSequence, + string ContentHash); + +public interface IPlatformContentPackageProjectionService +{ + Task ApplyAsync( + ApplyPlatformContentPackageCommand command, + CancellationToken cancellationToken = default); +} + +public sealed class ContentV2Exception(string code, string message) : Exception(message) +{ + public string Code { get; } = code; +} diff --git a/Tiku.Application/Content/ContentV2Rules.cs b/Tiku.Application/Content/ContentV2Rules.cs new file mode 100644 index 0000000..a7680c0 --- /dev/null +++ b/Tiku.Application/Content/ContentV2Rules.cs @@ -0,0 +1,157 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Tiku.Domain.Content; + +namespace Tiku.Application.Content; + +public sealed record TargetProfileFact( + Guid TargetDimensionDefinitionId, + Guid TargetNodeId, + IReadOnlySet AncestorNodeIds); + +public sealed record PlacementRuleConditionFact( + Guid TargetDimensionDefinitionId, + Guid TargetNodeId, + TargetRuleOperator Operator); + +public sealed record PlacementRuleGroupFact( + int GroupOrder, + IReadOnlyCollection Conditions); + +public static class QuestionApplicabilityEvaluator +{ + public static bool Matches( + IReadOnlyCollection profile, + IReadOnlyCollection groups) + { + if (groups.Count == 0) return true; + var valuesByDimension = profile + .GroupBy(value => value.TargetDimensionDefinitionId) + .ToDictionary(group => group.Key, group => group.ToArray()); + return groups.Any(group => group.Conditions.Count > 0 && + group.Conditions.All(condition => Matches(valuesByDimension, condition))); + } + + private static bool Matches( + IReadOnlyDictionary valuesByDimension, + PlacementRuleConditionFact condition) + { + valuesByDimension.TryGetValue(condition.TargetDimensionDefinitionId, out var values); + values ??= []; + var positiveMatch = condition.Operator switch + { + TargetRuleOperator.Exact or TargetRuleOperator.NotExact => + values.Any(value => value.TargetNodeId == condition.TargetNodeId), + TargetRuleOperator.DescendantOf or TargetRuleOperator.NotDescendantOf => + values.Any(value => value.TargetNodeId == condition.TargetNodeId || + value.AncestorNodeIds.Contains(condition.TargetNodeId)), + _ => false + }; + return condition.Operator is TargetRuleOperator.NotExact or TargetRuleOperator.NotDescendantOf + ? !positiveMatch + : positiveMatch; + } +} + +public sealed record QuestionDeliveryFingerprintInput( + string QuestionType, + string? Content, + JsonElement Options, + int? CorrectOptionIndex, + JsonElement CorrectOptionIndices, + string? AnswerText, + JsonElement SubQuestions, + string? CodeLang, + string? CodeTemplate); + +public static class QuestionDeliveryFingerprint +{ + public static string Compute(QuestionDeliveryFingerprintInput input) + { + using var stream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(stream)) + { + writer.WriteStartObject(); + writer.WriteString("questionType", NormalizeText(input.QuestionType)); + writer.WriteString("content", NormalizeText(input.Content)); + writer.WritePropertyName("options"); + WriteCanonical(writer, input.Options); + if (input.CorrectOptionIndex.HasValue) + writer.WriteNumber("correctOptionIndex", input.CorrectOptionIndex.Value); + else + writer.WriteNull("correctOptionIndex"); + writer.WritePropertyName("correctOptionIndices"); + WriteCanonical(writer, input.CorrectOptionIndices); + writer.WriteString("answerText", NormalizeText(input.AnswerText)); + writer.WritePropertyName("subQuestions"); + WriteCanonical(writer, input.SubQuestions); + writer.WriteString("codeLang", NormalizeText(input.CodeLang)); + writer.WriteString("codeTemplate", NormalizeText(input.CodeTemplate)); + writer.WriteEndObject(); + } + + return Convert.ToHexString(SHA256.HashData(stream.ToArray())).ToLowerInvariant(); + } + + public static string NormalizePrompt(string? value) => NormalizeText(value); + + private static string NormalizeText(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return string.Empty; + var normalized = value.Normalize(NormalizationForm.FormKC).Trim(); + 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(); + } + + private static void WriteCanonical(Utf8JsonWriter writer, JsonElement value) + { + switch (value.ValueKind) + { + case JsonValueKind.Object: + writer.WriteStartObject(); + foreach (var property in value.EnumerateObject().OrderBy(property => property.Name, StringComparer.Ordinal)) + { + writer.WritePropertyName(property.Name); + WriteCanonical(writer, property.Value); + } + writer.WriteEndObject(); + break; + case JsonValueKind.Array: + writer.WriteStartArray(); + foreach (var item in value.EnumerateArray()) WriteCanonical(writer, item); + writer.WriteEndArray(); + break; + case JsonValueKind.String: + writer.WriteStringValue(NormalizeText(value.GetString())); + break; + case JsonValueKind.Number: + value.WriteTo(writer); + break; + case JsonValueKind.True: + writer.WriteBooleanValue(true); + break; + case JsonValueKind.False: + writer.WriteBooleanValue(false); + break; + default: + writer.WriteNullValue(); + break; + } + } +} diff --git a/Tiku.Application/Content/IContentManagementService.cs b/Tiku.Application/Content/IContentManagementService.cs deleted file mode 100644 index 545fc0d..0000000 --- a/Tiku.Application/Content/IContentManagementService.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Tiku.Application.Catalog; - -namespace Tiku.Application.Content; - -public interface IContentEntryManagementService -{ - Task> GetEntriesAsync( - ContentManagementActor actor, - ContentManagementFilter filter, - CancellationToken cancellationToken = default); - - Task> UpsertEntryAsync( - ContentManagementActor actor, - UpsertContentEntryCommand command, - CancellationToken cancellationToken = default); -} - -public interface IContentNodeManagementService -{ - Task> GetNodesAsync( - ContentManagementActor actor, - ContentManagementFilter filter, - CancellationToken cancellationToken = default); - - Task> UpsertNodeAsync( - ContentManagementActor actor, - UpsertContentNodeCommand command, - CancellationToken cancellationToken = default); -} - -public interface IQuestionCollectionManagementService -{ - Task> GetCollectionsAsync( - ContentManagementActor actor, - ContentManagementFilter filter, - CancellationToken cancellationToken = default); - - Task> UpsertCollectionAsync( - ContentManagementActor actor, - UpsertQuestionCollectionCommand command, - CancellationToken cancellationToken = default); - - Task ReplaceCollectionItemsAsync( - ContentManagementActor actor, - ReplaceCollectionItemsCommand command, - CancellationToken cancellationToken = default); -} - -public interface IPracticeBlueprintManagementService -{ - Task> GetPracticeBlueprintsAsync( - ContentManagementActor actor, - ContentManagementFilter filter, - CancellationToken cancellationToken = default); - - Task> UpsertPracticeBlueprintAsync( - ContentManagementActor actor, - UpsertPracticeBlueprintCommand command, - CancellationToken cancellationToken = default); - - ImportFieldMappingItem GetImportFieldMapping(string importType); - - ImportTemplateItem GetImportTemplate(string importType, string? format); -} diff --git a/Tiku.Application/Content/IContentNavigationQueryService.cs b/Tiku.Application/Content/IContentNavigationQueryService.cs deleted file mode 100644 index fd193e5..0000000 --- a/Tiku.Application/Content/IContentNavigationQueryService.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Tiku.Application.Catalog; - -namespace Tiku.Application.Content; - -public interface IContentNavigationQueryService -{ - Task> GetContentEntriesAsync( - ContentNavigationFilter filter, - CancellationToken cancellationToken = default); - - Task> GetContentNodesAsync( - ContentNavigationFilter filter, - CancellationToken cancellationToken = default); - - Task> GetQuestionCollectionsAsync( - ContentNavigationFilter filter, - CancellationToken cancellationToken = default); - - Task> GetPracticeBlueprintsAsync( - ContentNavigationFilter filter, - CancellationToken cancellationToken = default); - - Task> GetCollectionQuestionsAsync( - ContentNavigationFilter filter, - CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/Tiku.Application/Learning/LearningAccessAdministrationModels.cs b/Tiku.Application/Learning/LearningAccessAdministrationModels.cs deleted file mode 100644 index 62ad579..0000000 --- a/Tiku.Application/Learning/LearningAccessAdministrationModels.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Tiku.Domain.Learning; - -namespace Tiku.Application.Learning; - -public sealed record StudentTargetRegionItem( - Guid MarketRegionId, - string RegionCode, - string RegionName, - DateTimeOffset EffectiveAt, - DateTimeOffset? NextChangeAllowedAt); - -public sealed record ChangeStudentTargetRegionCommand(Guid MarketRegionId); - -public sealed record OverrideStudentTargetRegionCommand( - Guid UserId, - Guid MarketRegionId, - Guid ChangedBy, - string Reason); - -public sealed record UpsertClassContentAssignmentCommand( - Guid? Id, - Guid ClassId, - Guid ContentSliceId, - LearningContentResourceType ResourceType, - Guid ResourceId, - DateTimeOffset? StartsAt, - DateTimeOffset? EndsAt); - -public sealed record ClassContentAssignmentItem( - Guid Id, - Guid ClassId, - Guid ContentSliceId, - LearningContentResourceType ResourceType, - Guid ResourceId, - DateTimeOffset StartsAt, - DateTimeOffset? EndsAt, - ClassContentAssignmentStatus Status); - -public interface ILearningAccessAdministrationService -{ - Task GetTargetRegionAsync( - LearningActor actor, - CancellationToken cancellationToken = default); - - Task ChangeTargetRegionAsync( - LearningActor actor, - ChangeStudentTargetRegionCommand command, - CancellationToken cancellationToken = default); - - Task OverrideTargetRegionAsync( - Guid tenantId, - OverrideStudentTargetRegionCommand command, - CancellationToken cancellationToken = default); - - Task> GetClassAssignmentsAsync( - Guid tenantId, - Guid classId, - CancellationToken cancellationToken = default); - - Task UpsertClassAssignmentAsync( - Guid tenantId, - Guid actorUserId, - UpsertClassContentAssignmentCommand command, - CancellationToken cancellationToken = default); - - Task RevokeClassAssignmentAsync( - Guid tenantId, - Guid actorUserId, - Guid assignmentId, - string? reason, - CancellationToken cancellationToken = default); -} diff --git a/Tiku.Application/Learning/LearningAccessV2Models.cs b/Tiku.Application/Learning/LearningAccessV2Models.cs new file mode 100644 index 0000000..caf6a8b --- /dev/null +++ b/Tiku.Application/Learning/LearningAccessV2Models.cs @@ -0,0 +1,43 @@ +namespace Tiku.Application.Learning; + +public sealed record EffectiveLearningAccessSnapshot( + Guid TenantId, + Guid UserId, + Guid BusinessLineId, + Guid PrimaryProfileVersionId, + IReadOnlySet AlternateProfileVersionIds, + IReadOnlySet ContentReleaseIds, + IReadOnlySet AudienceSegmentIds, + IReadOnlySet ManifestVersionIds, + long GrantVersion, + long ContentVersion, + long StrongRevocationVersion, + DateTimeOffset ValidUntil, + DateTimeOffset CompiledAt); + +public interface IEffectiveLearningAccessService +{ + Task GetSnapshotAsync( + LearningActor actor, + Guid businessLineId, + CancellationToken cancellationToken = default); + + Task InvalidateAsync( + Guid tenantId, + Guid userId, + Guid businessLineId, + CancellationToken cancellationToken = default); +} + +public interface ILearningStrongRevocationService +{ + Task EnsureVersionAsync( + LearningActor actor, + long expectedVersion, + CancellationToken cancellationToken = default); + + Task InvalidateAsync( + Guid tenantId, + Guid userId, + CancellationToken cancellationToken = default); +} diff --git a/Tiku.Application/Learning/LearningActivityModels.cs b/Tiku.Application/Learning/LearningActivityModels.cs index 3114256..39e432f 100644 --- a/Tiku.Application/Learning/LearningActivityModels.cs +++ b/Tiku.Application/Learning/LearningActivityModels.cs @@ -18,7 +18,6 @@ public sealed record SubmitAnswerCommand( public sealed record SubmitPracticeSessionCommand( Guid PracticeSessionId, - long ExpectedSessionVersion, string IdempotencyKey); public sealed record QuestionActionCommand(QuestionLocator Locator, bool? Favorite); @@ -96,16 +95,8 @@ public sealed record WordStatsItem( public sealed record PracticeSessionCommand( string? Mode, - string? TargetType, - Guid? TargetId, - Guid? BlueprintId, - Guid? CollectionId, - Guid? EntryId, - Guid? ContentNodeId, - int? QuestionLimit, - int? DurationMinutes, - decimal? TotalScore, - JsonElement Metadata); + AccessResourceType ResourceType, + Guid ResourceId); public sealed record PracticeSessionFilter( Guid? PracticeSessionId = null, @@ -200,9 +191,13 @@ public sealed record PracticeSessionDetailItem( public sealed record PracticeSessionQuestionItem( Guid SessionQuestionId, - Guid QuestionReferenceId, - QuestionLocator Locator, - Guid QuestionId, + Guid? QuestionReferenceId, + QuestionLocator? Locator, + Guid? QuestionId, + Guid? QuestionAssetId, + Guid? QuestionRevisionId, + Guid? QuestionPlacementId, + Guid? AssessmentPolicyVersionId, string Type, string? TypeLabel, int? Difficulty, diff --git a/Tiku.Application/Learning/LearningTargetModels.cs b/Tiku.Application/Learning/LearningTargetModels.cs new file mode 100644 index 0000000..905698b --- /dev/null +++ b/Tiku.Application/Learning/LearningTargetModels.cs @@ -0,0 +1,109 @@ +using Tiku.Domain.Learning; + +namespace Tiku.Application.Learning; + +public sealed record LearningBusinessContextItem( + Guid BusinessLineId, + string BusinessCode, + string BusinessName, + Guid TenantBusinessLicenseId, + DateTimeOffset? LicenseEndsAt, + Guid? PrimaryProfileVersionId, + IReadOnlyCollection AlternateProfileVersionIds, + int MaxActiveTargets, + int MaxAlternateTargets, + int TargetChangeCooldownDays); + +public sealed record LearningContextItem(IReadOnlyCollection Businesses); + +public sealed record LearningTargetItem( + Guid ExamTargetProfileVersionId, + string ProfileCode, + string DisplayName, + int? ExamYear, + bool IsBaseTarget, + bool MayBePrimary, + bool MayBeAlternate, + bool IsPrimary, + bool IsAlternate); + +public sealed record LearningTargetSelectionItem( + Guid BusinessLineId, + Guid PrimaryProfileVersionId, + IReadOnlyCollection AlternateProfileVersionIds, + DateTimeOffset ChangedAt, + DateTimeOffset? NextChangeAllowedAt); + +public sealed record ChangeLearningTargetsCommand( + Guid PrimaryProfileVersionId, + IReadOnlyCollection AlternateProfileVersionIds); + +public interface IStudentLearningTargetService +{ + Task GetContextAsync( + LearningActor actor, + CancellationToken cancellationToken = default); + + Task> GetTargetsAsync( + LearningActor actor, + Guid businessLineId, + CancellationToken cancellationToken = default); + + Task ChangeTargetsAsync( + LearningActor actor, + Guid businessLineId, + ChangeLearningTargetsCommand command, + CancellationToken cancellationToken = default); + + Task OverrideTargetsAsync( + LearningActor student, + Guid businessLineId, + ChangeLearningTargetsCommand command, + Guid changedBy, + string reason, + CancellationToken cancellationToken = default); +} + +public sealed record UpsertClassAssignmentGrantCommand( + Guid? Id, + Guid ClassId, + Guid ProductAccessManifestVersionId, + AccessResourceType ResourceType, + Guid ResourceId, + DateTimeOffset? StartsAt, + DateTimeOffset? EndsAt); + +public sealed record ClassAssignmentGrantItem( + Guid Id, + Guid ClassId, + Guid ProductAccessManifestVersionId, + AccessResourceType ResourceType, + Guid ResourceId, + DateTimeOffset StartsAt, + DateTimeOffset? EndsAt, + ClassAssignmentGrantStatus Status, + Guid? CreatedBy, + Guid? RevokedBy, + string? RevokedReason); + +public interface IV2LearningAccessAdministrationService +{ + Task> GetClassGrantsAsync( + Guid tenantId, + Guid classId, + CancellationToken cancellationToken = default); + + Task UpsertClassGrantAsync( + Guid tenantId, + Guid actorUserId, + UpsertClassAssignmentGrantCommand command, + CancellationToken cancellationToken = default); + + Task RevokeClassGrantAsync( + Guid tenantId, + Guid actorUserId, + Guid classId, + Guid grantId, + string reason, + CancellationToken cancellationToken = default); +} diff --git a/Tiku.Application/Learning/StudentLearningCatalogModels.cs b/Tiku.Application/Learning/StudentLearningCatalogModels.cs new file mode 100644 index 0000000..b35659d --- /dev/null +++ b/Tiku.Application/Learning/StudentLearningCatalogModels.cs @@ -0,0 +1,26 @@ +using Tiku.Domain.Learning; + +namespace Tiku.Application.Learning; + +public sealed record StudentCatalogResourceItem( + AccessResourceType ResourceType, + Guid ResourceId, + string DisplayName, + int SortOrder, + IReadOnlyCollection ManifestVersionIds); + +public sealed record StudentLearningCatalogItem( + Guid BusinessLineId, + Guid PrimaryProfileVersionId, + IReadOnlyCollection AlternateProfileVersionIds, + IReadOnlyCollection Resources, + long GrantVersion, + long ContentVersion); + +public interface IStudentLearningCatalogService +{ + Task GetAsync( + LearningActor actor, + Guid businessLineId, + CancellationToken cancellationToken = default); +} diff --git a/Tiku.Application/PlatformAdmin/PlatformQuestionBankModels.cs b/Tiku.Application/PlatformAdmin/PlatformQuestionBankModels.cs deleted file mode 100644 index 2440f05..0000000 --- a/Tiku.Application/PlatformAdmin/PlatformQuestionBankModels.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System.Text.Json; -using Tiku.Application.Assets; -using Tiku.Domain.Content; -using Tiku.Domain.QuestionBanks; - -namespace Tiku.Application.PlatformAdmin; - -public sealed record PlatformQuestionBankFilter( - Guid? QuestionBankId = null, - Guid? ContentNodeId = null, - string? Keyword = null, - string? Type = null, - int? Difficulty = null, - string? Status = null, - int Page = 1, - int PageSize = 20); - -public sealed record PlatformQuestionBankItem( - Guid Id, - Guid? ContentEntryId, - string Name, - QuestionBankStatus Status, - int NodeCount, - int QuestionCount, - JsonElement Metadata, - DateTimeOffset CreatedAt, - DateTimeOffset? UpdatedAt); - -public sealed record UpsertPlatformQuestionBankCommand( - Guid? Id, - string Name, - JsonElement Metadata); - -public sealed record PlatformQuestionBankNodeItem( - Guid Id, - Guid QuestionBankId, - Guid EntryId, - Guid? ParentId, - string? NodeKey, - string Name, - ContentNodeType NodeType, - int Depth, - int SortOrder, - bool IsActive, - bool IsSelectable, - bool IsLeaf, - int QuestionCount, - JsonElement Metadata); - -public sealed record UpsertPlatformQuestionBankNodeCommand( - Guid? Id, - Guid QuestionBankId, - Guid? ParentId, - string? NodeKey, - string Name, - ContentNodeType NodeType, - int SortOrder, - bool IsSelectable, - JsonElement Metadata); - -public sealed record BatchCreatePlatformQuestionBankNodesCommand( - Guid QuestionBankId, - Guid? ParentId, - ContentNodeType NodeType, - IReadOnlyCollection Names); - -public sealed record PlatformQuestionItem( - Guid Id, - Guid? VersionId, - Guid QuestionBankId, - Guid EntryId, - Guid ContentNodeId, - string? LegacyId, - string Type, - string? TypeLabel, - int? Difficulty, - JsonElement Tags, - string? Content, - JsonElement Options, - int? CorrectOptionIndex, - JsonElement CorrectOptionIndices, - string? AnswerText, - string? Explanation, - JsonElement SubQuestions, - string? CodeLang, - string? CodeTemplate, - string? MediaUrl, - QuestionStatus Status, - int VersionNo, - DateTimeOffset CreatedAt, - DateTimeOffset? UpdatedAt); - -public sealed record PlatformQuestionPage( - IReadOnlyCollection Items, - int Total, - int Page, - int PageSize); - -public sealed record UpsertPlatformQuestionCommand( - Guid? Id, - Guid QuestionBankId, - Guid ContentNodeId, - string? LegacyId, - string? Type, - string? TypeLabel, - int? Difficulty, - JsonElement Tags, - string? Content, - JsonElement Options, - int? CorrectOptionIndex, - JsonElement CorrectOptionIndices, - string? AnswerText, - string? Explanation, - JsonElement SubQuestions, - string? CodeLang, - string? CodeTemplate, - string? MediaUrl, - string? Status, - JsonElement ExamMarkers, - string? SourceHash); - -public sealed record ArchivePlatformQuestionsCommand(IReadOnlyCollection QuestionIds); - -public sealed record PlatformQuestionImportCommand( - Guid QuestionBankId, - Guid? ContentNodeId, - string Format, - string? SourceName, - JsonElement Payload); - -public sealed record PlatformQuestionImportResult( - ContentImportJobDetail Detail, - int CreatedNodeCount, - int InsertedQuestionCount, - int UpdatedQuestionCount, - int SkippedQuestionCount); - -public interface IPlatformQuestionBankCatalogService -{ - Task> GetBanksAsync(PlatformAdminActor actor, - PlatformQuestionBankFilter filter, CancellationToken cancellationToken = default); - - Task UpsertBankAsync(PlatformAdminActor actor, UpsertPlatformQuestionBankCommand command, - CancellationToken cancellationToken = default); - - Task ArchiveBankAsync(PlatformAdminActor actor, Guid bankId, - CancellationToken cancellationToken = default); - -} - -public interface IPlatformQuestionBankNodeService -{ - Task> GetNodesAsync(PlatformAdminActor actor, Guid bankId, - CancellationToken cancellationToken = default); - - Task UpsertNodeAsync(PlatformAdminActor actor, - UpsertPlatformQuestionBankNodeCommand command, CancellationToken cancellationToken = default); - - Task> BatchCreateNodesAsync(PlatformAdminActor actor, - BatchCreatePlatformQuestionBankNodesCommand command, CancellationToken cancellationToken = default); - - Task ArchiveNodeAsync(PlatformAdminActor actor, Guid nodeId, - CancellationToken cancellationToken = default); - -} - -public interface IPlatformQuestionAdministrationService -{ - Task GetQuestionsAsync(PlatformAdminActor actor, PlatformQuestionBankFilter filter, - CancellationToken cancellationToken = default); - - Task UpsertQuestionAsync(PlatformAdminActor actor, UpsertPlatformQuestionCommand command, - CancellationToken cancellationToken = default); - - Task ArchiveQuestionsAsync(PlatformAdminActor actor, ArchivePlatformQuestionsCommand command, - CancellationToken cancellationToken = default); - -} - -public interface IPlatformQuestionImportService -{ - Task PreviewImportAsync(PlatformAdminActor actor, - PlatformQuestionImportCommand command, CancellationToken cancellationToken = default); - - Task ExecuteImportAsync(PlatformAdminActor actor, - PlatformQuestionImportCommand command, CancellationToken cancellationToken = default); - - Task GetImportAsync(PlatformAdminActor actor, Guid jobId, - CancellationToken cancellationToken = default); - -} - -public interface IPlatformQuestionAssetService -{ - Task SignQuestionAssetUploadAsync(PlatformAdminActor actor, AssetUploadSignCommand command, - CancellationToken cancellationToken = default); - - Task ConfirmQuestionAssetUploadAsync(PlatformAdminActor actor, - AssetUploadConfirmCommand command, CancellationToken cancellationToken = default); -} diff --git a/Tiku.Application/QuestionBanks/IQuestionBankQueryService.cs b/Tiku.Application/QuestionBanks/IQuestionBankQueryService.cs deleted file mode 100644 index f3096ca..0000000 --- a/Tiku.Application/QuestionBanks/IQuestionBankQueryService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Tiku.Application.Catalog; - -namespace Tiku.Application.QuestionBanks; - -public interface IQuestionBankQueryService -{ - Task> GetQuestionBanksAsync( - QuestionBankFilter filter, - CancellationToken cancellationToken = default); - - Task> GetQuestionsAsync( - QuestionBankFilter filter, - CancellationToken cancellationToken = default); - - Task GetQuestionAsync( - QuestionBankFilter filter, - CancellationToken cancellationToken = default); - -} diff --git a/Tiku.Domain/Catalog/TaxonomyEntities.cs b/Tiku.Domain/Catalog/TaxonomyEntities.cs deleted file mode 100644 index 4464afb..0000000 --- a/Tiku.Domain/Catalog/TaxonomyEntities.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text.Json; -using Tiku.Domain.Common; - -namespace Tiku.Domain.Catalog; - -public sealed class TaxonomyNode : AuditableTenantEntity -{ - public Guid? ParentOwnerTenantId { get; set; } - public Guid? ParentId { get; set; } - public TaxonomyNodeType NodeType { get; set; } = TaxonomyNodeType.KnowledgePoint; - public string Code { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public string? Path { get; set; } - public int Depth { get; set; } - public int SortOrder { get; set; } - public bool IsActive { get; set; } = true; - public JsonElement Metadata { get; set; } = JsonDefaults.Object(); -} - -public sealed class QuestionTaxonomyAssignment : Entity, ITenantOwned -{ - public Guid QuestionId { get; set; } - public Guid TaxonomyOwnerTenantId { get; set; } - public Guid TaxonomyNodeId { get; set; } - public bool IsPrimary { get; set; } - public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; - public Guid TenantId { get; set; } -} - -public enum TaxonomyNodeType -{ - Subject, - Chapter, - KnowledgePoint, - Paper, - Custom -} \ No newline at end of file diff --git a/Tiku.Domain/Content/ContentV2AssessmentEntities.cs b/Tiku.Domain/Content/ContentV2AssessmentEntities.cs new file mode 100644 index 0000000..48029e9 --- /dev/null +++ b/Tiku.Domain/Content/ContentV2AssessmentEntities.cs @@ -0,0 +1,70 @@ +using System.Text.Json; +using Tiku.Domain.Common; + +namespace Tiku.Domain.Content; + +public sealed class AssessmentPolicy : AuditableTenantEntity +{ + public Guid BusinessLineId { get; set; } + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public Guid? CurrentVersionId { get; set; } + public bool IsActive { get; set; } = true; +} + +public sealed class AssessmentPolicyVersion : Entity, ITenantOwned +{ + public Guid AssessmentPolicyId { get; set; } + public int VersionNo { get; set; } = 1; + public AssessmentGradingMode GradingMode { get; set; } = AssessmentGradingMode.AuthoritativeObjective; + public decimal DefaultScore { get; set; } = 1; + public int RoundingScale { get; set; } = 2; + public ContentDefinitionStatus Status { get; set; } = ContentDefinitionStatus.Draft; + public DateTimeOffset? PublishedAt { get; set; } + public Guid? CreatedBy { get; set; } + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + public Guid TenantId { get; set; } +} + +public sealed class ObjectiveGradingRule : Entity, ITenantOwned +{ + public Guid AssessmentPolicyVersionId { get; set; } + public string RuleType { get; set; } = string.Empty; + public JsonElement Configuration { get; set; } = JsonDefaults.Object(); + public Guid TenantId { get; set; } +} + +public sealed class SubjectiveRubric : Entity, ITenantOwned +{ + public Guid AssessmentPolicyVersionId { get; set; } + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public decimal MaxScore { get; set; } + public JsonElement Criteria { get; set; } = JsonDefaults.Array(); + public Guid TenantId { get; set; } +} + +public sealed class AssessmentScoreBand : Entity, ITenantOwned +{ + public Guid AssessmentPolicyVersionId { get; set; } + public decimal MinScore { get; set; } + public decimal MaxScore { get; set; } + public string Label { get; set; } = string.Empty; + public Guid TenantId { get; set; } +} + +public sealed class ScoreAdjustmentRule : Entity, ITenantOwned +{ + public Guid AssessmentPolicyVersionId { get; set; } + public int Priority { get; set; } + public string RuleType { get; set; } = string.Empty; + public JsonElement Configuration { get; set; } = JsonDefaults.Object(); + public Guid TenantId { get; set; } +} + +public enum AssessmentGradingMode +{ + AuthoritativeObjective, + PendingManualReview, + Hybrid +} diff --git a/Tiku.Domain/Content/ContentV2CurriculumEntities.cs b/Tiku.Domain/Content/ContentV2CurriculumEntities.cs new file mode 100644 index 0000000..e882923 --- /dev/null +++ b/Tiku.Domain/Content/ContentV2CurriculumEntities.cs @@ -0,0 +1,94 @@ +using System.Text.Json; +using Tiku.Domain.Common; + +namespace Tiku.Domain.Content; + +public sealed class KnowledgeConcept : AuditableTenantEntity +{ + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public bool IsActive { get; set; } = true; +} + +public sealed class Curriculum : AuditableTenantEntity +{ + public Guid BusinessLineId { get; set; } + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public Guid? CurrentVersionId { get; set; } + public bool IsActive { get; set; } = true; +} + +public sealed class CurriculumVersion : Entity, ITenantOwned +{ + public Guid CurriculumId { get; set; } + public int VersionNo { get; set; } = 1; + public string Name { get; set; } = string.Empty; + public int? ExamYear { get; set; } + public ContentDefinitionStatus Status { get; set; } = ContentDefinitionStatus.Draft; + public DateTimeOffset? PublishedAt { get; set; } + public Guid? CreatedBy { get; set; } + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + public Guid TenantId { get; set; } +} + +public sealed class CurriculumNode : AuditableTenantEntity +{ + public Guid CurriculumVersionId { get; set; } + public Guid? ParentId { get; set; } + public Guid? KnowledgeConceptId { get; set; } + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public CurriculumNodeKind Kind { get; set; } = CurriculumNodeKind.KnowledgePoint; + public int SortOrder { get; set; } +} + +public sealed class CurriculumNodeClosure : Entity, ITenantOwned +{ + public Guid CurriculumVersionId { get; set; } + public Guid AncestorNodeId { get; set; } + public Guid DescendantNodeId { get; set; } + public int Depth { get; set; } + public Guid TenantId { get; set; } +} + +public sealed class ExamPaperSpecification : AuditableTenantEntity +{ + public Guid BusinessLineId { get; set; } + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public Guid? CurrentVersionId { get; set; } + public bool IsActive { get; set; } = true; +} + +public sealed class ExamPaperSpecificationVersion : Entity, ITenantOwned +{ + public Guid SpecificationId { get; set; } + public int VersionNo { get; set; } = 1; + public int? ExamYear { get; set; } + public int DurationMinutes { get; set; } + public decimal TotalScore { get; set; } + public JsonElement Sections { get; set; } = JsonDefaults.Array(); + public ContentDefinitionStatus Status { get; set; } = ContentDefinitionStatus.Draft; + public DateTimeOffset? PublishedAt { get; set; } + public Guid? CreatedBy { get; set; } + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + public Guid TenantId { get; set; } +} + +public enum CurriculumNodeKind +{ + Subject, + Chapter, + Section, + KnowledgePoint, + Custom +} + +public enum ContentDefinitionStatus +{ + Draft, + Published, + Retired +} diff --git a/Tiku.Domain/Content/ContentV2QuestionEntities.cs b/Tiku.Domain/Content/ContentV2QuestionEntities.cs new file mode 100644 index 0000000..e805010 --- /dev/null +++ b/Tiku.Domain/Content/ContentV2QuestionEntities.cs @@ -0,0 +1,104 @@ +using System.Text.Json; +using Tiku.Domain.Common; + +namespace Tiku.Domain.Content; + +public sealed class QuestionAsset : AuditableTenantEntity +{ + public Guid? FamilyId { get; set; } + public Guid? CurrentRevisionId { get; set; } + public string DeliveryFingerprint { get; set; } = string.Empty; + public QuestionAssetStatus Status { get; set; } = QuestionAssetStatus.Draft; + public Guid? CreatedBy { get; set; } + public Guid? UpdatedBy { get; set; } +} + +public sealed class QuestionRevision : Entity, ITenantOwned +{ + public Guid QuestionAssetId { get; set; } + public int RevisionNo { get; set; } = 1; + public string QuestionType { get; set; } = "choice"; + public string? TypeLabel { get; set; } + public int? Difficulty { get; set; } + public string NormalizedContent { get; set; } = string.Empty; + public string? Content { get; set; } + public JsonElement Options { get; set; } = JsonDefaults.Array(); + public int? CorrectOptionIndex { get; set; } + public JsonElement CorrectOptionIndices { get; set; } = JsonDefaults.Array(); + public string? AnswerText { get; set; } + public string? Explanation { get; set; } + public JsonElement SubQuestions { get; set; } = JsonDefaults.Array(); + public string? CodeLang { get; set; } + public string? CodeTemplate { get; set; } + public string DeliveryFingerprint { get; set; } = string.Empty; + public Guid? CreatedBy { get; set; } + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + public Guid TenantId { get; set; } +} + +public sealed class QuestionFamily : AuditableTenantEntity +{ + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public Guid? CreatedBy { get; set; } +} + +public sealed class QuestionPlacement : AuditableTenantEntity +{ + public Guid QuestionAssetOwnerTenantId { get; set; } + public Guid QuestionAssetId { get; set; } + public Guid CurriculumVersionId { get; set; } + public Guid CurriculumNodeId { get; set; } + public Guid AssessmentPolicyVersionId { get; set; } + public int? DifficultyOverride { get; set; } + public QuestionPlacementStatus Status { get; set; } = QuestionPlacementStatus.Draft; + public DateTimeOffset? ValidFrom { get; set; } + public DateTimeOffset? ValidTo { get; set; } + public Guid? CreatedBy { get; set; } + public Guid? UpdatedBy { get; set; } +} + +public sealed class QuestionPlacementConcept : Entity, ITenantOwned +{ + public Guid PlacementId { get; set; } + public Guid KnowledgeConceptId { get; set; } + public Guid TenantId { get; set; } +} + +public sealed class QuestionPlacementRuleGroup : Entity, ITenantOwned +{ + public Guid PlacementId { get; set; } + public int GroupOrder { get; set; } + public Guid TenantId { get; set; } +} + +public sealed class QuestionPlacementRuleCondition : Entity, ITenantOwned +{ + public Guid RuleGroupId { get; set; } + public Guid TargetDimensionDefinitionId { get; set; } + public Guid TargetNodeId { get; set; } + public TargetRuleOperator Operator { get; set; } = TargetRuleOperator.Exact; + public Guid TenantId { get; set; } +} + +public enum QuestionAssetStatus +{ + Draft, + Published, + Archived +} + +public enum QuestionPlacementStatus +{ + Draft, + Active, + Retired +} + +public enum TargetRuleOperator +{ + Exact, + DescendantOf, + NotExact, + NotDescendantOf +} diff --git a/Tiku.Domain/Content/ContentV2ReleaseEntities.cs b/Tiku.Domain/Content/ContentV2ReleaseEntities.cs new file mode 100644 index 0000000..f0fa3ba --- /dev/null +++ b/Tiku.Domain/Content/ContentV2ReleaseEntities.cs @@ -0,0 +1,136 @@ +using System.Text.Json; +using Tiku.Domain.Common; + +namespace Tiku.Domain.Content; + +public sealed class ContentRelease : AuditableTenantEntity +{ + public Guid BusinessLineId { get; set; } + public Guid CurriculumVersionId { get; set; } + public int ReleaseNo { get; set; } = 1; + public string Name { get; set; } = string.Empty; + public ContentReleaseStatus Status { get; set; } = ContentReleaseStatus.Draft; + public string CompilerVersion { get; set; } = "content-v2"; + public string SourceFingerprint { get; set; } = string.Empty; + public DateTimeOffset? PublishedAt { get; set; } + public Guid? CreatedBy { get; set; } +} + +public sealed class AudienceSegment : Entity, ITenantOwned +{ + public Guid ContentReleaseId { get; set; } + public string RuleHash { get; set; } = string.Empty; + public int SegmentOrdinal { get; set; } + public Guid TenantId { get; set; } +} + +public sealed class AudienceSegmentMember : Entity, ITenantOwned +{ + public Guid ContentReleaseId { get; set; } + public Guid AudienceSegmentId { get; set; } + public Guid ExamTargetProfileVersionId { get; set; } + public Guid TenantId { get; set; } +} + +public sealed class ContentReleaseQuestion : Entity, ITenantOwned +{ + public Guid ContentReleaseId { get; set; } + public Guid AudienceSegmentId { get; set; } + public Guid CurriculumNodeId { get; set; } + public Guid QuestionPlacementId { get; set; } + public Guid QuestionAssetOwnerTenantId { get; set; } + public Guid QuestionAssetId { get; set; } + public Guid QuestionRevisionId { get; set; } + public Guid AssessmentPolicyVersionId { get; set; } + public int Ordinal { get; set; } + public int? Difficulty { get; set; } + public string QuestionType { get; set; } = "choice"; + public ContentReleaseQuestionStatus Status { get; set; } = ContentReleaseQuestionStatus.Active; + public Guid TenantId { get; set; } +} + +public sealed class CollectionRelease : AuditableTenantEntity +{ + public Guid ContentReleaseId { get; set; } + public Guid SourceCollectionId { get; set; } + public int VersionNo { get; set; } = 1; + public string Name { get; set; } = string.Empty; + public ContentReleaseStatus Status { get; set; } = ContentReleaseStatus.Draft; + public DateTimeOffset? PublishedAt { get; set; } +} + +public sealed class CollectionReleaseQuestion : Entity, ITenantOwned +{ + public Guid CollectionReleaseId { get; set; } + public Guid ContentReleaseQuestionId { get; set; } + public int SortOrder { get; set; } + public decimal? Score { get; set; } + public bool Required { get; set; } = true; + public Guid TenantId { get; set; } +} + +public sealed class BlueprintRelease : AuditableTenantEntity +{ + public Guid ContentReleaseId { get; set; } + public Guid? CollectionReleaseId { get; set; } + public Guid SourceBlueprintId { get; set; } + public Guid ExamPaperSpecificationVersionId { get; set; } + public int VersionNo { get; set; } = 1; + public string Name { get; set; } = string.Empty; + public JsonElement AssemblyRules { get; set; } = JsonDefaults.Object(); + public ContentReleaseStatus Status { get; set; } = ContentReleaseStatus.Draft; + public DateTimeOffset? PublishedAt { get; set; } +} + +public sealed class PlatformContentPackageVersion : AuditableTenantEntity +{ + public string PackageCode { get; set; } = string.Empty; + public int VersionNo { get; set; } = 1; + public Guid BusinessLineId { get; set; } + public Guid ContentReleaseId { get; set; } + public string ContentHash { get; set; } = string.Empty; + public PlatformContentPackageStatus Status { get; set; } = PlatformContentPackageStatus.Draft; + public DateTimeOffset? PublishedAt { get; set; } +} + +public sealed class PlatformContentPackageCellProjection : Entity +{ + public Guid PackageOwnerTenantId { get; set; } + public string PackageCode { get; set; } = string.Empty; + public Guid PlatformContentPackageVersionId { get; set; } + public string CellId { get; set; } = string.Empty; + public long EventSequence { get; set; } + public string ContentHash { get; set; } = string.Empty; + public CellProjectionStatus Status { get; set; } = CellProjectionStatus.Pending; + public DateTimeOffset? ProjectedAt { get; set; } + public string? LastError { get; set; } +} + +public enum ContentReleaseStatus +{ + Draft, + Compiling, + Published, + Retired, + Failed +} + +public enum ContentReleaseQuestionStatus +{ + Active, + Retired +} + +public enum PlatformContentPackageStatus +{ + Draft, + Published, + Retired +} + +public enum CellProjectionStatus +{ + Pending, + Ready, + Failed +} diff --git a/Tiku.Domain/Content/ContentV2TargetEntities.cs b/Tiku.Domain/Content/ContentV2TargetEntities.cs new file mode 100644 index 0000000..b68d658 --- /dev/null +++ b/Tiku.Domain/Content/ContentV2TargetEntities.cs @@ -0,0 +1,88 @@ +using Tiku.Domain.Common; + +namespace Tiku.Domain.Content; + +public sealed class TargetDimensionDefinition : AuditableEntity +{ + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public bool IsHierarchical { get; set; } = true; + public bool IsActive { get; set; } = true; +} + +public sealed class TargetNode : AuditableEntity +{ + public Guid TargetDimensionDefinitionId { get; set; } + public Guid? ParentId { get; set; } + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + public bool IsActive { get; set; } = true; +} + +public sealed class ExamTargetProfile : AuditableEntity +{ + public Guid BusinessLineId { get; set; } + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public Guid? CurrentVersionId { get; set; } + public bool IsActive { get; set; } = true; +} + +public sealed class ExamTargetProfileVersion : Entity +{ + public Guid ExamTargetProfileId { get; set; } + public int VersionNo { get; set; } = 1; + public string DisplayName { get; set; } = string.Empty; + public int? ExamYear { get; set; } + public ContentDefinitionStatus Status { get; set; } = ContentDefinitionStatus.Draft; + public DateTimeOffset? PublishedAt { get; set; } + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; +} + +public sealed class ExamTargetProfileValue : Entity +{ + public Guid ExamTargetProfileVersionId { get; set; } + public Guid TargetDimensionDefinitionId { get; set; } + public Guid TargetNodeId { get; set; } +} + +public sealed class BusinessTargetPolicy : AuditableEntity +{ + public Guid BusinessLineId { get; set; } + public Guid? CurrentVersionId { get; set; } + public bool IsActive { get; set; } = true; +} + +public sealed class BusinessTargetPolicyVersion : Entity +{ + public Guid BusinessTargetPolicyId { get; set; } + public int VersionNo { get; set; } = 1; + public int MaxActiveTargets { get; set; } = 16; + public int MaxPrimaryTargets { get; set; } = 1; + public int MaxAlternateTargets { get; set; } + public int TargetChangeCooldownDays { get; set; } = 30; + public bool IncludesBaseContent { get; set; } + public TargetSelectionMode SelectionMode { get; set; } = TargetSelectionMode.SingleFromTenantLicense; + public ContentDefinitionStatus Status { get; set; } = ContentDefinitionStatus.Draft; + public DateTimeOffset? PublishedAt { get; set; } + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; +} + +public sealed class BusinessTargetDimensionRequirement : Entity +{ + public Guid BusinessTargetPolicyVersionId { get; set; } + public Guid TargetDimensionDefinitionId { get; set; } + public int MinValues { get; set; } = 1; + public int MaxValues { get; set; } = 1; + public bool RequiredForProfile { get; set; } = true; + public int SortOrder { get; set; } +} + +public enum TargetSelectionMode +{ + SingleFromTenantLicense, + BasePlusEntitledTargets, + BasePlusPrimaryAndEntitledAlternates, + EntitlementDefined +} diff --git a/Tiku.Domain/Learning/LearningAccessV2Entities.cs b/Tiku.Domain/Learning/LearningAccessV2Entities.cs new file mode 100644 index 0000000..03a9bec --- /dev/null +++ b/Tiku.Domain/Learning/LearningAccessV2Entities.cs @@ -0,0 +1,178 @@ +using Tiku.Domain.Common; + +namespace Tiku.Domain.Learning; + +public sealed class TenantBusinessLicense : AuditableTenantEntity +{ + public Guid BusinessLineId { get; set; } + public LicenseLifecycleStatus Status { get; set; } = LicenseLifecycleStatus.Active; + public DateTimeOffset StartsAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? EndsAt { get; set; } + public long Version { get; set; } = 1; +} + +public sealed class TenantLicensedTarget : Entity, ITenantOwned +{ + public Guid TenantBusinessLicenseId { get; set; } + public Guid ExamTargetProfileVersionId { get; set; } + public bool IsBaseTarget { get; set; } + public DateTimeOffset StartsAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? EndsAt { get; set; } + public Guid TenantId { get; set; } +} + +public sealed class ProductAccessManifest : AuditableTenantEntity +{ + public Guid LearningProductId { get; set; } + public Guid? CurrentVersionId { get; set; } + public bool IsActive { get; set; } = true; +} + +public sealed class ProductAccessManifestVersion : Entity, ITenantOwned +{ + public Guid ProductAccessManifestId { get; set; } + public int VersionNo { get; set; } = 1; + public AccessManifestStatus Status { get; set; } = AccessManifestStatus.Draft; + public int MaxActiveTargets { get; set; } = 1; + public int MaxAlternateTargets { get; set; } + public int? DailyQuestionLimit { get; set; } + public int? TotalQuestionLimit { get; set; } + public DateTimeOffset? PublishedAt { get; set; } + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + public Guid TenantId { get; set; } +} + +public sealed class ProductManifestRelease : Entity, ITenantOwned +{ + public Guid ProductAccessManifestVersionId { get; set; } + public Guid ContentOwnerTenantId { get; set; } + public Guid ContentReleaseId { get; set; } + public Guid? PlatformContentPackageOwnerTenantId { get; set; } + public Guid? PlatformContentPackageVersionId { get; set; } + public Guid TenantId { get; set; } +} + +public sealed class ProductManifestTarget : Entity, ITenantOwned +{ + public Guid ProductAccessManifestVersionId { get; set; } + public Guid ExamTargetProfileVersionId { get; set; } + public bool MayBePrimary { get; set; } = true; + public bool MayBeAlternate { get; set; } + public Guid TenantId { get; set; } +} + +public sealed class ProductManifestResource : Entity, ITenantOwned +{ + public Guid ProductAccessManifestVersionId { get; set; } + public AccessResourceType ResourceType { get; set; } + public Guid ResourceId { get; set; } + public string DisplayName { get; set; } = string.Empty; + public int SortOrder { get; set; } + public Guid TenantId { get; set; } +} + +public sealed class StudentEntitlement : AuditableTenantEntity +{ + public Guid UserId { get; set; } + public Guid LearningProductId { get; set; } + public Guid ProductAccessManifestVersionId { get; set; } + public StudentEntitlementStatus Status { get; set; } = StudentEntitlementStatus.Active; + public DateTimeOffset StartsAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? EndsAt { get; set; } + public string SourceType { get; set; } = string.Empty; + public Guid? SourceId { get; set; } + public int UsedQuestionCount { get; set; } + public DateTimeOffset? RevokedAt { get; set; } + public Guid? RevokedBy { get; set; } + public string? RevokedReason { get; set; } +} + +public sealed class StudentTargetSelectionHistory : AuditableTenantEntity +{ + public Guid UserId { get; set; } + public Guid BusinessLineId { get; set; } + public Guid ExamTargetProfileVersionId { get; set; } + public TargetSelectionRole Role { get; set; } = TargetSelectionRole.Primary; + public Guid? EntitlementId { get; set; } + public string SourceType { get; set; } = string.Empty; + public Guid SourceId { get; set; } + public DateTimeOffset EffectiveAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? EndedAt { get; set; } + public bool IsCurrent { get; set; } = true; + public Guid? ChangedBy { get; set; } + public string? Reason { get; set; } +} + +public sealed class ClassAssignmentGrant : AuditableTenantEntity +{ + public Guid ClassId { get; set; } + public Guid ProductAccessManifestVersionId { get; set; } + public AccessResourceType ResourceType { get; set; } + public Guid ResourceId { get; set; } + public DateTimeOffset StartsAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? EndsAt { get; set; } + public ClassAssignmentGrantStatus Status { get; set; } = ClassAssignmentGrantStatus.Active; + public Guid? CreatedBy { get; set; } + public Guid? RevokedBy { get; set; } + public string? RevokedReason { get; set; } +} + +public sealed class EffectiveAccessProjection : TenantEntity +{ + public Guid UserId { get; set; } + public Guid BusinessLineId { get; set; } + public Guid? PrimaryProfileVersionId { get; set; } + public Guid[] AlternateProfileVersionIds { get; set; } = []; + public Guid[] ContentReleaseIds { get; set; } = []; + public Guid[] AudienceSegmentIds { get; set; } = []; + public Guid[] ManifestVersionIds { get; set; } = []; + public long GrantVersion { get; set; } = 1; + public long ContentVersion { get; set; } = 1; + public long StrongRevocationVersion { get; set; } = 1; + public DateTimeOffset ValidUntil { get; set; } + public DateTimeOffset CompiledAt { get; set; } = DateTimeOffset.UtcNow; +} + +public enum LicenseLifecycleStatus +{ + Active, + Suspended, + Expired, + Cancelled +} + +public enum AccessManifestStatus +{ + Draft, + Published, + Retired +} + +public enum AccessResourceType +{ + ContentRelease, + CurriculumNode, + CollectionRelease, + BlueprintRelease +} + +public enum TargetSelectionRole +{ + Primary, + Alternate +} + +public enum StudentEntitlementStatus +{ + Active, + Expired, + Revoked, + Cancelled +} + +public enum ClassAssignmentGrantStatus +{ + Active, + Revoked, + Expired +} diff --git a/Tiku.Domain/Learning/LearningEntities.cs b/Tiku.Domain/Learning/LearningEntities.cs index 83329b8..3e4eb9c 100644 --- a/Tiku.Domain/Learning/LearningEntities.cs +++ b/Tiku.Domain/Learning/LearningEntities.cs @@ -36,10 +36,15 @@ public sealed class PracticeSession : TenantEntity public sealed class PracticeSessionQuestion : TenantEntity { public Guid PracticeSessionId { get; set; } - public Guid QuestionReferenceId { get; set; } + public Guid? ContentReleaseQuestionId { get; set; } + public Guid? QuestionRevisionId { get; set; } + public Guid? QuestionPlacementId { get; set; } + public Guid? AssessmentPolicyVersionId { get; set; } + public Guid? QuestionAssetId { get; set; } + public Guid? QuestionReferenceId { get; set; } public Guid QuestionOwnerTenantId { get; set; } - public Guid QuestionId { get; set; } - public Guid QuestionVersionId { get; set; } + public Guid? QuestionId { get; set; } + public Guid? QuestionVersionId { get; set; } public int Position { get; set; } public decimal? Score { get; set; } public string QuestionType { get; set; } = "choice"; @@ -102,18 +107,6 @@ public sealed class CurrentAnswer : TenantEntity public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow; } -public sealed class AnswerSubmissionIdempotency : Entity, ITenantOwned -{ - public Guid TenantId { get; set; } - public Guid UserId { get; set; } - public Guid PracticeSessionId { get; set; } - public string IdempotencyKey { get; set; } = string.Empty; - public string RequestHash { get; set; } = string.Empty; - public Guid AnswerRecordId { get; set; } - public JsonElement ResponseSnapshot { get; set; } = JsonDefaults.Object(); - public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; -} - public enum AnswerGradingStatus { PendingReview, @@ -158,6 +151,19 @@ public sealed class WrongQuestion : ITenantOwned public Guid TenantId { get; set; } } +public sealed class WrongQuestionV2 : ITenantOwned +{ + public Guid UserId { get; set; } + public Guid QuestionAssetOwnerTenantId { get; set; } + public Guid QuestionAssetId { get; set; } + public Guid LastQuestionRevisionId { get; set; } + public Guid LastQuestionPlacementId { get; set; } + public int WrongCount { get; set; } = 1; + public DateTimeOffset LastWrongAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? ResolvedAt { get; set; } + public Guid TenantId { get; set; } +} + public sealed class RecentPractice : AuditableTenantEntity { public Guid? UserId { get; set; } diff --git a/Tiku.Domain/Learning/LearningReportEntities.cs b/Tiku.Domain/Learning/LearningReportEntities.cs index 1b332ef..1d48f91 100644 --- a/Tiku.Domain/Learning/LearningReportEntities.cs +++ b/Tiku.Domain/Learning/LearningReportEntities.cs @@ -216,6 +216,7 @@ public enum UserScoreEventType public enum PracticeUsageScopeType { Tenant, + ProductManifest, Region, Subject, QuestionBank, diff --git a/Tiku.Infrastructure/Catalog/TaxonomyService.cs b/Tiku.Infrastructure/Catalog/TaxonomyService.cs deleted file mode 100644 index bb81062..0000000 --- a/Tiku.Infrastructure/Catalog/TaxonomyService.cs +++ /dev/null @@ -1,156 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Tiku.Application.Catalog; -using Tiku.Application.QuestionBanks; -using Tiku.Application.Security; -using Tiku.Domain.Catalog; -using Tiku.Domain.Content; -using Tiku.Domain.Tenancy; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.Infrastructure.Catalog; - -public sealed class TaxonomyService( - ICatalogPersistence catalogPersistence, - IPublicQuestionAccessPolicy accessPolicy, - ITenantExecutionScope tenantExecutionScope) : ITaxonomyService -{ - public async Task> ListAsync( - Guid tenantId, - CancellationToken cancellationToken = default) - { - var includePlatform = true; - try - { - await accessPolicy.EnsureCanStartAsync(tenantId, cancellationToken); - } - catch (PublicQuestionAccessDeniedException) - { - includePlatform = false; - } - - return await tenantExecutionScope.ExecuteAsync( - new SystemScopeRequest( - tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(TaxonomyService), - "List platform taxonomy with tenant extensions", Guid.NewGuid().ToString("N")), - async (provider, token) => - { - var systemCatalog = provider.GetRequiredService(); - var systemTenancy = provider.GetRequiredService(); - var platformTenantId = includePlatform - ? await systemTenancy.Tenants.AsNoTracking() - .Where(tenant => tenant.Mode == TenantMode.PlatformOwned) - .Select(tenant => (Guid?)tenant.Id) - .SingleOrDefaultAsync(token) - : null; - return await systemCatalog.TaxonomyNodes.AsNoTracking() - .Where(node => - node.IsActive && - (node.TenantId == tenantId || - (platformTenantId.HasValue && node.TenantId == platformTenantId.Value))) - .OrderBy(node => node.Depth) - .ThenBy(node => node.SortOrder) - .Select(node => new TaxonomyNodeItem( - node.Id, - node.TenantId == tenantId ? QuestionSource.Tenant : QuestionSource.Platform, - node.ParentId, - !node.ParentOwnerTenantId.HasValue - ? null - : node.ParentOwnerTenantId == tenantId - ? QuestionSource.Tenant - : QuestionSource.Platform, - node.NodeType, - node.Code, - node.Name, - node.Path, - node.Depth, - node.SortOrder, - node.Metadata)) - .ToArrayAsync(token); - }, - cancellationToken); - } - - public async Task CreateAsync( - Guid tenantId, - CreateTaxonomyNodeCommand command, - CancellationToken cancellationToken = default) - { - Guid? parentOwnerTenantId = null; - TaxonomyParent? parent = null; - if (command.ParentId.HasValue) - { - parentOwnerTenantId = command.ParentSource switch - { - QuestionSource.Tenant => tenantId, - QuestionSource.Platform => await ResolvePlatformTenantIdAsync(tenantId, cancellationToken), - _ => throw new InvalidOperationException("A parent source is required when parentId is provided.") - }; - parent = await tenantExecutionScope.ExecuteAsync( - new SystemScopeRequest( - tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(TaxonomyService), - "Validate taxonomy extension parent ownership", Guid.NewGuid().ToString("N")), - async (provider, token) => - { - var systemCatalog = provider.GetRequiredService(); - var systemTenancy = provider.GetRequiredService(); - return await systemCatalog.TaxonomyNodes.AsNoTracking() - .Where(node => - node.TenantId == parentOwnerTenantId && - node.Id == command.ParentId.Value && - node.IsActive) - .Select(node => new TaxonomyParent(node.Path, node.Depth)) - .SingleOrDefaultAsync(token); - }, - cancellationToken) - ?? throw new InvalidOperationException("Taxonomy parent was not found."); - } - - var node = new TaxonomyNode - { - TenantId = tenantId, - ParentOwnerTenantId = parentOwnerTenantId, - ParentId = command.ParentId, - NodeType = command.NodeType, - Code = command.Code.Trim(), - Name = command.Name.Trim(), - Depth = parent is null ? 0 : parent.Depth + 1, - SortOrder = command.SortOrder, - Metadata = command.Metadata.Clone() - }; - node.Path = parent is null - ? $"n{node.Id:N}" - : $"{parent.Path}.n{node.Id:N}"; - catalogPersistence.TaxonomyNodes.Add(node); - await catalogPersistence.SaveChangesAsync(cancellationToken); - return new TaxonomyNodeItem( - node.Id, - QuestionSource.Tenant, - node.ParentId, - node.ParentId.HasValue ? command.ParentSource : null, - node.NodeType, - node.Code, - node.Name, - node.Path, - node.Depth, - node.SortOrder, - node.Metadata); - } - - private async Task ResolvePlatformTenantIdAsync(Guid tenantId, CancellationToken cancellationToken) - { - await accessPolicy.EnsureCanStartAsync(tenantId, cancellationToken); - return await tenantExecutionScope.ExecuteAsync( - new SystemScopeRequest( - tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(TaxonomyService), - "Resolve platform taxonomy owner", Guid.NewGuid().ToString("N")), - async (provider, token) => await provider.GetRequiredService() - .Tenants.AsNoTracking() - .Where(tenant => tenant.Mode == TenantMode.PlatformOwned) - .Select(tenant => tenant.Id) - .SingleAsync(token), - cancellationToken); - } - - private sealed record TaxonomyParent(string? Path, int Depth); -} diff --git a/Tiku.Infrastructure/Content/Collections/QuestionCollectionManagementService.cs b/Tiku.Infrastructure/Content/Collections/QuestionCollectionManagementService.cs deleted file mode 100644 index 8882cc1..0000000 --- a/Tiku.Infrastructure/Content/Collections/QuestionCollectionManagementService.cs +++ /dev/null @@ -1,183 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Tiku.Application.Catalog; -using Tiku.Application.Content; -using Tiku.Domain.Catalog; -using Tiku.Domain.Content; -using Tiku.Domain.QuestionBanks; -using Tiku.Infrastructure.Security; - -namespace Tiku.Infrastructure.Content; - -internal sealed class QuestionCollectionManagementService(ContentManagementDependencies dependencies) - : ContentManagementServiceBase(dependencies), IQuestionCollectionManagementService -{ - public async Task> GetCollectionsAsync( - ContentManagementActor actor, - ContentManagementFilter filter, - CancellationToken cancellationToken = default) - { - var scope = await RequireDataScopeAsync(actor, cancellationToken); - var regionIds = scope.RegionIds.ToArray(); - var query = questionBankPersistence.QuestionCollections - .AsNoTracking() - .Where(collection => collection.TenantId == actor.TenantId) - .ApplyDataScope( - scope, - collection => collection.CreatedBy == actor.UserId, - collection => collection.RegionId.HasValue && regionIds.Contains(collection.RegionId.Value)); - - if (!filter.IncludeInactive) query = query.Where(collection => collection.Status == ContentStatus.Active); - - if (filter.RegionId.HasValue) query = query.Where(collection => collection.RegionId == filter.RegionId.Value); - - if (filter.EntryId.HasValue) query = query.Where(collection => collection.EntryId == filter.EntryId.Value); - - if (filter.NodeId.HasValue) query = query.Where(collection => collection.NodeId == filter.NodeId.Value); - - if (TryParse(filter.CollectionType, out QuestionCollectionType collectionType)) - query = query.Where(collection => collection.CollectionType == collectionType); - - if (!string.IsNullOrWhiteSpace(filter.Keyword)) - { - var keyword = filter.Keyword.Trim(); - query = query.Where(collection => collection.Name.Contains(keyword)); - } - - var items = await query - .OrderBy(collection => collection.SortOrder) - .ThenBy(collection => collection.CreatedAt) - .Take(ResolveLimit(filter.Limit)) - .Select(collection => ToCollectionItem(collection)) - .ToArrayAsync(cancellationToken); - - return new CatalogList(items); - } - - public async Task> UpsertCollectionAsync( - ContentManagementActor actor, - UpsertQuestionCollectionCommand command, - CancellationToken cancellationToken = default) - { - var scope = await RequireDataScopeAsync(actor, cancellationToken); - ArgumentException.ThrowIfNullOrWhiteSpace(command.Name); - await AssertRegionAsync(actor.TenantId, command.RegionId, cancellationToken); - await AssertEntryAsync(actor, scope, command.EntryId, cancellationToken); - await AssertNodeAsync(actor, scope, command.NodeId, cancellationToken); - await AssertReferenceAsync(actor.TenantId, command.SubjectId, "subject_not_found", cancellationToken); - await AssertReferenceAsync(actor.TenantId, command.CategoryId, "category_not_found", - cancellationToken); - await AssertReferenceAsync(actor.TenantId, command.QuestionBankId, "question_bank_not_found", - cancellationToken); - - var collection = await ResolveEntityByIdOrLegacyAsync( - questionBankPersistence.QuestionCollections, - actor.TenantId, - command.Id, - command.LegacyId, - cancellationToken); - - var isNew = collection is null; - if (command.Id.HasValue && (collection is null || collection.Id != command.Id.Value)) - throw new ContentManagementException("Collection was not found.", "collection_not_found"); - - if (collection is not null && !scope.AllowsResource(actor.UserId, collection.CreatedBy, collection.RegionId)) - throw new ContentManagementException("Collection was not found.", "collection_not_found"); - - if (collection is null && !scope.AllowsResource(actor.UserId, actor.UserId, command.RegionId)) - throw new ContentManagementException("Collection was not found.", "collection_not_found"); - - collection ??= new QuestionCollection - { - Id = command.Id ?? Guid.NewGuid(), - TenantId = actor.TenantId, - CreatedBy = actor.UserId - }; - - collection.RegionId = command.RegionId; - collection.EntryId = command.EntryId; - collection.NodeId = command.NodeId; - collection.SubjectId = command.SubjectId; - collection.CategoryId = command.CategoryId; - collection.QuestionBankId = command.QuestionBankId; - collection.LegacyId = Normalize(command.LegacyId); - collection.Name = command.Name.Trim(); - collection.CollectionType = - Parse(command.CollectionType, QuestionCollectionType.Dynamic, "collection_type_invalid"); - collection.SourceType = Parse(command.SourceType, QuestionCollectionSourceType.Filters, - "collection_source_type_invalid"); - collection.Filters = JsonObjectOrDefault(command.Filters); - collection.TotalScore = command.TotalScore; - collection.DurationMinutes = command.DurationMinutes; - collection.Status = Parse(command.Status, ContentStatus.Active, "content_status_invalid"); - collection.SortOrder = command.Order ?? 0; - collection.AccessRules = JsonObjectOrDefault(command.AccessRules); - collection.Metadata = JsonObjectOrDefault(command.Metadata); - collection.UpdatedBy = actor.UserId; - - if (isNew) questionBankPersistence.QuestionCollections.Add(collection); - - await questionBankPersistence.SaveChangesAsync(cancellationToken); - return new ContentManagementResult(ToCollectionItem(collection)); - } - - public async Task ReplaceCollectionItemsAsync( - ContentManagementActor actor, - ReplaceCollectionItemsCommand command, - CancellationToken cancellationToken = default) - { - var scope = await RequireDataScopeAsync(actor, cancellationToken); - var regionIds = scope.RegionIds.ToArray(); - var collection = await questionBankPersistence.QuestionCollections - .Where(item => item.TenantId == actor.TenantId && item.Id == command.CollectionId) - .ApplyDataScope( - scope, - item => item.CreatedBy == actor.UserId, - item => item.RegionId.HasValue && regionIds.Contains(item.RegionId.Value)) - .SingleOrDefaultAsync(cancellationToken); - - if (collection is null) - throw new ContentManagementException("Collection was not found.", "collection_not_found"); - - var resolvedQuestions = new List<(CollectionQuestionCommand Command, TenantQuestionReference Reference)>(); - foreach (var question in command.Questions) - { - var reference = await questionReferenceService.ResolveAsync( - actor.TenantId, - actor.UserId, - question.Locator, - cancellationToken); - resolvedQuestions.Add((question, reference)); - } - - var oldItems = await questionBankPersistence.QuestionCollectionItems - .Where(item => item.TenantId == actor.TenantId && item.CollectionId == command.CollectionId) - .ToArrayAsync(cancellationToken); - questionBankPersistence.QuestionCollectionItems.RemoveRange(oldItems); - - var items = resolvedQuestions - .Select((resolved, index) => new QuestionCollectionItem - { - TenantId = actor.TenantId, - CollectionId = command.CollectionId, - QuestionReferenceId = resolved.Reference.Id, - QuestionOwnerTenantId = resolved.Reference.QuestionOwnerTenantId, - QuestionId = resolved.Reference.QuestionId, - SectionKey = Normalize(resolved.Command.SectionKey), - SortOrder = resolved.Command.Order ?? index, - Score = resolved.Command.Score, - Required = resolved.Command.Required ?? true, - Metadata = JsonObjectOrDefault(resolved.Command.Metadata) - }) - .ToArray(); - - questionBankPersistence.QuestionCollectionItems.AddRange(items); - collection.QuestionCount = items.Length; - collection.UpdatedBy = actor.UserId; - await questionBankPersistence.SaveChangesAsync(cancellationToken); - - return new CollectionItemsReplaceResult( - command.CollectionId, - collection.QuestionCount, - items.Select(ToCollectionItemItem).ToArray()); - } -} diff --git a/Tiku.Infrastructure/Content/ContentManagementDependencies.cs b/Tiku.Infrastructure/Content/ContentManagementDependencies.cs deleted file mode 100644 index 2139302..0000000 --- a/Tiku.Infrastructure/Content/ContentManagementDependencies.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Tiku.Application.Content; -using Tiku.Application.QuestionBanks; -using Tiku.Application.Security; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.Infrastructure.Content; - -internal sealed record ContentManagementDependencies( - IQuestionBankPersistence QuestionBankPersistence, - IQuestionReferenceService questionReferenceService, - ICurrentAccessContext currentAccessContext); - -internal abstract partial class ContentManagementServiceBase(ContentManagementDependencies dependencies) -{ - protected IQuestionBankPersistence questionBankPersistence { get; } = dependencies.QuestionBankPersistence; - protected IQuestionReferenceService questionReferenceService { get; } = dependencies.questionReferenceService; - protected ICurrentAccessContext currentAccessContext { get; } = dependencies.currentAccessContext; - protected const int DefaultLimit = 100; - protected const int MaxLimit = 1000; -} diff --git a/Tiku.Infrastructure/Content/ContentNavigationQueryService.cs b/Tiku.Infrastructure/Content/ContentNavigationQueryService.cs deleted file mode 100644 index 21ee36d..0000000 --- a/Tiku.Infrastructure/Content/ContentNavigationQueryService.cs +++ /dev/null @@ -1,353 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Tiku.Application.Catalog; -using Tiku.Application.Content; -using Tiku.Domain.Common; -using Tiku.Domain.Content; -using Tiku.Domain.Learning; -using Tiku.Domain.QuestionBanks; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.Infrastructure.Content; - -public sealed class ContentNavigationQueryService( - IQuestionBankPersistence dbContext, - ILearningPersistence learningPersistence) : IContentNavigationQueryService -{ - private const int DefaultLimit = 100; - private const int MaxLimit = 1000; - - public async Task> GetContentEntriesAsync( - ContentNavigationFilter filter, - CancellationToken cancellationToken = default) - { - var query = dbContext.ContentEntries - .AsNoTracking() - .Where(entry => - entry.TenantId == filter.TenantId && - entry.IsActive); - if (filter.AllowedRegionIds is not null) - query = query.Where(entry => entry.RegionId == null || - filter.AllowedRegionIds.Contains(entry.RegionId.Value)); - - if (!filter.IncludeHidden) query = query.Where(entry => entry.Visibility != ContentVisibility.Hidden); - - if (filter.RegionId.HasValue) - query = query.Where(entry => entry.RegionId == filter.RegionId.Value || entry.RegionId == null); - - if (TryParseEntryType(filter.EntryType, out var entryType)) - query = query.Where(entry => entry.EntryType == entryType); - - query = ApplyKeyword(query, filter.Keyword); - - var items = await query - .OrderBy(entry => entry.SortOrder) - .ThenBy(entry => entry.CreatedAt) - .Take(ResolveLimit(filter.Limit, 500)) - .Select(entry => new ContentEntryCatalogItem( - entry.Id, - entry.RegionId, - entry.LegacyId, - entry.EntryKey, - entry.Name, - entry.EntryType, - entry.Icon, - entry.Route, - entry.Description, - entry.Visibility, - entry.AccessRules, - entry.LayoutConfig, - entry.SortOrder)) - .ToArrayAsync(cancellationToken); - - return new CatalogList(items); - } - - public async Task> GetContentNodesAsync( - ContentNavigationFilter filter, - CancellationToken cancellationToken = default) - { - if (!filter.EntryId.HasValue) throw new RequiredFieldException("entryId is required."); - - var query = dbContext.ContentNodes - .AsNoTracking() - .Where(node => - node.TenantId == filter.TenantId && - node.EntryId == filter.EntryId.Value); - if (filter.AllowedRegionIds is not null) - query = query.Where(node => node.RegionId == null || - filter.AllowedRegionIds.Contains(node.RegionId.Value)); - - if (!filter.IncludeInactive) query = query.Where(node => node.IsActive); - - if (filter.RegionId.HasValue) - query = query.Where(node => node.RegionId == filter.RegionId.Value || node.RegionId == null); - - if (filter.ParentWasSpecified) - query = filter.ParentIsRoot - ? query.Where(node => node.ParentId == null) - : query.Where(node => node.ParentId == filter.ParentId); - - if (TryParseMarkerType(filter.MarkerType, out var markerType)) - query = query.Where(node => node.MarkerType == markerType); - - query = ApplyKeyword(query, filter.Keyword); - - query = string.Equals(filter.Mode, "flat", StringComparison.OrdinalIgnoreCase) - ? query.OrderBy(node => node.Path).ThenBy(node => node.SortOrder) - : query.OrderBy(node => node.SortOrder).ThenBy(node => node.CreatedAt); - - var items = await query - .Take(ResolveLimit(filter.Limit, 1000)) - .Select(node => new ContentNodeCatalogItem( - node.Id, - node.EntryId, - node.RegionId, - node.ParentId, - node.LegacyId, - node.NodeKey, - node.Name, - node.NodeType, - node.MarkerType, - node.MarkerConfig, - node.Path, - node.Depth, - node.SortOrder, - node.IsSelectable, - node.IsLeaf, - node.AccessRules, - node.Metadata)) - .ToArrayAsync(cancellationToken); - - return new CatalogList(items); - } - - public async Task> GetQuestionCollectionsAsync( - ContentNavigationFilter filter, - CancellationToken cancellationToken = default) - { - var query = dbContext.QuestionCollections - .AsNoTracking() - .Where(collection => - collection.TenantId == filter.TenantId && - collection.Status == ContentStatus.Active); - var allowedSliceIds = filter.AllowedContentSliceIds?.ToArray() ?? []; - query = query.Where(collection => learningPersistence.ContentSlices.Any(slice => - slice.TenantId == filter.TenantId && - allowedSliceIds.Contains(slice.Id) && - slice.Status == ContentSliceStatus.Active && - slice.ResourceType == LearningContentResourceType.Collection && - slice.ResourceId == collection.Id)); - - if (filter.RegionId.HasValue) - query = query.Where(collection => - collection.RegionId == filter.RegionId.Value || collection.RegionId == null); - - if (filter.EntryId.HasValue) query = query.Where(collection => collection.EntryId == filter.EntryId.Value); - - if (filter.NodeId.HasValue) query = query.Where(collection => collection.NodeId == filter.NodeId.Value); - - if (TryParseCollectionType(filter.CollectionType, out var collectionType)) - query = query.Where(collection => collection.CollectionType == collectionType); - - query = ApplyKeyword(query, filter.Keyword); - - var items = await query - .OrderBy(collection => collection.SortOrder) - .ThenBy(collection => collection.CreatedAt) - .Take(ResolveLimit(filter.Limit, 500)) - .Select(collection => new QuestionCollectionCatalogItem( - collection.Id, - collection.RegionId, - collection.EntryId, - collection.NodeId, - collection.SubjectId, - collection.CategoryId, - collection.QuestionBankId, - collection.LegacyId, - collection.Name, - collection.CollectionType, - collection.SourceType, - collection.Filters, - collection.QuestionCount, - collection.TotalScore, - collection.DurationMinutes, - collection.Status, - collection.SortOrder, - collection.AccessRules, - collection.Metadata)) - .ToArrayAsync(cancellationToken); - - return new CatalogList(items); - } - - public async Task> GetPracticeBlueprintsAsync( - ContentNavigationFilter filter, - CancellationToken cancellationToken = default) - { - var query = dbContext.PracticeBlueprints - .AsNoTracking() - .Where(blueprint => - blueprint.TenantId == filter.TenantId && - blueprint.Status == ContentStatus.Active); - var allowedSliceIds = filter.AllowedContentSliceIds?.ToArray() ?? []; - query = query.Where(blueprint => learningPersistence.ContentSlices.Any(slice => - slice.TenantId == filter.TenantId && - allowedSliceIds.Contains(slice.Id) && - slice.Status == ContentSliceStatus.Active && - slice.ResourceType == LearningContentResourceType.Blueprint && - slice.ResourceId == blueprint.Id)); - - if (filter.RegionId.HasValue) - query = query.Where(blueprint => blueprint.RegionId == filter.RegionId.Value || blueprint.RegionId == null); - - if (filter.EntryId.HasValue) query = query.Where(blueprint => blueprint.EntryId == filter.EntryId.Value); - - if (filter.NodeId.HasValue) query = query.Where(blueprint => blueprint.NodeId == filter.NodeId.Value); - - if (filter.CollectionId.HasValue) - query = query.Where(blueprint => blueprint.CollectionId == filter.CollectionId.Value); - - if (TryParsePracticeMode(filter.Mode, out var mode)) query = query.Where(blueprint => blueprint.Mode == mode); - - query = ApplyKeyword(query, filter.Keyword); - - var items = await query - .OrderBy(blueprint => blueprint.SortOrder) - .ThenBy(blueprint => blueprint.CreatedAt) - .Take(ResolveLimit(filter.Limit, 500)) - .Select(blueprint => new PracticeBlueprintCatalogItem( - blueprint.Id, - blueprint.RegionId, - blueprint.EntryId, - blueprint.NodeId, - blueprint.CollectionId, - blueprint.LegacyId, - blueprint.Name, - blueprint.Mode, - blueprint.AssemblyType, - blueprint.QuestionLimit, - blueprint.DurationMinutes, - blueprint.TotalScore, - blueprint.PassScore, - blueprint.Sections, - blueprint.Rules, - blueprint.AccessRules, - blueprint.Status, - blueprint.SortOrder)) - .ToArrayAsync(cancellationToken); - - return new CatalogList(items); - } - - public async Task> GetCollectionQuestionsAsync( - ContentNavigationFilter filter, - CancellationToken cancellationToken = default) - { - if (!filter.CollectionId.HasValue) throw new RequiredFieldException("collectionId is required."); - - var allowedSliceIds = filter.AllowedContentSliceIds?.ToArray() ?? []; - var collectionExists = await dbContext.QuestionCollections - .AsNoTracking() - .AnyAsync( - collection => - collection.TenantId == filter.TenantId && - collection.Id == filter.CollectionId.Value && - collection.Status == ContentStatus.Active && - learningPersistence.ContentSlices.Any(slice => - slice.TenantId == filter.TenantId && - allowedSliceIds.Contains(slice.Id) && - slice.Status == ContentSliceStatus.Active && - slice.ResourceType == LearningContentResourceType.Collection && - slice.ResourceId == collection.Id), - cancellationToken); - - if (!collectionExists) throw new ContentNavigationNotFoundException("Question collection was not found."); - - var emptyOptions = JsonDefaults.Array(); - var query = - from item in dbContext.QuestionCollectionItems.AsNoTracking() - join question in dbContext.Questions.AsNoTracking() - on new { item.TenantId, item.QuestionId } equals new { question.TenantId, QuestionId = question.Id } - join version in dbContext.QuestionVersions.AsNoTracking() - on new { question.TenantId, QuestionId = question.Id, VersionId = question.CurrentVersionId } - equals new { version.TenantId, version.QuestionId, VersionId = (Guid?)version.Id } - into versions - from version in versions.DefaultIfEmpty() - where item.TenantId == filter.TenantId && - item.CollectionId == filter.CollectionId.Value && - question.Status == QuestionStatus.Published - orderby item.SectionKey, item.SortOrder, question.CreatedAt - select new CollectionQuestionCatalogItem( - question.Id, - question.LegacyId, - question.EntryId, - question.ContentNodeId, - question.PrimaryCollectionId, - question.SubjectId, - question.CategoryId, - question.NodeId, - question.Type, - question.TypeLabel, - question.Difficulty, - question.Tags, - question.ExamMarkers, - question.MediaUrl, - question.HasVideoExplanation, - item.SectionKey, - item.Score, - item.SortOrder, - version == null ? null : version.Id, - version == null ? null : version.Content, - version == null ? emptyOptions : version.Options, - version == null ? null : version.CodeLang, - version == null ? null : version.CodeTemplate); - - var items = await query - .Take(ResolveLimit(filter.Limit, MaxLimit)) - .ToArrayAsync(cancellationToken); - - return new CatalogList(items); - } - - private static IQueryable ApplyKeyword(IQueryable query, string? keyword) - where T : class - { - if (string.IsNullOrWhiteSpace(keyword)) return query; - - var trimmed = keyword.Trim(); - return query.Where(entity => EF.Property(entity, nameof(ContentEntry.Name)).Contains(trimmed)); - } - - private static int ResolveLimit(int? limit, int defaultLimit) - { - return Math.Clamp(limit ?? defaultLimit, 1, MaxLimit); - } - - private static bool TryParseEntryType(string? value, out ContentEntryType type) - { - return Enum.TryParse(NormalizeEnumValue(value), true, out type); - } - - private static bool TryParseCollectionType(string? value, out QuestionCollectionType type) - { - return Enum.TryParse(NormalizeEnumValue(value), true, out type); - } - - private static bool TryParsePracticeMode(string? value, out PracticeMode mode) - { - return Enum.TryParse(NormalizeEnumValue(value), true, out mode); - } - - private static bool TryParseMarkerType(string? value, out ContentMarkerType type) - { - return Enum.TryParse(NormalizeEnumValue(value), true, out type); - } - - private static string? NormalizeEnumValue(string? value) - { - return string.IsNullOrWhiteSpace(value) - ? null - : value.Replace("_", string.Empty, StringComparison.Ordinal) - .Replace("-", string.Empty, StringComparison.Ordinal); - } -} diff --git a/Tiku.Infrastructure/Content/Entries/ContentEntryManagementService.cs b/Tiku.Infrastructure/Content/Entries/ContentEntryManagementService.cs deleted file mode 100644 index ff55314..0000000 --- a/Tiku.Infrastructure/Content/Entries/ContentEntryManagementService.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Tiku.Application.Catalog; -using Tiku.Application.Content; -using Tiku.Domain.Content; -using Tiku.Infrastructure.Security; - -namespace Tiku.Infrastructure.Content; - -internal sealed class ContentEntryManagementService(ContentManagementDependencies dependencies) - : ContentManagementServiceBase(dependencies), IContentEntryManagementService -{ - public async Task> GetEntriesAsync( - ContentManagementActor actor, - ContentManagementFilter filter, - CancellationToken cancellationToken = default) - { - var scope = await RequireDataScopeAsync(actor, cancellationToken); - var regionIds = scope.RegionIds.ToArray(); - var query = questionBankPersistence.ContentEntries - .AsNoTracking() - .Where(entry => entry.TenantId == actor.TenantId) - .ApplyDataScope( - scope, - entry => entry.CreatedBy == actor.UserId, - entry => entry.RegionId.HasValue && regionIds.Contains(entry.RegionId.Value)); - - if (!filter.IncludeInactive) query = query.Where(entry => entry.IsActive); - - if (filter.RegionId.HasValue) query = query.Where(entry => entry.RegionId == filter.RegionId.Value); - - if (TryParse(filter.EntryType, out ContentEntryType entryType)) - query = query.Where(entry => entry.EntryType == entryType); - - if (!string.IsNullOrWhiteSpace(filter.Keyword)) - { - var keyword = filter.Keyword.Trim(); - query = query.Where(entry => - entry.Name.Contains(keyword) || - entry.EntryKey.Contains(keyword) || - (entry.Description != null && entry.Description.Contains(keyword))); - } - - var items = await query - .OrderBy(entry => entry.SortOrder) - .ThenBy(entry => entry.CreatedAt) - .Take(ResolveLimit(filter.Limit)) - .Select(entry => ToEntryItem(entry)) - .ToArrayAsync(cancellationToken); - - return new CatalogList(items); - } - - public async Task> UpsertEntryAsync( - ContentManagementActor actor, - UpsertContentEntryCommand command, - CancellationToken cancellationToken = default) - { - var scope = await RequireDataScopeAsync(actor, cancellationToken); - ArgumentException.ThrowIfNullOrWhiteSpace(command.Name); - await AssertRegionAsync(actor.TenantId, command.RegionId, cancellationToken); - - var entryKey = Normalize(command.EntryKey) ?? - Normalize(command.Id?.ToString("N")) ?? - Guid.NewGuid().ToString("N"); - var entry = await ResolveEntityAsync( - questionBankPersistence.ContentEntries, - actor.TenantId, - command.Id, - item => item.EntryKey == entryKey, - cancellationToken); - - var isNew = entry is null; - if (command.Id.HasValue && (entry is null || entry.Id != command.Id.Value)) - throw new ContentManagementException("Content entry was not found.", "entry_not_found"); - - if (entry is not null && !scope.AllowsResource(actor.UserId, entry.CreatedBy, entry.RegionId)) - throw new ContentManagementException("Content entry was not found.", "entry_not_found"); - - if (entry is null && !scope.AllowsResource(actor.UserId, actor.UserId, command.RegionId)) - throw new ContentManagementException("Content entry was not found.", "entry_not_found"); - - entry ??= new ContentEntry - { - Id = command.Id ?? Guid.NewGuid(), - TenantId = actor.TenantId, - EntryKey = entryKey, - CreatedBy = actor.UserId - }; - - entry.RegionId = command.RegionId; - entry.LegacyId = Normalize(command.LegacyId); - entry.Name = command.Name.Trim(); - entry.EntryType = Parse(command.EntryType, ContentEntryType.QuestionPractice, "entry_type_invalid"); - entry.Icon = Normalize(command.Icon); - entry.Route = Normalize(command.Route); - entry.Description = Normalize(command.Description); - entry.Visibility = Parse(command.Visibility, ContentVisibility.Public, "visibility_invalid"); - entry.AccessRules = JsonObjectOrDefault(command.AccessRules); - entry.LayoutConfig = JsonObjectOrDefault(command.LayoutConfig); - entry.SortOrder = command.Order ?? 0; - entry.IsActive = command.IsActive ?? true; - entry.UpdatedBy = actor.UserId; - - if (isNew) questionBankPersistence.ContentEntries.Add(entry); - - await questionBankPersistence.SaveChangesAsync(cancellationToken); - return new ContentManagementResult(ToEntryItem(entry)); - } -} diff --git a/Tiku.Infrastructure/Content/Foundation/ContentManagementFoundation.cs b/Tiku.Infrastructure/Content/Foundation/ContentManagementFoundation.cs deleted file mode 100644 index 8a2c8d2..0000000 --- a/Tiku.Infrastructure/Content/Foundation/ContentManagementFoundation.cs +++ /dev/null @@ -1,500 +0,0 @@ -using System.Linq.Expressions; -using System.Text.Json; -using Microsoft.EntityFrameworkCore; -using Tiku.Application.Content; -using Tiku.Application.QuestionBanks; -using Tiku.Application.Security; -using Tiku.Domain.Catalog; -using Tiku.Domain.Common; -using Tiku.Domain.Content; -using Tiku.Infrastructure.Security; - -namespace Tiku.Infrastructure.Content; - -internal abstract partial class ContentManagementServiceBase -{ - protected static readonly IReadOnlyDictionary Specs = - new Dictionary(StringComparer.Ordinal) - { - ["questions"] = new( - "questions", - "题目导入模板", - "用于导入刷题题库,后端会校验题型、答案、目标科目、分类、题集和租户隔离。", - [ - Field("legacyId", "旧系统 ID", false, ["legacy_id", "externalId", "id"], "用于幂等更新。", - "tj-english-2026-001"), - Field("type", "题型", true, ["题型", "questionType"], "choice、multi、judge、reading、short_answer 等。", - "choice"), - Field("content", "题干", true, ["题干", "stem", "question"], "支持 Markdown、图片 URL 和公式。", - "多租户 SaaS 最重要的安全边界是什么?"), - Field("options", "选项", false, ["选项", "choices"], "客观题选项。", new[] { "前端隐藏", "后端权限" }), - Field("correctOptionIndices", "正确选项索引", false, ["答案", "answer"], "从 0 开始;CSV 可用 A/B/C/D。", - new[] { 1 }), - Field("answerText", "文字答案", false, ["主观题答案"], "主观题答案。", "以后端权限和数据库约束为准。"), - Field("explanation", "解析", false, ["解析", "analysis"], "题目解析内容。", "最终权限以后端强制为准。"), - Field("difficulty", "难度", false, ["难度"], "建议 1-5。", 2), - Field("tags", "标签", false, ["标签", "tag"], "JSON 数组或 CSV 中用 | 分隔。", new[] { "安全", "多租户" }) - ], - [ - ["legacyId", "type", "content", "选项A", "选项B", "答案", "explanation", "difficulty", "tags"], - [ - "tj-english-2026-001", "choice", "多租户 SaaS 最重要的安全边界是什么?", "前端隐藏", "后端权限", "B", "最终权限以后端强制为准。", - "2", "安全|多租户" - ] - ], - new - { - items = new[] - { - new - { - legacyId = "tj-english-2026-001", - type = "choice", - content = "多租户 SaaS 最重要的安全边界是什么?", - options = new[] { "前端隐藏", "后端权限" }, - correctOptionIndices = new[] { 1 }, - explanation = "最终权限以后端强制为准。", - difficulty = 2, - tags = new[] { "安全", "多租户" } - } - } - }), - ["vocabulary"] = new( - "vocabulary", - "单词导入模板", - "用于导入词汇单元和单词,后端会按单元归组并幂等写入。", - [ - Field("unitName", "单元名称", true, ["unit", "单元"], "单词所属单元。", "核心词汇 Unit 1"), - Field("word", "单词", true, ["单词"], "英文单词或词组。", "scale"), - Field("meaning", "释义", true, ["释义", "中文"], "中文释义。", "n. 规模;等级"), - Field("phonetic", "音标", false, ["音标"], "音标展示文本。", "/skeil/"), - Field("example", "例句", false, ["例句"], "英文例句。", "The platform must scale safely.") - ], - [ - ["unitName", "word", "phonetic", "meaning", "example", "difficulty", "tags"], - ["核心词汇 Unit 1", "scale", "/skeil/", "n. 规模;等级", "The platform must scale safely.", "2", "高频|SaaS"] - ], - new - { - units = new[] - { new { name = "核心词汇 Unit 1", words = new[] { new { word = "scale", meaning = "n. 规模;等级" } } } } - }), - ["handbook"] = new( - "handbook", - "知识手册导入模板", - "用于导入手册科目、章节、小节和知识点。", - [ - Field("subjectName", "手册科目", true, ["subject", "手册"], "知识手册顶层名称。", "专升本英语知识手册"), - Field("chapterName", "章节", true, ["chapter", "章节"], "章节名称。", "第一章 语法基础"), - Field("title", "知识点标题", true, ["entryTitle", "标题"], "知识点条目标题。", "that 引导的主语从句"), - Field("content", "正文", true, ["正文", "markdown"], "Markdown 正文。", "主语从句可放在句首。") - ], - [ - ["subjectName", "chapterName", "title", "content", "tags"], - ["专升本英语知识手册", "第一章 语法基础", "that 引导的主语从句", "主语从句可放在句首。", "语法"] - ], - new - { - subjects = new[] { new { name = "专升本英语知识手册", chapters = new[] { new { name = "第一章 语法基础" } } } } - }), - ["scoreline"] = new( - "scoreline", - "分数线导入模板", - "用于导入动态字段、院校、专业和年份分数线记录。", - [ - Field("kind", "数据类型", true, ["type", "类型"], "field、school、major、record。", "record"), - Field("schoolName", "院校名称", false, ["school", "院校"], "院校名称。", "天津职业技术师范大学"), - Field("majorName", "专业名称", false, ["major", "专业"], "专业名称。", "软件工程"), - Field("year", "年份", false, ["年份"], "record 常用。", 2026), - Field("fieldValues", "字段值", false, ["values", "分数字段"], "record 的动态字段 JSON。", new { minScore = 188 }) - ], - [ - ["kind", "schoolName", "majorName", "year", "minScore"], - ["record", "天津职业技术师范大学", "软件工程", "2026", "188"] - ], - new - { - records = new[] - { - new - { - schoolName = "天津职业技术师范大学", majorName = "软件工程", year = 2026, - fieldValues = new { minScore = 188 } - } - } - }), - ["videos"] = new( - "videos", - "视频解析导入模板", - "用于导入视频解析元数据并绑定到题目。", - [ - Field("title", "标题", true, ["视频标题", "name"], "视频标题。", "多租户隔离题解析"), - Field("videoUrl", "视频 URL", false, ["video_url", "url"], "外部视频 URL。", - "https://cdn.example.test/video.mp4"), - Field("assetId", "资源 ID", false, ["asset_id"], "对象存储资源台账 ID。", - "00000000-0000-0000-0000-000000000000"), - Field("legacyQuestionId", "题目外部 ID", false, ["legacy_question_id"], "按旧题目 ID 绑定。", - "tj-english-2026-001") - ], - [ - ["title", "videoUrl", "legacyQuestionId", "videoType"], - ["多租户隔离题解析", "https://cdn.example.test/video.mp4", "tj-english-2026-001", "specific"] - ], - new { videos = new[] { new { title = "多租户隔离题解析", videoUrl = "https://cdn.example.test/video.mp4" } } }) - }; - - protected async Task<(string Path, int Depth)> BuildNodePathAsync( - Guid tenantId, - Guid entryId, - Guid nodeId, - Guid? parentId, - CancellationToken cancellationToken) - { - var label = $"n_{nodeId:N}"; - if (!parentId.HasValue) return (label, 0); - - var parent = await questionBankPersistence.ContentNodes - .AsNoTracking() - .Where(node => node.TenantId == tenantId && node.EntryId == entryId && node.Id == parentId.Value) - .Select(node => new { node.Path, node.Depth }) - .SingleOrDefaultAsync(cancellationToken); - - if (parent is null) - throw new ContentManagementException("Parent node was not found in this entry.", "parent_node_not_found"); - - return ($"{parent.Path}.{label}", parent.Depth + 1); - } - - protected async Task AssertRegionAsync(Guid tenantId, Guid? regionId, CancellationToken cancellationToken) - { - await AssertReferenceAsync(tenantId, regionId, "region_not_found", cancellationToken); - } - - protected async Task AssertEntryAsync(Guid tenantId, Guid? entryId, CancellationToken cancellationToken) - { - await AssertReferenceAsync(tenantId, entryId, "entry_not_found", cancellationToken); - } - - protected async Task AssertEntryAsync( - ContentManagementActor actor, - CurrentDataScope scope, - Guid? entryId, - CancellationToken cancellationToken) - { - if (!entryId.HasValue) return; - - var regionIds = scope.RegionIds.ToArray(); - var exists = await questionBankPersistence.ContentEntries - .Where(entry => entry.TenantId == actor.TenantId && entry.Id == entryId.Value) - .ApplyDataScope( - scope, - entry => entry.CreatedBy == actor.UserId, - entry => entry.RegionId.HasValue && regionIds.Contains(entry.RegionId.Value)) - .AnyAsync(cancellationToken); - if (!exists) throw new ContentManagementException("Content entry was not found.", "entry_not_found"); - } - - protected async Task AssertNodeAsync(Guid tenantId, Guid? nodeId, CancellationToken cancellationToken) - { - await AssertReferenceAsync(tenantId, nodeId, "node_not_found", cancellationToken); - } - - protected async Task AssertNodeAsync( - ContentManagementActor actor, - CurrentDataScope scope, - Guid? nodeId, - CancellationToken cancellationToken) - { - if (!nodeId.HasValue) return; - - var regionIds = scope.RegionIds.ToArray(); - var exists = await questionBankPersistence.ContentNodes - .Where(node => node.TenantId == actor.TenantId && node.Id == nodeId.Value) - .ApplyDataScope( - scope, - node => node.CreatedBy == actor.UserId, - node => node.RegionId.HasValue && regionIds.Contains(node.RegionId.Value)) - .AnyAsync(cancellationToken); - if (!exists) throw new ContentManagementException("Content node was not found.", "node_not_found"); - } - - protected async Task RequireDataScopeAsync( - ContentManagementActor actor, - CancellationToken cancellationToken) - { - var access = await currentAccessContext.GetAsync(cancellationToken); - if (!access.IsCurrentTenantMember || access.UserId != actor.UserId || access.TenantId != actor.TenantId) - throw new ContentManagementException("Content resource was not found.", "content_not_found"); - - return access.DataScope; - } - - protected async Task AssertReferenceAsync( - Guid tenantId, - Guid? id, - string code, - CancellationToken cancellationToken) - where TEntity : class - { - if (!id.HasValue) return; - - var exists = await questionBankPersistence.Set() - .AnyAsync(entity => - EF.Property(entity, nameof(ContentEntry.TenantId)) == tenantId && - EF.Property(entity, nameof(ContentEntry.Id)) == id.Value, - cancellationToken); - - if (!exists) throw new ContentManagementException("Referenced entity was not found in this tenant.", code); - } - - protected static async Task ResolveEntityAsync( - DbSet set, - Guid tenantId, - Guid? id, - Expression> alternatePredicate, - CancellationToken cancellationToken) - where TEntity : class - { - if (id.HasValue) - { - var byId = await set.SingleOrDefaultAsync(entity => - EF.Property(entity, nameof(ContentEntry.TenantId)) == tenantId && - EF.Property(entity, nameof(ContentEntry.Id)) == id.Value, - cancellationToken); - if (byId is not null) return byId; - } - - return await set - .Where(entity => EF.Property(entity, nameof(ContentEntry.TenantId)) == tenantId) - .SingleOrDefaultAsync(alternatePredicate, cancellationToken); - } - - protected static async Task ResolveEntityByIdOrLegacyAsync( - DbSet set, - Guid tenantId, - Guid? id, - string? legacyId, - CancellationToken cancellationToken) - where TEntity : class - { - if (id.HasValue) - { - var byId = await set.SingleOrDefaultAsync(entity => - EF.Property(entity, nameof(ContentEntry.TenantId)) == tenantId && - EF.Property(entity, nameof(ContentEntry.Id)) == id.Value, - cancellationToken); - if (byId is not null) return byId; - } - - var normalizedLegacyId = Normalize(legacyId); - if (normalizedLegacyId is null) return null; - - return await set.SingleOrDefaultAsync(entity => - EF.Property(entity, nameof(ContentEntry.TenantId)) == tenantId && - EF.Property(entity, nameof(ContentEntry.LegacyId)) == normalizedLegacyId, - cancellationToken); - } - - protected static ContentEntryManagementItem ToEntryItem(ContentEntry entry) - { - return new ContentEntryManagementItem( - entry.Id, - entry.RegionId, - entry.LegacyId, - entry.EntryKey, - entry.Name, - entry.EntryType, - entry.Icon, - entry.Route, - entry.Description, - entry.Visibility, - entry.AccessRules, - entry.LayoutConfig, - entry.SortOrder, - entry.IsActive, - entry.CreatedAt, - entry.UpdatedAt); - } - - protected static ContentNodeManagementItem ToNodeItem(ContentNode node) - { - return new ContentNodeManagementItem( - node.Id, - node.EntryId, - node.RegionId, - node.ParentId, - node.LegacyId, - node.NodeKey, - node.Name, - node.NodeType, - node.MarkerType, - node.MarkerConfig, - node.Path, - node.Depth, - node.SortOrder, - node.IsActive, - node.IsSelectable, - node.IsLeaf, - node.AccessRules, - node.Metadata, - node.CreatedAt, - node.UpdatedAt); - } - - protected static QuestionCollectionManagementItem ToCollectionItem(QuestionCollection collection) - { - return new QuestionCollectionManagementItem( - collection.Id, - collection.RegionId, - collection.EntryId, - collection.NodeId, - collection.SubjectId, - collection.CategoryId, - collection.QuestionBankId, - collection.LegacyId, - collection.Name, - collection.CollectionType, - collection.SourceType, - collection.Filters, - collection.QuestionCount, - collection.TotalScore, - collection.DurationMinutes, - collection.Status, - collection.SortOrder, - collection.AccessRules, - collection.Metadata, - collection.CreatedAt, - collection.UpdatedAt); - } - - protected static QuestionCollectionItemManagementItem ToCollectionItemItem(QuestionCollectionItem item) - { - return new QuestionCollectionItemManagementItem( - item.Id, - item.CollectionId, - item.QuestionId, - new QuestionLocator( - item.QuestionOwnerTenantId == item.TenantId ? QuestionSource.Tenant : QuestionSource.Platform, - item.QuestionId), - item.SectionKey, - item.SortOrder, - item.Score, - item.Required, - item.Metadata); - } - - protected static PracticeBlueprintManagementItem ToBlueprintItem(PracticeBlueprint blueprint) - { - return new PracticeBlueprintManagementItem( - blueprint.Id, - blueprint.RegionId, - blueprint.EntryId, - blueprint.NodeId, - blueprint.CollectionId, - blueprint.LegacyId, - blueprint.Name, - blueprint.Mode, - blueprint.AssemblyType, - blueprint.QuestionLimit, - blueprint.DurationMinutes, - blueprint.TotalScore, - blueprint.PassScore, - blueprint.Sections, - blueprint.Rules, - blueprint.AccessRules, - blueprint.Status, - blueprint.SortOrder, - blueprint.CreatedAt, - blueprint.UpdatedAt); - } - - protected static int ResolveLimit(int? limit) - { - return limit is > 0 ? Math.Min(limit.Value, MaxLimit) : DefaultLimit; - } - - protected static string? Normalize(string? value) - { - return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); - } - - protected static JsonElement JsonObjectOrDefault(JsonElement value) - { - return value.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null - ? JsonDefaults.Object() - : value; - } - - protected static JsonElement JsonArrayOrDefault(JsonElement value) - { - return value.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null - ? JsonDefaults.Array() - : value; - } - - protected static TEnum Parse(string? value, TEnum fallback, string code) - where TEnum : struct - { - if (string.IsNullOrWhiteSpace(value)) return fallback; - - if (Enum.TryParse(value, true, out var parsed)) return parsed; - - throw new ContentManagementException("Invalid enum value.", code); - } - - protected static TEnum? ParseNullable(string? value, string code) - where TEnum : struct - { - if (string.IsNullOrWhiteSpace(value)) return null; - - if (Enum.TryParse(value, true, out var parsed)) return parsed; - - throw new ContentManagementException("Invalid enum value.", code); - } - - protected static bool TryParse(string? value, out TEnum parsed) - where TEnum : struct - { - return Enum.TryParse(value, true, out parsed); - } - - protected static string EscapeCsv(string value) - { - return value.Contains(',') || value.Contains('"') || value.Contains('\n') - ? $"\"{value.Replace("\"", "\"\"", StringComparison.Ordinal)}\"" - : value; - } - - protected static ImportSpec ResolveImportSpec(string importType) - { - var normalized = importType.Trim().ToLowerInvariant(); - return Specs.TryGetValue(normalized, out var spec) - ? spec - : throw new ContentManagementException("Import type is not supported.", "import_type_invalid"); - } - - protected static ImportFieldSpec Field( - string field, - string label, - bool required, - string[] aliases, - string description, - object example) - { - return new ImportFieldSpec( - field, - label, - required, - aliases, - description, - JsonSerializer.SerializeToElement(example)); - } - - protected sealed record ImportSpec( - string ImportType, - string Title, - string Description, - IReadOnlyCollection Fields, - string[][] CsvRows, - object JsonExample); -} diff --git a/Tiku.Infrastructure/Content/Imports/ContentImportService.cs b/Tiku.Infrastructure/Content/Imports/ContentImportService.cs index 96f1f8e..525d2bf 100644 --- a/Tiku.Infrastructure/Content/Imports/ContentImportService.cs +++ b/Tiku.Infrastructure/Content/Imports/ContentImportService.cs @@ -14,7 +14,6 @@ namespace Tiku.Infrastructure.Content; internal sealed class ContentImportService( DirectContentServiceDependencies dependencies, - IQuestionManagementService questionService, IVocabularyManagementService vocabularyService, IHandbookManagementService handbookService, IScorelineManagementService scorelineService, @@ -241,35 +240,9 @@ internal sealed class ContentImportService( switch (command.ImportType.ToLowerInvariant()) { case "questions": - var result = await questionService.CreateQuestionAsync(actor, new QuestionWriteCommand( - null, - command.QuestionBankId, - command.SubjectId, - command.CategoryId, - null, - command.EntryId, - command.ContentNodeId, - command.CollectionId, - GetString(payload, "legacyId"), - GetString(payload, "type") ?? "choice", - GetString(payload, "typeLabel"), - GetInt(payload, "difficulty"), - GetElement(payload, "tags", JsonDefaults.Array()), - GetString(payload, "content") ?? GetString(payload, "title"), - GetElement(payload, "options", JsonDefaults.Array()), - GetInt(payload, "correctOptionIndex"), - GetElement(payload, "correctOptionIndices", JsonDefaults.Array()), - GetString(payload, "answerText") ?? GetString(payload, "answer"), - GetString(payload, "explanation"), - GetElement(payload, "subQuestions", JsonDefaults.Array()), - GetString(payload, "codeLang"), - GetString(payload, "codeTemplate"), - GetString(payload, "mediaUrl"), - "Published", - GetElement(payload, "examMarkers", JsonDefaults.Object()), - GetString(payload, "sourceHash"), - true), cancellationToken); - return ("question", result.Item.Id); + throw new ContentManagementException( + "Question imports must use the V2 preview and decision workflow.", + "question_import_v1_retired"); case "vocabulary": var word = await vocabularyService.UpsertVocabularyWordAsync(actor, new VocabularyWordCommand( null, diff --git a/Tiku.Infrastructure/Content/Nodes/ContentNodeManagementService.cs b/Tiku.Infrastructure/Content/Nodes/ContentNodeManagementService.cs deleted file mode 100644 index 9c8fcf7..0000000 --- a/Tiku.Infrastructure/Content/Nodes/ContentNodeManagementService.cs +++ /dev/null @@ -1,138 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Tiku.Application.Catalog; -using Tiku.Application.Content; -using Tiku.Domain.Content; -using Tiku.Infrastructure.Security; - -namespace Tiku.Infrastructure.Content; - -internal sealed class ContentNodeManagementService(ContentManagementDependencies dependencies) - : ContentManagementServiceBase(dependencies), IContentNodeManagementService -{ - public async Task> GetNodesAsync( - ContentManagementActor actor, - ContentManagementFilter filter, - CancellationToken cancellationToken = default) - { - var scope = await RequireDataScopeAsync(actor, cancellationToken); - if (!filter.EntryId.HasValue) throw new ContentManagementException("entryId is required.", "entry_id_required"); - - await AssertEntryAsync(actor, scope, filter.EntryId, cancellationToken); - var regionIds = scope.RegionIds.ToArray(); - var query = questionBankPersistence.ContentNodes - .AsNoTracking() - .Where(node => node.TenantId == actor.TenantId && node.EntryId == filter.EntryId.Value) - .ApplyDataScope( - scope, - node => node.CreatedBy == actor.UserId, - node => node.RegionId.HasValue && regionIds.Contains(node.RegionId.Value)); - - if (!filter.IncludeInactive) query = query.Where(node => node.IsActive); - - if (filter.RegionId.HasValue) query = query.Where(node => node.RegionId == filter.RegionId.Value); - - if (filter.ParentId is not null) - { - if (string.Equals(filter.ParentId, "root", StringComparison.OrdinalIgnoreCase)) - query = query.Where(node => node.ParentId == null); - else if (Guid.TryParse(filter.ParentId, out var parentId)) - query = query.Where(node => node.ParentId == parentId); - } - - if (TryParse(filter.MarkerType, out ContentMarkerType markerType)) - query = query.Where(node => node.MarkerType == markerType); - - if (!string.IsNullOrWhiteSpace(filter.Keyword)) - { - var keyword = filter.Keyword.Trim(); - query = query.Where(node => - node.Name.Contains(keyword) || - (node.NodeKey != null && node.NodeKey.Contains(keyword))); - } - - query = string.Equals(filter.Mode, "flat", StringComparison.OrdinalIgnoreCase) - ? query.OrderBy(node => node.Path).ThenBy(node => node.SortOrder) - : query.OrderBy(node => node.SortOrder).ThenBy(node => node.CreatedAt); - - var items = await query - .Take(ResolveLimit(filter.Limit)) - .Select(node => ToNodeItem(node)) - .ToArrayAsync(cancellationToken); - - return new CatalogList(items); - } - - public async Task> UpsertNodeAsync( - ContentManagementActor actor, - UpsertContentNodeCommand command, - CancellationToken cancellationToken = default) - { - var scope = await RequireDataScopeAsync(actor, cancellationToken); - ArgumentException.ThrowIfNullOrWhiteSpace(command.Name); - await AssertEntryAsync(actor, scope, command.EntryId, cancellationToken); - await AssertRegionAsync(actor.TenantId, command.RegionId, cancellationToken); - await AssertNodeAsync(actor, scope, command.ParentId, cancellationToken); - - var nodeKey = Normalize(command.NodeKey) ?? - Normalize(command.Id?.ToString("N")) ?? - Guid.NewGuid().ToString("N"); - var node = await ResolveEntityAsync( - questionBankPersistence.ContentNodes, - actor.TenantId, - command.Id, - item => item.EntryId == command.EntryId && item.NodeKey == nodeKey, - cancellationToken); - - var isNew = node is null; - if (command.Id.HasValue && (node is null || node.Id != command.Id.Value)) - throw new ContentManagementException("Content node was not found.", "node_not_found"); - - if (node is not null && !scope.AllowsResource(actor.UserId, node.CreatedBy, node.RegionId)) - throw new ContentManagementException("Content node was not found.", "node_not_found"); - - if (node is null && !scope.AllowsResource(actor.UserId, actor.UserId, command.RegionId)) - throw new ContentManagementException("Content node was not found.", "node_not_found"); - - node ??= new ContentNode - { - Id = command.Id ?? Guid.NewGuid(), - TenantId = actor.TenantId, - EntryId = command.EntryId, - NodeKey = nodeKey, - CreatedBy = actor.UserId - }; - - var path = await BuildNodePathAsync(actor.TenantId, command.EntryId, node.Id, command.ParentId, - cancellationToken); - node.EntryId = command.EntryId; - node.RegionId = command.RegionId; - node.ParentId = command.ParentId; - node.LegacyId = Normalize(command.LegacyId); - node.Name = command.Name.Trim(); - node.NodeType = Parse(command.NodeType, ContentNodeType.Category, "node_type_invalid"); - node.MarkerType = ParseNullable(command.MarkerType, "marker_type_invalid"); - node.MarkerConfig = JsonObjectOrDefault(command.MarkerConfig); - node.Path = path.Path; - node.Depth = path.Depth; - node.SortOrder = command.Order ?? 0; - node.IsActive = command.IsActive ?? true; - node.IsSelectable = command.IsSelectable ?? true; - node.IsLeaf = command.IsLeaf ?? false; - node.AccessRules = JsonObjectOrDefault(command.AccessRules); - node.Metadata = JsonObjectOrDefault(command.Metadata); - node.UpdatedBy = actor.UserId; - - if (isNew) questionBankPersistence.ContentNodes.Add(node); - - if (command.ParentId.HasValue) - { - var parent = await questionBankPersistence.ContentNodes.SingleOrDefaultAsync( - item => item.TenantId == actor.TenantId && item.Id == command.ParentId.Value, - cancellationToken); - if (parent is not null) parent.IsLeaf = false; - } - - await questionBankPersistence.SaveChangesAsync(cancellationToken); - return new ContentManagementResult(ToNodeItem(node)); - } -} diff --git a/Tiku.Infrastructure/Content/PracticeBlueprints/PracticeBlueprintManagementService.cs b/Tiku.Infrastructure/Content/PracticeBlueprints/PracticeBlueprintManagementService.cs deleted file mode 100644 index 3187a75..0000000 --- a/Tiku.Infrastructure/Content/PracticeBlueprints/PracticeBlueprintManagementService.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System.Text; -using System.Text.Json; -using Microsoft.EntityFrameworkCore; -using Tiku.Application.Catalog; -using Tiku.Application.Content; -using Tiku.Domain.Content; -using Tiku.Infrastructure.Security; - -namespace Tiku.Infrastructure.Content; - -internal sealed class PracticeBlueprintManagementService(ContentManagementDependencies dependencies) - : ContentManagementServiceBase(dependencies), IPracticeBlueprintManagementService -{ - public async Task> GetPracticeBlueprintsAsync( - ContentManagementActor actor, - ContentManagementFilter filter, - CancellationToken cancellationToken = default) - { - var scope = await RequireDataScopeAsync(actor, cancellationToken); - var regionIds = scope.RegionIds.ToArray(); - var query = questionBankPersistence.PracticeBlueprints - .AsNoTracking() - .Where(blueprint => blueprint.TenantId == actor.TenantId) - .ApplyDataScope( - scope, - blueprint => blueprint.CreatedBy == actor.UserId, - blueprint => blueprint.RegionId.HasValue && regionIds.Contains(blueprint.RegionId.Value)); - - if (!filter.IncludeInactive) query = query.Where(blueprint => blueprint.Status == ContentStatus.Active); - - if (filter.RegionId.HasValue) query = query.Where(blueprint => blueprint.RegionId == filter.RegionId.Value); - - if (filter.EntryId.HasValue) query = query.Where(blueprint => blueprint.EntryId == filter.EntryId.Value); - - if (filter.NodeId.HasValue) query = query.Where(blueprint => blueprint.NodeId == filter.NodeId.Value); - - if (filter.CollectionId.HasValue) - query = query.Where(blueprint => blueprint.CollectionId == filter.CollectionId.Value); - - if (TryParse(filter.Mode, out PracticeMode mode)) query = query.Where(blueprint => blueprint.Mode == mode); - - if (!string.IsNullOrWhiteSpace(filter.Keyword)) - { - var keyword = filter.Keyword.Trim(); - query = query.Where(blueprint => blueprint.Name.Contains(keyword)); - } - - var items = await query - .OrderBy(blueprint => blueprint.SortOrder) - .ThenBy(blueprint => blueprint.CreatedAt) - .Take(ResolveLimit(filter.Limit)) - .Select(blueprint => ToBlueprintItem(blueprint)) - .ToArrayAsync(cancellationToken); - - return new CatalogList(items); - } - - public async Task> UpsertPracticeBlueprintAsync( - ContentManagementActor actor, - UpsertPracticeBlueprintCommand command, - CancellationToken cancellationToken = default) - { - var scope = await RequireDataScopeAsync(actor, cancellationToken); - ArgumentException.ThrowIfNullOrWhiteSpace(command.Name); - await AssertRegionAsync(actor.TenantId, command.RegionId, cancellationToken); - await AssertEntryAsync(actor, scope, command.EntryId, cancellationToken); - await AssertNodeAsync(actor, scope, command.NodeId, cancellationToken); - await AssertReferenceAsync(actor.TenantId, command.CollectionId, "collection_not_found", - cancellationToken); - - var blueprint = await ResolveEntityByIdOrLegacyAsync( - questionBankPersistence.PracticeBlueprints, - actor.TenantId, - command.Id, - command.LegacyId, - cancellationToken); - - var isNew = blueprint is null; - if (command.Id.HasValue && (blueprint is null || blueprint.Id != command.Id.Value)) - throw new ContentManagementException("Practice blueprint was not found.", "practice_blueprint_not_found"); - - if (blueprint is not null && !scope.AllowsResource(actor.UserId, blueprint.CreatedBy, blueprint.RegionId)) - throw new ContentManagementException("Practice blueprint was not found.", "practice_blueprint_not_found"); - - if (blueprint is null && !scope.AllowsResource(actor.UserId, actor.UserId, command.RegionId)) - throw new ContentManagementException("Practice blueprint was not found.", "practice_blueprint_not_found"); - - blueprint ??= new PracticeBlueprint - { - Id = command.Id ?? Guid.NewGuid(), - TenantId = actor.TenantId, - CreatedBy = actor.UserId - }; - - blueprint.RegionId = command.RegionId; - blueprint.EntryId = command.EntryId; - blueprint.NodeId = command.NodeId; - blueprint.CollectionId = command.CollectionId; - blueprint.LegacyId = Normalize(command.LegacyId); - blueprint.Name = command.Name.Trim(); - blueprint.Mode = Parse(command.Mode, PracticeMode.Sequential, "practice_mode_invalid"); - blueprint.AssemblyType = Parse(command.AssemblyType, PracticeAssemblyType.Collection, - "practice_assembly_type_invalid"); - blueprint.QuestionLimit = command.QuestionLimit; - blueprint.DurationMinutes = command.DurationMinutes; - blueprint.TotalScore = command.TotalScore; - blueprint.PassScore = command.PassScore; - blueprint.Sections = JsonArrayOrDefault(command.Sections); - blueprint.Rules = JsonObjectOrDefault(command.Rules); - blueprint.AccessRules = JsonObjectOrDefault(command.AccessRules); - blueprint.Status = Parse(command.Status, ContentStatus.Active, "content_status_invalid"); - blueprint.SortOrder = command.Order ?? 0; - blueprint.UpdatedBy = actor.UserId; - - if (isNew) questionBankPersistence.PracticeBlueprints.Add(blueprint); - - await questionBankPersistence.SaveChangesAsync(cancellationToken); - return new ContentManagementResult(ToBlueprintItem(blueprint)); - } - - public ImportFieldMappingItem GetImportFieldMapping(string importType) - { - var spec = ResolveImportSpec(importType); - return new ImportFieldMappingItem( - spec.ImportType, - spec.Title, - spec.Description, - spec.Fields, - spec.Fields.Where(field => field.Required).Select(field => field.Field).ToArray()); - } - - public ImportTemplateItem GetImportTemplate(string importType, string? format) - { - var spec = ResolveImportSpec(importType); - var normalizedFormat = string.IsNullOrWhiteSpace(format) ? "json" : format.Trim().ToLowerInvariant(); - var content = normalizedFormat switch - { - "json" => JsonSerializer.Serialize(spec.JsonExample, new JsonSerializerOptions { WriteIndented = true }), - "csv" => string.Join( - "\n", - spec.CsvRows.Select(row => string.Join(",", row.Select(EscapeCsv)))), - _ => throw new ContentManagementException("Import template format is not supported.", - "import_template_format_invalid") - }; - - return new ImportTemplateItem( - spec.ImportType, - normalizedFormat, - $"{spec.ImportType}-import-template.{normalizedFormat}", - normalizedFormat == "csv" ? "text/csv" : "application/json", - Convert.ToBase64String(Encoding.UTF8.GetBytes(content)), - content, - spec.Fields); - } -} diff --git a/Tiku.Infrastructure/Content/Questions/QuestionManagementService.cs b/Tiku.Infrastructure/Content/Questions/QuestionManagementService.cs deleted file mode 100644 index 6e0ce9b..0000000 --- a/Tiku.Infrastructure/Content/Questions/QuestionManagementService.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Tiku.Application.Content; -using Tiku.Application.Security; -using Tiku.Domain.QuestionBanks; - -namespace Tiku.Infrastructure.Content; - -internal sealed class QuestionManagementService(DirectContentServiceDependencies dependencies) - : DirectContentServiceBase(dependencies), IQuestionManagementService -{ - public async Task> CreateQuestionAsync( - DirectContentActor actor, - QuestionWriteCommand command, - CancellationToken cancellationToken = default) - { - ValidateQuestionForPublication(command); - await AssertQuestionReferencesAsync(actor.TenantId, command, cancellationToken); - await using var transaction = unitOfWork.Database.CurrentTransaction is null - ? await unitOfWork.Database.BeginTransactionAsync(cancellationToken) - : null; - - var question = new Question - { - Id = command.QuestionId ?? Guid.NewGuid(), - TenantId = actor.TenantId - }; - ApplyQuestion(question, command); - if (question.Status != QuestionStatus.Archived) - await featureAccessService.ConsumeQuotaIfConfiguredAsync( - actor.TenantId, - SaasQuotaMetricCatalog.PrivateQuestionCount, - cancellationToken: cancellationToken); - questionBankPersistence.Questions.Add(question); - await unitOfWork.SaveChangesAsync(cancellationToken); - - var version = BuildQuestionVersion(actor, question.Id, 1, command); - questionBankPersistence.QuestionVersions.Add(version); - question.CurrentVersionId = version.Id; - await SyncPrimaryCollectionItemAsync(actor, question, cancellationToken); - - await unitOfWork.SaveChangesAsync(cancellationToken); - if (transaction is not null) await transaction.CommitAsync(cancellationToken); - return new ContentManagementResult(ToQuestionItem(question, version)); - } - - public async Task> UpdateQuestionAsync( - DirectContentActor actor, - QuestionWriteCommand command, - CancellationToken cancellationToken = default) - { - if (!command.QuestionId.HasValue) - throw new ContentManagementException("questionId is required.", "question_id_required"); - - ValidateQuestionForPublication(command); - - var question = await questionBankPersistence.Questions.SingleOrDefaultAsync( - item => item.TenantId == actor.TenantId && item.Id == command.QuestionId.Value, - cancellationToken); - if (question is null) throw new ContentManagementException("Question was not found.", "question_not_found"); - - await AssertQuestionReferencesAsync(actor.TenantId, command, cancellationToken); - await using var transaction = unitOfWork.Database.CurrentTransaction is null - ? await unitOfWork.Database.BeginTransactionAsync(cancellationToken) - : null; - var wasCounted = question.Status != QuestionStatus.Archived; - ApplyQuestion(question, command); - var isCounted = question.Status != QuestionStatus.Archived; - if (!wasCounted && isCounted) - await featureAccessService.ConsumeQuotaIfConfiguredAsync( - actor.TenantId, - SaasQuotaMetricCatalog.PrivateQuestionCount, - cancellationToken: cancellationToken); - var nextVersionNo = await questionBankPersistence.QuestionVersions - .Where(item => item.TenantId == actor.TenantId && item.QuestionId == question.Id) - .Select(item => (int?)item.VersionNo) - .MaxAsync(cancellationToken) ?? 0; - var version = BuildQuestionVersion(actor, question.Id, nextVersionNo + 1, command); - questionBankPersistence.QuestionVersions.Add(version); - question.CurrentVersionId = version.Id; - - await SyncPrimaryCollectionItemAsync(actor, question, cancellationToken); - - await unitOfWork.SaveChangesAsync(cancellationToken); - if (wasCounted && !isCounted) - await featureAccessService.ReleaseQuotaAsync( - actor.TenantId, - SaasQuotaMetricCatalog.PrivateQuestionCount, - 1, - cancellationToken); - if (transaction is not null) await transaction.CommitAsync(cancellationToken); - return new ContentManagementResult(ToQuestionItem(question, version)); - } -} diff --git a/Tiku.Infrastructure/ContentV2/ContentCandidateReader.cs b/Tiku.Infrastructure/ContentV2/ContentCandidateReader.cs new file mode 100644 index 0000000..a3a70b2 --- /dev/null +++ b/Tiku.Infrastructure/ContentV2/ContentCandidateReader.cs @@ -0,0 +1,90 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tiku.Application.Content; +using Tiku.Application.Security; +using Tiku.Domain.Content; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure.ContentV2; + +internal sealed class ContentCandidateReader(ITenantExecutionScope tenantExecutionScope) : IContentCandidateReader +{ + public async Task> GetCandidatesAsync( + ContentCandidateQuery query, + CancellationToken cancellationToken = default) + { + if (query.ContentReleaseIds.Count == 0 || query.AudienceSegmentIds.Count == 0) return []; + var limit = Math.Clamp(query.Limit, 1, 500); + return await tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + query.ContentOwnerTenantId, + SystemScopeCallerType.PublicQuestionBank, + nameof(ContentCandidateReader), + "Read compiled content candidates with an explicit content owner", + Guid.NewGuid().ToString("N")), + async (provider, token) => + { + var persistence = provider.GetRequiredService(); + var releaseIds = query.ContentReleaseIds.ToArray(); + var segmentIds = query.AudienceSegmentIds.ToArray(); + var candidates = persistence.ContentReleaseQuestions.AsNoTracking().Where(item => + item.TenantId == query.ContentOwnerTenantId && + releaseIds.Contains(item.ContentReleaseId) && + segmentIds.Contains(item.AudienceSegmentId) && + item.Status == ContentReleaseQuestionStatus.Active); + if (query.CurriculumNodeId.HasValue) + candidates = candidates.Where(item => item.CurriculumNodeId == query.CurriculumNodeId.Value); + if (!string.IsNullOrWhiteSpace(query.QuestionType)) + { + var type = query.QuestionType.Trim(); + candidates = candidates.Where(item => item.QuestionType == type); + } + if (query.Difficulty.HasValue) + candidates = candidates.Where(item => item.Difficulty == query.Difficulty.Value); + if (query.CollectionReleaseIds?.Count > 0) + { + var collectionIds = query.CollectionReleaseIds.ToArray(); + candidates = candidates.Where(candidate => persistence.CollectionReleaseQuestions.Any(item => + item.TenantId == query.ContentOwnerTenantId && + collectionIds.Contains(item.CollectionReleaseId) && + item.ContentReleaseQuestionId == candidate.Id)); + } + + var count = await candidates.CountAsync(token); + if (count == 0) return Array.Empty(); + var start = (int)((uint)query.Seed % (uint)count); + var ordered = candidates + .OrderBy(item => item.ContentReleaseId) + .ThenBy(item => item.AudienceSegmentId) + .ThenBy(item => item.CurriculumNodeId) + .ThenBy(item => item.Ordinal) + .ThenBy(item => item.Id); + var first = await Project(ordered.Skip(start).Take(limit)) + .ToArrayAsync(token); + if (first.Length == limit || start == 0) return first; + var wrapped = await Project(ordered.Take(limit - first.Length)) + .ToArrayAsync(token); + return first.Concat(wrapped).DistinctBy(item => item.ContentReleaseQuestionId).ToArray(); + }, + cancellationToken); + } + + private static IQueryable Project(IQueryable query) + { + return query.Select(item => new ContentCandidateItem( + item.Id, + item.TenantId, + item.ContentReleaseId, + item.AudienceSegmentId, + item.CurriculumNodeId, + item.QuestionPlacementId, + item.QuestionAssetOwnerTenantId, + item.QuestionAssetId, + item.QuestionRevisionId, + item.AssessmentPolicyVersionId, + item.Ordinal, + item.Difficulty, + item.QuestionType)); + } +} diff --git a/Tiku.Infrastructure/ContentV2/ContentReleaseCompiler.cs b/Tiku.Infrastructure/ContentV2/ContentReleaseCompiler.cs new file mode 100644 index 0000000..bb7b4ee --- /dev/null +++ b/Tiku.Infrastructure/ContentV2/ContentReleaseCompiler.cs @@ -0,0 +1,369 @@ +using System.Data; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tiku.Application.Content; +using Tiku.Application.Security; +using Tiku.Domain.Content; +using Tiku.Domain.Learning; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure.ContentV2; + +internal sealed class ContentReleaseCompiler( + IQuestionBankPersistence content, + ILearningPersistence learning, + ITenantExecutionScope tenantExecutionScope) : IContentReleaseCompiler +{ + internal const string ReleasePublishedEvent = "content_release_published"; + + public async Task PublishAsync( + PublishContentReleaseCommand command, + CancellationToken cancellationToken = default) + { + await using var transaction = await content.Database.BeginTransactionAsync( + IsolationLevel.RepeatableRead, + cancellationToken); + var publicationLockKey = $"content-release:{command.TenantId:N}:{command.CurriculumVersionId:N}"; + await content.Database.ExecuteSqlInterpolatedAsync( + $"SELECT pg_advisory_xact_lock(hashtextextended({publicationLockKey}, 0))", + cancellationToken); + + var curriculumVersion = await content.CurriculumVersions.AsNoTracking().SingleOrDefaultAsync( + item => item.TenantId == command.TenantId && + item.Id == command.CurriculumVersionId && + item.Status == ContentDefinitionStatus.Published, + cancellationToken) ?? throw Error("curriculum_version_not_publishable", + "A published curriculum version is required."); + var curriculum = await content.Curricula.AsNoTracking().SingleAsync( + item => item.TenantId == command.TenantId && item.Id == curriculumVersion.CurriculumId, + cancellationToken); + + var placements = await content.QuestionPlacements.AsNoTracking() + .Where(item => item.TenantId == command.TenantId && + item.CurriculumVersionId == curriculumVersion.Id && + item.Status == QuestionPlacementStatus.Active && + (item.ValidFrom == null || item.ValidFrom <= DateTimeOffset.UtcNow) && + (item.ValidTo == null || item.ValidTo > DateTimeOffset.UtcNow)) + .OrderBy(item => item.Id) + .ToArrayAsync(cancellationToken); + if (placements.Length == 0) + throw Error("content_release_empty", "The curriculum has no active question placements."); + + var platformOwnerIds = await tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + null, + SystemScopeCallerType.Platform, + nameof(ContentReleaseCompiler), + "Resolve the unique platform V2 content owner", + Guid.NewGuid().ToString("N"), + true), + (provider, token) => provider.GetRequiredService().Tenants.AsNoTracking() + .Where(item => item.Mode == TenantMode.PlatformOwned) + .Select(item => item.Id) + .ToArrayAsync(token), + cancellationToken); + if (platformOwnerIds.Length != 1) + throw Error("platform_content_owner_invalid", + "Exactly one platform-owned content tenant is required before publishing content."); + var permittedOwnerIds = platformOwnerIds.Append(command.TenantId).Distinct().ToArray(); + if (placements.Any(item => !permittedOwnerIds.Contains(item.QuestionAssetOwnerTenantId))) + throw Error("question_asset_owner_forbidden", + "A placement may reference only this tenant's private question or the platform public owner."); + + var assetIds = placements.Select(item => item.QuestionAssetId).Distinct().ToArray(); + var assets = new List(); + foreach (var ownerTenantId in permittedOwnerIds) + assets.AddRange(await ReadForOwnerAsync( + ownerTenantId, + "Load explicitly placed published V2 question assets", + (persistence, token) => persistence.QuestionAssets.AsNoTracking() + .Where(item => item.TenantId == ownerTenantId && + assetIds.Contains(item.Id) && + item.Status == QuestionAssetStatus.Published && + item.CurrentRevisionId != null) + .ToArrayAsync(token), + cancellationToken)); + var assetsByOwner = assets.ToDictionary(item => (item.TenantId, item.Id)); + var requestedAssets = placements + .Select(item => (item.QuestionAssetOwnerTenantId, item.QuestionAssetId)) + .Distinct() + .ToArray(); + if (requestedAssets.Any(key => !assetsByOwner.ContainsKey(key))) + throw Error("content_release_question_unpublished", + "Every placement must reference a published question asset with a current revision."); + var revisionIds = assets.Select(item => item.CurrentRevisionId!.Value).Distinct().ToArray(); + var revisions = new List(); + foreach (var ownerTenantId in permittedOwnerIds) + revisions.AddRange(await ReadForOwnerAsync( + ownerTenantId, + "Load immutable V2 question revisions for release compilation", + (persistence, token) => persistence.QuestionRevisions.AsNoTracking() + .Where(item => item.TenantId == ownerTenantId && revisionIds.Contains(item.Id)) + .ToArrayAsync(token), + cancellationToken)); + var revisionsByOwner = revisions.ToDictionary(item => (item.TenantId, item.Id)); + if (requestedAssets.Any(key => + !revisionsByOwner.ContainsKey((key.QuestionAssetOwnerTenantId, + assetsByOwner[key].CurrentRevisionId!.Value)))) + throw Error("content_release_revision_missing", "One or more immutable question revisions are missing."); + + var policyVersionIds = placements.Select(item => item.AssessmentPolicyVersionId).Distinct().ToArray(); + var publishedPolicyIds = await content.AssessmentPolicyVersions.AsNoTracking() + .Where(item => item.TenantId == command.TenantId && + policyVersionIds.Contains(item.Id) && + item.Status == ContentDefinitionStatus.Published) + .Select(item => item.Id) + .ToArrayAsync(cancellationToken); + if (publishedPolicyIds.Length != policyVersionIds.Length) + throw Error("content_release_assessment_unpublished", + "Every placement must reference a published assessment policy version."); + + var profileVersions = await ( + from profile in content.ExamTargetProfiles.AsNoTracking() + join version in content.ExamTargetProfileVersions.AsNoTracking() + on profile.Id equals version.ExamTargetProfileId + where profile.BusinessLineId == curriculum.BusinessLineId && + profile.IsActive && + version.Status == ContentDefinitionStatus.Published + select version) + .ToArrayAsync(cancellationToken); + if (profileVersions.Length == 0) + throw Error("content_release_target_profiles_missing", + "At least one published exam target profile is required."); + var profileFacts = await LoadProfileFactsAsync(profileVersions, cancellationToken); + var placementRules = await LoadPlacementRulesAsync(placements, cancellationToken); + + var placementMatches = placements.Select(placement => + { + var groups = placementRules.GetValueOrDefault(placement.Id) ?? []; + var matchingProfiles = profileFacts + .Where(pair => QuestionApplicabilityEvaluator.Matches(pair.Value, groups)) + .Select(pair => pair.Key) + .Order() + .ToArray(); + if (matchingProfiles.Length == 0) + throw Error("content_release_rule_matches_nothing", + $"Placement '{placement.Id}' does not match any published target profile."); + return new PlacementMatch(placement, matchingProfiles, HashIds(matchingProfiles)); + }).ToArray(); + + var now = DateTimeOffset.UtcNow; + var nextReleaseNo = await content.ContentReleases + .Where(item => item.TenantId == command.TenantId && + item.CurriculumVersionId == curriculumVersion.Id) + .Select(item => (int?)item.ReleaseNo) + .MaxAsync(cancellationToken) ?? 0; + var sourceFingerprint = ComputeSourceFingerprint(placementMatches, assetsByOwner, revisionsByOwner); + var release = new ContentRelease + { + TenantId = command.TenantId, + BusinessLineId = curriculum.BusinessLineId, + CurriculumVersionId = curriculumVersion.Id, + ReleaseNo = nextReleaseNo + 1, + Name = command.Name.Trim(), + Status = ContentReleaseStatus.Compiling, + SourceFingerprint = sourceFingerprint, + PublishedAt = now, + CreatedBy = command.ActorUserId + }; + content.ContentReleases.Add(release); + await content.SaveChangesAsync(cancellationToken); + + var segmentsByHash = new Dictionary(StringComparer.Ordinal); + foreach (var match in placementMatches) + { + if (segmentsByHash.ContainsKey(match.AudienceHash)) continue; + var segment = new AudienceSegment + { + TenantId = command.TenantId, + ContentReleaseId = release.Id, + RuleHash = match.AudienceHash, + SegmentOrdinal = segmentsByHash.Count + }; + segmentsByHash.Add(match.AudienceHash, segment); + content.AudienceSegments.Add(segment); + foreach (var profileVersionId in match.ProfileVersionIds) + content.AudienceSegmentMembers.Add(new AudienceSegmentMember + { + TenantId = command.TenantId, + ContentReleaseId = release.Id, + AudienceSegmentId = segment.Id, + ExamTargetProfileVersionId = profileVersionId + }); + } + + foreach (var group in placementMatches + .OrderBy(item => item.Placement.CurriculumNodeId) + .ThenBy(item => assetsByOwner[(item.Placement.QuestionAssetOwnerTenantId, + item.Placement.QuestionAssetId)].DeliveryFingerprint) + .GroupBy(item => new { item.AudienceHash, item.Placement.CurriculumNodeId })) + { + var ordinal = 0; + foreach (var match in group) + { + var asset = assetsByOwner[(match.Placement.QuestionAssetOwnerTenantId, + match.Placement.QuestionAssetId)]; + var revision = revisionsByOwner[(asset.TenantId, asset.CurrentRevisionId!.Value)]; + content.ContentReleaseQuestions.Add(new ContentReleaseQuestion + { + TenantId = command.TenantId, + ContentReleaseId = release.Id, + AudienceSegmentId = segmentsByHash[match.AudienceHash].Id, + CurriculumNodeId = match.Placement.CurriculumNodeId, + QuestionPlacementId = match.Placement.Id, + QuestionAssetOwnerTenantId = asset.TenantId, + QuestionAssetId = asset.Id, + QuestionRevisionId = revision.Id, + AssessmentPolicyVersionId = match.Placement.AssessmentPolicyVersionId, + Ordinal = ordinal++, + Difficulty = match.Placement.DifficultyOverride ?? revision.Difficulty, + QuestionType = revision.QuestionType, + Status = ContentReleaseQuestionStatus.Active + }); + } + } + + await content.SaveChangesAsync(cancellationToken); + release.Status = ContentReleaseStatus.Published; + learning.LearningOutboxMessages.Add(new LearningOutboxMessage + { + TenantId = command.TenantId, + EventType = ReleasePublishedEvent, + SchemaVersion = 1, + AggregateId = release.Id, + Payload = JsonSerializer.SerializeToElement(new + { + releaseId = release.Id, + curriculumVersionId = curriculumVersion.Id, + businessLineId = curriculum.BusinessLineId, + releaseNo = release.ReleaseNo, + sourceFingerprint + }), + OccurredAt = now, + AvailableAt = now + }); + await content.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + return new ContentReleaseCompilationResult( + release.Id, + release.ReleaseNo, + placements.Length, + placements.Length, + segmentsByHash.Count, + profileVersions.Length, + sourceFingerprint, + now); + } + + private async Task>> LoadProfileFactsAsync( + IReadOnlyCollection versions, + CancellationToken cancellationToken) + { + var versionIds = versions.Select(item => item.Id).ToArray(); + var values = await content.ExamTargetProfileValues.AsNoTracking() + .Where(item => versionIds.Contains(item.ExamTargetProfileVersionId)) + .ToArrayAsync(cancellationToken); + var nodeIds = values.Select(item => item.TargetNodeId).Distinct().ToArray(); + var allNodes = await content.TargetNodes.AsNoTracking() + .Select(item => new NodeParent(item.Id, item.ParentId)) + .ToDictionaryAsync(item => item.Id, cancellationToken); + var ancestors = nodeIds.ToDictionary(nodeId => nodeId, nodeId => ResolveAncestors(nodeId, allNodes)); + return versions.ToDictionary( + version => version.Id, + version => (IReadOnlyCollection)values + .Where(value => value.ExamTargetProfileVersionId == version.Id) + .Select(value => new TargetProfileFact( + value.TargetDimensionDefinitionId, + value.TargetNodeId, + ancestors[value.TargetNodeId])) + .ToArray()); + } + + private async Task>> LoadPlacementRulesAsync( + IReadOnlyCollection placements, + CancellationToken cancellationToken) + { + var placementIds = placements.Select(item => item.Id).ToArray(); + var groups = await content.QuestionPlacementRuleGroups.AsNoTracking() + .Where(item => item.TenantId == placements.First().TenantId && placementIds.Contains(item.PlacementId)) + .ToArrayAsync(cancellationToken); + var groupIds = groups.Select(item => item.Id).ToArray(); + var conditions = await content.QuestionPlacementRuleConditions.AsNoTracking() + .Where(item => item.TenantId == placements.First().TenantId && groupIds.Contains(item.RuleGroupId)) + .ToArrayAsync(cancellationToken); + return groups.GroupBy(item => item.PlacementId).ToDictionary( + group => group.Key, + group => (IReadOnlyCollection)group.OrderBy(item => item.GroupOrder) + .Select(item => new PlacementRuleGroupFact( + item.GroupOrder, + conditions.Where(condition => condition.RuleGroupId == item.Id) + .Select(condition => new PlacementRuleConditionFact( + condition.TargetDimensionDefinitionId, + condition.TargetNodeId, + condition.Operator)) + .ToArray())) + .ToArray()); + } + + private static IReadOnlySet ResolveAncestors( + Guid nodeId, + IReadOnlyDictionary nodes) + { + var result = new HashSet(); + var current = nodeId; + while (nodes.TryGetValue(current, out var node) && node.ParentId is Guid parentId) + { + if (!result.Add(parentId)) throw Error("target_node_cycle", "A target-node hierarchy contains a cycle."); + current = parentId; + } + + return result; + } + + private static string ComputeSourceFingerprint( + IEnumerable matches, + IReadOnlyDictionary<(Guid OwnerTenantId, Guid AssetId), QuestionAsset> assets, + IReadOnlyDictionary<(Guid OwnerTenantId, Guid RevisionId), QuestionRevision> revisions) + { + var values = matches.OrderBy(item => item.Placement.Id).Select(item => + { + var asset = assets[(item.Placement.QuestionAssetOwnerTenantId, item.Placement.QuestionAssetId)]; + var revision = revisions[(asset.TenantId, asset.CurrentRevisionId!.Value)]; + return $"{item.Placement.Id:N}:{asset.TenantId:N}:{revision.Id:N}:{item.Placement.AssessmentPolicyVersionId:N}:{item.AudienceHash}"; + }); + return HashText(string.Join('|', values)); + } + + private static string HashIds(IEnumerable ids) => HashText(string.Join(',', ids.Select(id => id.ToString("N")))); + private static string HashText(string value) => + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant(); + private static ContentV2Exception Error(string code, string message) => new(code, message); + + private Task ReadForOwnerAsync( + Guid ownerTenantId, + string reason, + Func> read, + CancellationToken cancellationToken) + { + return tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + ownerTenantId, + SystemScopeCallerType.PublicQuestionBank, + nameof(ContentReleaseCompiler), + reason, + Guid.NewGuid().ToString("N")), + (provider, token) => read(provider.GetRequiredService(), token), + cancellationToken); + } + + private sealed record PlacementMatch( + QuestionPlacement Placement, + Guid[] ProfileVersionIds, + string AudienceHash); + + private sealed record NodeParent(Guid Id, Guid? ParentId); +} diff --git a/Tiku.Infrastructure/ContentV2/ContentV2AuthoringService.cs b/Tiku.Infrastructure/ContentV2/ContentV2AuthoringService.cs new file mode 100644 index 0000000..9ff7164 --- /dev/null +++ b/Tiku.Infrastructure/ContentV2/ContentV2AuthoringService.cs @@ -0,0 +1,542 @@ +using System.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tiku.Application.Content; +using Tiku.Application.Security; +using Tiku.Domain.Content; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure.ContentV2; + +internal sealed class ContentV2AuthoringService( + IQuestionBankPersistence content, + ITenantExecutionScope tenantExecutionScope) : IContentV2AuthoringService +{ + private const int MaxImportRows = 2000; + + public async Task SearchDuplicatesAsync( + ContentAuthorActor actor, + QuestionRevisionDraft draft, + int limit = 20, + CancellationToken cancellationToken = default) + { + ValidateDraft(draft); + var fingerprint = Fingerprint(draft); + var platformOwnerId = await GetPlatformOwnerIdAsync(cancellationToken); + var ownerIds = new[] { actor.TenantId, platformOwnerId }.Distinct().ToArray(); + var normalized = QuestionDeliveryFingerprint.NormalizePrompt(draft.Content); + var take = Math.Clamp(limit, 1, 100); + + var exact = new List(); + foreach (var ownerTenantId in ownerIds) + exact.AddRange(await SearchOwnerAsync( + actor.TenantId, + ownerTenantId, + fingerprint, + normalized, + true, + take, + [], + cancellationToken)); + + if (normalized.Length < 3 || exact.Count >= take) + return new QuestionDuplicateSearchResult(fingerprint, exact.Take(take).ToArray()); + + var exactAssetIds = exact.Select(item => item.QuestionAssetId).ToArray(); + var similar = new List(); + foreach (var ownerTenantId in ownerIds) + { + var remaining = take - exact.Count - similar.Count; + if (remaining <= 0) break; + similar.AddRange(await SearchOwnerAsync( + actor.TenantId, + ownerTenantId, + fingerprint, + normalized, + false, + remaining, + exactAssetIds, + cancellationToken)); + } + return new QuestionDuplicateSearchResult( + fingerprint, + exact.Concat(similar.OrderByDescending(item => item.PromptSimilarity).ThenBy(item => item.QuestionAssetId)) + .Take(take).ToArray()); + } + + public async Task ExecuteAsync( + ContentAuthorActor actor, + ExecuteQuestionAuthoringCommand command, + CancellationToken cancellationToken = default) + { + ValidateDraft(command.Draft); + var fingerprint = Fingerprint(command.Draft); + if (command.Decision is QuestionImportDecision.Skip or QuestionImportDecision.ManualReview) + return new QuestionAuthoringResult( + command.Decision, null, null, null, fingerprint, + command.Decision == QuestionImportDecision.Skip ? "skipped" : "manual_review"); + + if (command.Decision is QuestionImportDecision.ReusePlatform or QuestionImportDecision.ReuseTenant) + return await ResolveReuseAsync(actor, command, fingerprint, cancellationToken); + + if (command.Decision is not (QuestionImportDecision.CreateNew or QuestionImportDecision.CreateVariant)) + throw Error("question_authoring_decision_invalid", "The question authoring decision is invalid."); + if (command.Decision == QuestionImportDecision.CreateVariant && !command.FamilyId.HasValue) + throw Error("question_family_required", "A variant must belong to a question family."); + + await using var transaction = await content.Database.BeginTransactionAsync( + IsolationLevel.ReadCommitted, + cancellationToken); + var lockKey = $"question-fingerprint:{actor.TenantId:N}:{fingerprint}"; + await content.Database.ExecuteSqlInterpolatedAsync( + $"SELECT pg_advisory_xact_lock(hashtextextended({lockKey}, 0))", + cancellationToken); + if (await content.QuestionAssets.AnyAsync(item => + item.TenantId == actor.TenantId && item.DeliveryFingerprint == fingerprint, + cancellationToken)) + throw Error("question_exact_duplicate_exists", + "An exact tenant question already exists; choose reuse_tenant instead."); + if (command.FamilyId.HasValue && !await content.QuestionFamilies.AnyAsync(item => + item.TenantId == actor.TenantId && item.Id == command.FamilyId.Value, + cancellationToken)) + throw Error("question_family_not_found", "The question family was not found in this tenant."); + + var asset = new QuestionAsset + { + TenantId = actor.TenantId, + FamilyId = command.FamilyId, + DeliveryFingerprint = fingerprint, + Status = command.PublishAsset ? QuestionAssetStatus.Published : QuestionAssetStatus.Draft, + CreatedBy = actor.UserId, + UpdatedBy = actor.UserId + }; + content.QuestionAssets.Add(asset); + await content.SaveChangesAsync(cancellationToken); + var revision = BuildRevision(actor, asset.Id, 1, command.Draft, fingerprint); + content.QuestionRevisions.Add(revision); + await content.SaveChangesAsync(cancellationToken); + asset.CurrentRevisionId = revision.Id; + await content.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + return Result(command.Decision, asset, revision); + } + + public async Task CreateRevisionAsync( + ContentAuthorActor actor, + Guid questionAssetId, + QuestionRevisionDraft draft, + bool publishAsset, + CancellationToken cancellationToken = default) + { + ValidateDraft(draft); + var fingerprint = Fingerprint(draft); + await using var transaction = await content.Database.BeginTransactionAsync(cancellationToken); + var lockKey = $"question-asset:{actor.TenantId:N}:{questionAssetId:N}"; + await content.Database.ExecuteSqlInterpolatedAsync( + $"SELECT pg_advisory_xact_lock(hashtextextended({lockKey}, 0))", + cancellationToken); + var asset = await content.QuestionAssets.SingleOrDefaultAsync(item => + item.TenantId == actor.TenantId && item.Id == questionAssetId, + cancellationToken) ?? throw Error("question_asset_not_found", "The tenant question asset was not found."); + if (asset.Status == QuestionAssetStatus.Archived) + throw Error("question_asset_archived", "An archived question asset cannot receive a new revision."); + if (await content.QuestionAssets.AnyAsync(item => + item.TenantId == actor.TenantId && + item.Id != asset.Id && + item.DeliveryFingerprint == fingerprint, + cancellationToken)) + throw Error("question_exact_duplicate_exists", "Another exact tenant question already exists."); + var nextRevision = (await content.QuestionRevisions + .Where(item => item.TenantId == actor.TenantId && item.QuestionAssetId == asset.Id) + .Select(item => (int?)item.RevisionNo) + .MaxAsync(cancellationToken) ?? 0) + 1; + var revision = BuildRevision(actor, asset.Id, nextRevision, draft, fingerprint); + asset.CurrentRevisionId = revision.Id; + asset.DeliveryFingerprint = fingerprint; + asset.Status = publishAsset ? QuestionAssetStatus.Published : QuestionAssetStatus.Draft; + asset.UpdatedBy = actor.UserId; + content.QuestionRevisions.Add(revision); + await content.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + return Result(QuestionImportDecision.CreateNew, asset, revision); + } + + public async Task CreatePlacementAsync( + ContentAuthorActor actor, + CreateQuestionPlacementCommand command, + CancellationToken cancellationToken = default) + { + if (command.RuleGroups.GroupBy(item => item.GroupOrder).Any(group => group.Count() > 1) || + command.RuleGroups.Any(group => group.Conditions.Count == 0)) + throw Error("placement_rule_invalid", "Rule group order must be unique and every group must have conditions."); + var platformOwnerId = await GetPlatformOwnerIdAsync(cancellationToken); + if (command.QuestionAssetOwnerTenantId != actor.TenantId && + command.QuestionAssetOwnerTenantId != platformOwnerId) + throw Error("question_asset_owner_forbidden", + "A placement may reference only this tenant's private question or the platform public owner."); + var asset = await ReadForOwnerAsync( + command.QuestionAssetOwnerTenantId, + "Validate an explicitly selected reusable V2 question asset", + (persistence, token) => persistence.QuestionAssets.AsNoTracking().SingleOrDefaultAsync(item => + item.TenantId == command.QuestionAssetOwnerTenantId && + item.Id == command.QuestionAssetId && + item.Status == QuestionAssetStatus.Published, + token), + cancellationToken) ?? throw Error("question_asset_not_found", "A published reusable question was not found."); + + var curriculumNodeValid = await ( + from version in content.CurriculumVersions.AsNoTracking() + join node in content.CurriculumNodes.AsNoTracking() + on new { version.TenantId, VersionId = version.Id } + equals new { node.TenantId, VersionId = node.CurriculumVersionId } + where version.TenantId == actor.TenantId && + version.Id == command.CurriculumVersionId && + node.Id == command.CurriculumNodeId && + version.Status != ContentDefinitionStatus.Retired + select node.Id) + .AnyAsync(cancellationToken); + if (!curriculumNodeValid) + throw Error("curriculum_node_not_found", "The curriculum node does not belong to the requested version."); + if (!await content.AssessmentPolicyVersions.AsNoTracking().AnyAsync(item => + item.TenantId == actor.TenantId && + item.Id == command.AssessmentPolicyVersionId && + item.Status != ContentDefinitionStatus.Retired, + cancellationToken)) + throw Error("assessment_policy_not_found", "The assessment policy version was not found."); + + var conceptIds = command.KnowledgeConceptIds.Distinct().ToArray(); + if (await content.KnowledgeConcepts.CountAsync(item => + item.TenantId == actor.TenantId && conceptIds.Contains(item.Id) && item.IsActive, + cancellationToken) != conceptIds.Length) + throw Error("knowledge_concept_not_found", "One or more knowledge concepts were not found."); + var conditions = command.RuleGroups.SelectMany(group => group.Conditions).ToArray(); + await ValidateConditionsAsync(conditions, cancellationToken); + + var placement = new QuestionPlacement + { + TenantId = actor.TenantId, + QuestionAssetOwnerTenantId = asset.TenantId, + QuestionAssetId = asset.Id, + CurriculumVersionId = command.CurriculumVersionId, + CurriculumNodeId = command.CurriculumNodeId, + AssessmentPolicyVersionId = command.AssessmentPolicyVersionId, + DifficultyOverride = command.DifficultyOverride, + Status = command.Activate ? QuestionPlacementStatus.Active : QuestionPlacementStatus.Draft, + CreatedBy = actor.UserId, + UpdatedBy = actor.UserId + }; + content.QuestionPlacements.Add(placement); + foreach (var conceptId in conceptIds) + content.QuestionPlacementConcepts.Add(new QuestionPlacementConcept + { + TenantId = actor.TenantId, + PlacementId = placement.Id, + KnowledgeConceptId = conceptId + }); + foreach (var groupCommand in command.RuleGroups.OrderBy(item => item.GroupOrder)) + { + var group = new QuestionPlacementRuleGroup + { + TenantId = actor.TenantId, + PlacementId = placement.Id, + GroupOrder = groupCommand.GroupOrder + }; + content.QuestionPlacementRuleGroups.Add(group); + foreach (var condition in groupCommand.Conditions) + content.QuestionPlacementRuleConditions.Add(new QuestionPlacementRuleCondition + { + TenantId = actor.TenantId, + RuleGroupId = group.Id, + TargetDimensionDefinitionId = condition.TargetDimensionDefinitionId, + TargetNodeId = condition.TargetNodeId, + Operator = condition.Operator + }); + } + + await content.SaveChangesAsync(cancellationToken); + return new QuestionPlacementItem( + placement.Id, + placement.QuestionAssetOwnerTenantId, + placement.QuestionAssetId, + placement.CurriculumVersionId, + placement.CurriculumNodeId, + placement.AssessmentPolicyVersionId, + placement.Status.ToString()); + } + + public async Task PreviewImportAsync( + ContentAuthorActor actor, + IReadOnlyCollection<(string RowKey, QuestionRevisionDraft Draft)> rows, + CancellationToken cancellationToken = default) + { + ValidateImportRows(rows.Select(item => item.RowKey).ToArray()); + var result = new List(rows.Count); + foreach (var row in rows) + { + var duplicates = await SearchDuplicatesAsync(actor, row.Draft, 20, cancellationToken); + var exact = duplicates.Candidates.Where(item => item.ExactDeliveryMatch).ToArray(); + var suggested = exact.Any(item => item.Source == "tenant") + ? QuestionImportDecision.ReuseTenant + : exact.Any(item => item.Source == "platform") + ? QuestionImportDecision.ReusePlatform + : duplicates.Candidates.Count > 0 + ? QuestionImportDecision.ManualReview + : QuestionImportDecision.CreateNew; + result.Add(new QuestionImportPreviewRow( + row.RowKey.Trim(), + duplicates.DeliveryFingerprint, + suggested, + duplicates.Candidates)); + } + + return new QuestionImportPreviewResult(result.Count, result); + } + + public async Task ExecuteImportAsync( + ContentAuthorActor actor, + IReadOnlyCollection rows, + CancellationToken cancellationToken = default) + { + ValidateImportRows(rows.Select(item => item.RowKey).ToArray()); + var result = new List(rows.Count); + foreach (var row in rows) + result.Add(new QuestionImportExecutionRow( + row.RowKey.Trim(), + await ExecuteAsync(actor, row.Command, cancellationToken))); + return new QuestionImportExecutionResult(result.Count, result); + } + + private async Task ResolveReuseAsync( + ContentAuthorActor actor, + ExecuteQuestionAuthoringCommand command, + string fingerprint, + CancellationToken cancellationToken) + { + var ownerId = command.Decision == QuestionImportDecision.ReuseTenant + ? actor.TenantId + : await GetPlatformOwnerIdAsync(cancellationToken); + var asset = await ReadForOwnerAsync( + ownerId, + "Resolve an explicitly selected exact V2 question reuse", + async (persistence, token) => + { + var query = persistence.QuestionAssets.AsNoTracking().Where(item => + item.TenantId == ownerId && + item.DeliveryFingerprint == fingerprint && + item.Status == QuestionAssetStatus.Published && + item.CurrentRevisionId != null); + if (command.ReuseQuestionAssetId.HasValue) + query = query.Where(item => item.Id == command.ReuseQuestionAssetId.Value); + return await query.SingleOrDefaultAsync(token); + }, + cancellationToken) ?? throw Error( + "question_exact_reuse_not_found", + "The selected exact reusable question was not found in the requested owner scope."); + return new QuestionAuthoringResult( + command.Decision, + asset.TenantId, + asset.Id, + asset.CurrentRevisionId, + asset.DeliveryFingerprint, + "reused"); + } + + private async Task ValidateConditionsAsync( + IReadOnlyCollection conditions, + CancellationToken cancellationToken) + { + if (conditions.Count == 0) return; + if (conditions.GroupBy(item => new + { + item.TargetDimensionDefinitionId, + item.TargetNodeId, + item.Operator + }).Any(group => group.Count() > 1)) + throw Error("placement_rule_duplicate", "A placement rule contains duplicate conditions."); + var nodeIds = conditions.Select(item => item.TargetNodeId).Distinct().ToArray(); + var nodes = await content.TargetNodes.AsNoTracking() + .Where(item => nodeIds.Contains(item.Id) && item.IsActive) + .Select(item => new { item.Id, item.TargetDimensionDefinitionId }) + .ToArrayAsync(cancellationToken); + if (conditions.Any(condition => !nodes.Any(node => + node.Id == condition.TargetNodeId && + node.TargetDimensionDefinitionId == condition.TargetDimensionDefinitionId))) + throw Error("placement_rule_target_invalid", "A target node does not belong to its declared dimension."); + } + + private async Task GetPlatformOwnerIdAsync(CancellationToken cancellationToken) + { + var ids = await tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + null, + SystemScopeCallerType.Platform, + nameof(ContentV2AuthoringService), + "Resolve the unique platform V2 content owner", + Guid.NewGuid().ToString("N"), + true), + (provider, token) => provider.GetRequiredService().Tenants.AsNoTracking() + .Where(item => item.Mode == TenantMode.PlatformOwned) + .Select(item => item.Id) + .Take(2) + .ToArrayAsync(token), + cancellationToken); + return ids.Length == 1 + ? ids[0] + : throw Error("platform_content_owner_invalid", + "Exactly one platform-owned content tenant is required."); + } + + private static QuestionRevision BuildRevision( + ContentAuthorActor actor, + Guid assetId, + int revisionNo, + QuestionRevisionDraft draft, + string fingerprint) + { + return new QuestionRevision + { + TenantId = actor.TenantId, + QuestionAssetId = assetId, + RevisionNo = revisionNo, + QuestionType = draft.QuestionType.Trim(), + TypeLabel = draft.TypeLabel?.Trim(), + Difficulty = draft.Difficulty, + NormalizedContent = QuestionDeliveryFingerprint.NormalizePrompt(draft.Content), + Content = draft.Content, + Options = draft.Options.Clone(), + CorrectOptionIndex = draft.CorrectOptionIndex, + CorrectOptionIndices = draft.CorrectOptionIndices.Clone(), + AnswerText = draft.AnswerText, + Explanation = draft.Explanation, + SubQuestions = draft.SubQuestions.Clone(), + CodeLang = draft.CodeLang?.Trim(), + CodeTemplate = draft.CodeTemplate, + DeliveryFingerprint = fingerprint, + CreatedBy = actor.UserId + }; + } + + private static QuestionAuthoringResult Result( + QuestionImportDecision decision, + QuestionAsset asset, + QuestionRevision revision) + { + return new QuestionAuthoringResult( + decision, + asset.TenantId, + asset.Id, + revision.Id, + asset.DeliveryFingerprint, + asset.Status.ToString()); + } + + private static string Fingerprint(QuestionRevisionDraft draft) + { + return QuestionDeliveryFingerprint.Compute(new QuestionDeliveryFingerprintInput( + draft.QuestionType, + draft.Content, + draft.Options, + draft.CorrectOptionIndex, + draft.CorrectOptionIndices, + draft.AnswerText, + draft.SubQuestions, + draft.CodeLang, + draft.CodeTemplate)); + } + + private static void ValidateDraft(QuestionRevisionDraft draft) + { + if (string.IsNullOrWhiteSpace(draft.QuestionType)) + throw Error("question_type_required", "Question type is required."); + if (string.IsNullOrWhiteSpace(draft.Content) && string.IsNullOrWhiteSpace(draft.CodeTemplate)) + throw Error("question_content_required", "Question content or a code template is required."); + if (draft.Difficulty is < 1 or > 5) + throw Error("question_difficulty_invalid", "Question difficulty must be between 1 and 5."); + } + + private static void ValidateImportRows(IReadOnlyCollection rowKeys) + { + if (rowKeys.Count == 0 || rowKeys.Count > MaxImportRows) + throw Error("question_import_size_invalid", + $"An import must contain between 1 and {MaxImportRows} rows."); + if (rowKeys.Any(string.IsNullOrWhiteSpace) || + rowKeys.Select(item => item.Trim()).Distinct(StringComparer.Ordinal).Count() != rowKeys.Count) + throw Error("question_import_row_key_invalid", "Import row keys must be non-empty and unique."); + } + + private static ContentV2Exception Error(string code, string message) => new(code, message); + + private Task ReadForOwnerAsync( + Guid ownerTenantId, + string reason, + Func> read, + CancellationToken cancellationToken) + { + return tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + ownerTenantId, + SystemScopeCallerType.PublicQuestionBank, + nameof(ContentV2AuthoringService), + reason, + Guid.NewGuid().ToString("N")), + (provider, token) => read(provider.GetRequiredService(), token), + cancellationToken); + } + + private Task SearchOwnerAsync( + Guid actorTenantId, + Guid ownerTenantId, + string fingerprint, + string normalized, + bool exact, + int limit, + IReadOnlyCollection excludedAssetIds, + CancellationToken cancellationToken) + { + var excluded = excludedAssetIds.ToArray(); + return ReadForOwnerAsync( + ownerTenantId, + exact ? "Search exact V2 question duplicates" : "Search similar V2 question candidates", + async (persistence, token) => + { + var query = + from asset in persistence.QuestionAssets.AsNoTracking() + join revision in persistence.QuestionRevisions.AsNoTracking() + on new { asset.TenantId, QuestionAssetId = asset.Id, Id = asset.CurrentRevisionId } + equals new + { + revision.TenantId, + revision.QuestionAssetId, + Id = (Guid?)revision.Id + } + where asset.TenantId == ownerTenantId && + !excluded.Contains(asset.Id) && + asset.Status != QuestionAssetStatus.Archived + select new { Asset = asset, Revision = revision }; + if (exact) + query = query.Where(item => item.Asset.DeliveryFingerprint == fingerprint); + else + query = query.Where(item => + EF.Functions.TrigramsAreSimilar(item.Revision.NormalizedContent, normalized)); + return await query + .OrderByDescending(item => exact + ? 1 + : EF.Functions.TrigramsSimilarity(item.Revision.NormalizedContent, normalized)) + .ThenBy(item => item.Asset.Id) + .Select(item => new QuestionDuplicateCandidate( + item.Asset.TenantId, + item.Asset.Id, + item.Revision.Id, + item.Asset.TenantId == actorTenantId ? "tenant" : "platform", + exact, + exact ? 1 : EF.Functions.TrigramsSimilarity(item.Revision.NormalizedContent, normalized), + item.Revision.QuestionType, + item.Revision.Content, + item.Revision.Difficulty)) + .Take(limit) + .ToArrayAsync(token); + }, + cancellationToken); + } +} diff --git a/Tiku.Infrastructure/ContentV2/PlatformContentActorResolver.cs b/Tiku.Infrastructure/ContentV2/PlatformContentActorResolver.cs new file mode 100644 index 0000000..8cadafa --- /dev/null +++ b/Tiku.Infrastructure/ContentV2/PlatformContentActorResolver.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tiku.Application.Content; +using Tiku.Application.Security; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure.ContentV2; + +internal sealed class PlatformContentActorResolver(ITenantExecutionScope tenantExecutionScope) + : IPlatformContentActorResolver +{ + public Task ResolveAsync( + Guid userId, + CancellationToken cancellationToken = default) + { + return tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + null, + SystemScopeCallerType.Platform, + nameof(PlatformContentActorResolver), + "Resolve the single platform-owned V2 content owner", + Guid.NewGuid().ToString("N"), + true), + async (provider, token) => + { + var tenancy = provider.GetRequiredService(); + var ownerIds = await tenancy.Tenants.AsNoTracking() + .Where(item => item.Mode == TenantMode.PlatformOwned && item.Status == TenantStatus.Active) + .Select(item => item.Id) + .Take(2) + .ToArrayAsync(token); + if (ownerIds.Length != 1) + throw new ContentV2Exception( + "platform_content_owner_invalid", + "Exactly one active platform content owner is required."); + return new ContentAuthorActor(ownerIds[0], userId); + }, + cancellationToken); + } +} diff --git a/Tiku.Infrastructure/ContentV2/PlatformContentPackageProjectionService.cs b/Tiku.Infrastructure/ContentV2/PlatformContentPackageProjectionService.cs new file mode 100644 index 0000000..db2fc32 --- /dev/null +++ b/Tiku.Infrastructure/ContentV2/PlatformContentPackageProjectionService.cs @@ -0,0 +1,113 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tiku.Application.Content; +using Tiku.Application.Security; +using Tiku.Domain.Content; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure.ContentV2; + +internal sealed class PlatformContentPackageProjectionService( + IQuestionBankPersistence content, + ITenantExecutionScope tenantExecutionScope) : + IPlatformContentPackageProjectionService +{ + public async Task ApplyAsync( + ApplyPlatformContentPackageCommand command, + CancellationToken cancellationToken = default) + { + var packageCode = Required(command.PackageCode, nameof(command.PackageCode), 100); + var cellId = Required(command.CellId, nameof(command.CellId), 100); + var contentHash = Required(command.ContentHash, nameof(command.ContentHash), 64); + if (command.EventSequence <= 0) + throw new ContentV2Exception("platform_package_sequence_invalid", + "The platform package event sequence must be positive."); + + await using var transaction = await content.Database.BeginTransactionAsync(cancellationToken); + var lockKey = $"platform-package-cell:{command.PackageOwnerTenantId:N}:{packageCode}:{cellId}"; + await content.Database.ExecuteSqlInterpolatedAsync( + $"SELECT pg_advisory_xact_lock(hashtextextended({lockKey}, 0))", + cancellationToken); + + var package = await tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + command.PackageOwnerTenantId, + SystemScopeCallerType.PublicQuestionBank, + nameof(PlatformContentPackageProjectionService), + "Validate an immutable platform package before applying it to a Cell", + Guid.NewGuid().ToString("N")), + (provider, token) => provider.GetRequiredService() + .PlatformContentPackageVersions.AsNoTracking() + .SingleOrDefaultAsync(item => + item.TenantId == command.PackageOwnerTenantId && + item.Id == command.PlatformContentPackageVersionId && + item.PackageCode == packageCode && + item.ContentHash == contentHash && + item.Status == PlatformContentPackageStatus.Published, + token), + cancellationToken) ?? throw new ContentV2Exception( + "platform_package_invalid", + "The package event does not match a published immutable platform package version."); + + var projection = await content.PlatformContentPackageCellProjections + .SingleOrDefaultAsync(item => + item.PackageOwnerTenantId == command.PackageOwnerTenantId && + item.PackageCode == packageCode && + item.CellId == cellId, + cancellationToken); + if (projection is not null && command.EventSequence < projection.EventSequence) + { + await transaction.CommitAsync(cancellationToken); + return Result(CellPackageApplyStatus.IgnoredOutOfOrder, projection); + } + + if (projection is not null && command.EventSequence == projection.EventSequence) + { + if (projection.PlatformContentPackageVersionId != package.Id || + !string.Equals(projection.ContentHash, contentHash, StringComparison.Ordinal)) + throw new ContentV2Exception("platform_package_sequence_conflict", + "The same package event sequence was received with different immutable content."); + await transaction.CommitAsync(cancellationToken); + return Result(CellPackageApplyStatus.Duplicate, projection); + } + + if (projection is null) + { + projection = new PlatformContentPackageCellProjection + { + PackageOwnerTenantId = command.PackageOwnerTenantId, + PackageCode = packageCode, + CellId = cellId + }; + content.PlatformContentPackageCellProjections.Add(projection); + } + + projection.PlatformContentPackageVersionId = package.Id; + projection.EventSequence = command.EventSequence; + projection.ContentHash = contentHash; + projection.Status = CellProjectionStatus.Ready; + projection.ProjectedAt = DateTimeOffset.UtcNow; + projection.LastError = null; + await content.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + return Result(CellPackageApplyStatus.Applied, projection); + } + + private static CellPackageApplyResult Result( + CellPackageApplyStatus status, + PlatformContentPackageCellProjection projection) => new( + status, + projection.PlatformContentPackageVersionId, + projection.CellId, + projection.EventSequence, + projection.ContentHash); + + private static string Required(string value, string parameterName, int maxLength) + { + var normalized = value.Trim(); + if (normalized.Length == 0 || normalized.Length > maxLength) + throw new ContentV2Exception("platform_package_event_invalid", + $"{parameterName} is required and must not exceed {maxLength} characters."); + return normalized; + } +} diff --git a/Tiku.Infrastructure/DependencyInjection.cs b/Tiku.Infrastructure/DependencyInjection.cs index c4499b7..08ff5c8 100644 --- a/Tiku.Infrastructure/DependencyInjection.cs +++ b/Tiku.Infrastructure/DependencyInjection.cs @@ -50,8 +50,6 @@ public static class DependencyInjection services.AddScoped(provider => provider.GetRequiredService()); services.AddScoped(provider => provider.GetRequiredService()); services.AddScoped(provider => provider.GetRequiredService()); - services.AddScoped(provider => - provider.GetRequiredService()); services.AddScoped(provider => provider.GetRequiredService()); services.AddScoped(provider => provider.GetRequiredService()); diff --git a/Tiku.Infrastructure/Learning/Access/EffectiveLearningAccessService.cs b/Tiku.Infrastructure/Learning/Access/EffectiveLearningAccessService.cs new file mode 100644 index 0000000..c8b9195 --- /dev/null +++ b/Tiku.Infrastructure/Learning/Access/EffectiveLearningAccessService.cs @@ -0,0 +1,311 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tiku.Application.Content; +using Tiku.Application.Learning; +using Tiku.Application.Security; +using Tiku.Domain.Content; +using Tiku.Domain.Learning; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Caching; +using Tiku.Infrastructure.Persistence; +using ZiggyCreatures.Caching.Fusion; + +namespace Tiku.Infrastructure.Learning; + +internal sealed class EffectiveLearningAccessService( + ILearningAccessPersistence access, + ITenantExecutionScope tenantExecutionScope, + [FromKeyedServices(BusinessCachingServiceCollectionExtensions.CacheName)] + IFusionCache cache) : IEffectiveLearningAccessService +{ + private static readonly TimeSpan SnapshotLifetime = TimeSpan.FromSeconds(30); + + public async Task GetSnapshotAsync( + LearningActor actor, + Guid businessLineId, + CancellationToken cancellationToken = default) + { + return await cache.GetOrSetAsync( + CacheKey(actor.TenantId, actor.UserId, businessLineId), + (_, token) => CompileAsync(actor, businessLineId, token), + options => options + .SetDuration(SnapshotLifetime) + .SetFailSafe(false), + token: cancellationToken); + } + + public Task InvalidateAsync( + Guid tenantId, + Guid userId, + Guid businessLineId, + CancellationToken cancellationToken = default) + { + return cache.RemoveAsync(CacheKey(tenantId, userId, businessLineId), token: cancellationToken).AsTask(); + } + + private async Task CompileAsync( + LearningActor actor, + Guid businessLineId, + CancellationToken cancellationToken) + { + var now = DateTimeOffset.UtcNow; + if (!await access.Tenants.AsNoTracking().AnyAsync( + item => item.Id == actor.TenantId && item.Status == TenantStatus.Active, + cancellationToken)) + throw Error("learning_tenant_inactive", "The tenant is not active."); + + var license = await access.TenantBusinessLicenses.AsNoTracking().SingleOrDefaultAsync( + item => item.TenantId == actor.TenantId && + item.BusinessLineId == businessLineId && + item.Status == LicenseLifecycleStatus.Active && + item.StartsAt <= now && + (item.EndsAt == null || item.EndsAt > now), + cancellationToken) ?? throw Error("learning_license_required", + "The tenant has no active license for the requested business."); + + var selections = await access.StudentTargetSelectionHistory.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.BusinessLineId == businessLineId && + item.IsCurrent && + item.EffectiveAt <= now && + (item.EndedAt == null || item.EndedAt > now)) + .ToArrayAsync(cancellationToken); + var primary = selections.SingleOrDefault(item => item.Role == TargetSelectionRole.Primary) + ?? throw Error("student_primary_target_required", + "A primary exam target must be selected before starting practice."); + if (selections.Length > 16) + throw Error("student_target_limit_exceeded", "No more than 16 active targets are allowed per business."); + + var selectedProfileIds = selections.Select(item => item.ExamTargetProfileVersionId).Distinct().ToArray(); + if (selectedProfileIds.Length != selections.Length) + throw Error("student_target_selection_duplicate", "The active target selection contains duplicates."); + + var licensedProfileIds = await access.TenantLicensedTargets.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && + item.TenantBusinessLicenseId == license.Id && + item.StartsAt <= now && + (item.EndsAt == null || item.EndsAt > now) && + selectedProfileIds.Contains(item.ExamTargetProfileVersionId)) + .Select(item => item.ExamTargetProfileVersionId) + .Distinct() + .ToArrayAsync(cancellationToken); + if (licensedProfileIds.Length != selectedProfileIds.Length) + throw Error("student_target_not_licensed", "One or more selected exam targets are not tenant licensed."); + + var entitlementRows = await ( + from entitlement in access.StudentEntitlementsV2.AsNoTracking() + join product in access.LearningProducts.AsNoTracking() + on new { entitlement.TenantId, Id = entitlement.LearningProductId } + equals new { product.TenantId, product.Id } + join manifest in access.ProductAccessManifestVersions.AsNoTracking() + on new { entitlement.TenantId, Id = entitlement.ProductAccessManifestVersionId } + equals new { manifest.TenantId, manifest.Id } + where entitlement.TenantId == actor.TenantId && + entitlement.UserId == actor.UserId && + entitlement.Status == StudentEntitlementStatus.Active && + entitlement.StartsAt <= now && + (entitlement.EndsAt == null || entitlement.EndsAt > now) && + product.BusinessLineId == businessLineId && + product.IsActive && + manifest.Status == AccessManifestStatus.Published + select new ManifestGrant(manifest.Id, manifest.MaxActiveTargets, manifest.MaxAlternateTargets, + entitlement.EndsAt, null)) + .ToArrayAsync(cancellationToken); + + var classGrantRows = await ( + from member in access.TenantClassMembers.AsNoTracking() + join grant in access.ClassAssignmentGrants.AsNoTracking() + on new { member.TenantId, member.ClassId } equals new { grant.TenantId, grant.ClassId } + join manifest in access.ProductAccessManifestVersions.AsNoTracking() + on new { grant.TenantId, Id = grant.ProductAccessManifestVersionId } + equals new { manifest.TenantId, manifest.Id } + join definition in access.ProductAccessManifests.AsNoTracking() + on new { manifest.TenantId, Id = manifest.ProductAccessManifestId } + equals new { definition.TenantId, definition.Id } + join product in access.LearningProducts.AsNoTracking() + on new { definition.TenantId, Id = definition.LearningProductId } + equals new { product.TenantId, product.Id } + where member.TenantId == actor.TenantId && + member.UserId == actor.UserId && + member.MemberType == TenantClassMemberType.Student && + member.Status == TenantClassMemberStatus.Active && + grant.Status == ClassAssignmentGrantStatus.Active && + grant.StartsAt <= now && + (grant.EndsAt == null || grant.EndsAt > now) && + manifest.Status == AccessManifestStatus.Published && + product.BusinessLineId == businessLineId && + product.IsActive + select new ManifestGrant(manifest.Id, manifest.MaxActiveTargets, manifest.MaxAlternateTargets, + grant.EndsAt, grant.Id)) + .ToArrayAsync(cancellationToken); + + var grants = entitlementRows.Concat(classGrantRows).DistinctBy(item => item.ManifestVersionId).ToArray(); + if (grants.Length == 0) + throw Error("student_entitlement_required", "No active product or class grant covers this business."); + var manifestIds = grants.Select(item => item.ManifestVersionId).ToArray(); + + await ValidateManifestTargetsAsync(actor.TenantId, selections, grants, cancellationToken); + + var manifestReleases = await access.ProductManifestReleases.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && manifestIds.Contains(item.ProductAccessManifestVersionId)) + .ToArrayAsync(cancellationToken); + if (manifestReleases.Length == 0) + throw Error("learning_manifest_empty", "The active access manifests contain no content release."); + + var releaseIds = manifestReleases.Select(item => item.ContentReleaseId).Distinct().ToArray(); + var releases = new List(); + foreach (var ownerTenantId in manifestReleases.Select(item => item.ContentOwnerTenantId).Distinct()) + releases.AddRange(await ReadForOwnerAsync( + ownerTenantId, + "Validate explicitly licensed V2 content releases", + async (persistence, token) => (await persistence.ContentReleases.AsNoTracking() + .Where(item => item.TenantId == ownerTenantId && + releaseIds.Contains(item.Id) && + item.BusinessLineId == businessLineId && + item.Status == ContentReleaseStatus.Published) + .Select(item => new { item.TenantId, item.Id, item.ReleaseNo }) + .ToArrayAsync(token)) + .Select(item => new ReleaseFact(item.TenantId, item.Id, item.ReleaseNo)).ToArray(), + cancellationToken)); + var validReleaseIds = manifestReleases + .Where(link => releases.Any(release => + release.TenantId == link.ContentOwnerTenantId && release.Id == link.ContentReleaseId)) + .Select(link => link.ContentReleaseId) + .Distinct() + .ToArray(); + if (validReleaseIds.Length != releaseIds.Length) + throw Error("learning_manifest_release_invalid", + "An access manifest references a missing, unpublished, or cross-business release."); + + var segmentIds = new List(); + foreach (var ownerTenantId in manifestReleases.Select(item => item.ContentOwnerTenantId).Distinct()) + segmentIds.AddRange(await ReadForOwnerAsync( + ownerTenantId, + "Compile V2 audience segments for explicitly licensed releases", + (persistence, token) => persistence.AudienceSegmentMembers.AsNoTracking() + .Where(item => item.TenantId == ownerTenantId && + validReleaseIds.Contains(item.ContentReleaseId) && + selectedProfileIds.Contains(item.ExamTargetProfileVersionId)) + .Select(item => item.AudienceSegmentId) + .Distinct() + .ToArrayAsync(token), + cancellationToken)); + if (segmentIds.Count == 0) + throw Error("learning_audience_empty", "No published content matches the selected exam targets."); + + var version = await access.LearningAccessVersions.AsNoTracking().SingleOrDefaultAsync( + item => item.TenantId == actor.TenantId && item.UserId == actor.UserId, + cancellationToken); + var validUntil = grants.Where(item => item.EndsAt.HasValue).Select(item => item.EndsAt!.Value) + .Append(license.EndsAt ?? now.AddHours(4)) + .Min(); + var alternateProfileIds = selections + .Where(item => item.Role == TargetSelectionRole.Alternate) + .Select(item => item.ExamTargetProfileVersionId).Order().ToArray(); + var orderedReleaseIds = validReleaseIds.Order().ToArray(); + var orderedSegmentIds = segmentIds.Distinct().Order().ToArray(); + var orderedManifestIds = manifestIds.Order().ToArray(); + var grantVersion = version?.GrantVersion ?? 1; + var contentVersion = Math.Max(version?.ContentVersion ?? 1, license.Version); + var strongRevocationVersion = version?.StrongRevocationVersion ?? 1; + var projectionId = Guid.NewGuid(); + await access.Database.ExecuteSqlInterpolatedAsync($""" + INSERT INTO effective_access_projections + (id, tenant_id, user_id, business_line_id, primary_profile_version_id, + alternate_profile_version_ids, content_release_ids, audience_segment_ids, + manifest_version_ids, grant_version, content_version, strong_revocation_version, + valid_until, compiled_at) + VALUES + ({projectionId}, {actor.TenantId}, {actor.UserId}, {businessLineId}, + {primary.ExamTargetProfileVersionId}, {alternateProfileIds}, {orderedReleaseIds}, + {orderedSegmentIds}, {orderedManifestIds}, {grantVersion}, {contentVersion}, + {strongRevocationVersion}, {validUntil}, {now}) + ON CONFLICT (tenant_id, user_id, business_line_id) DO UPDATE SET + primary_profile_version_id = EXCLUDED.primary_profile_version_id, + alternate_profile_version_ids = EXCLUDED.alternate_profile_version_ids, + content_release_ids = EXCLUDED.content_release_ids, + audience_segment_ids = EXCLUDED.audience_segment_ids, + manifest_version_ids = EXCLUDED.manifest_version_ids, + grant_version = EXCLUDED.grant_version, + content_version = EXCLUDED.content_version, + strong_revocation_version = EXCLUDED.strong_revocation_version, + valid_until = EXCLUDED.valid_until, + compiled_at = EXCLUDED.compiled_at + """, cancellationToken); + + return new EffectiveLearningAccessSnapshot( + actor.TenantId, + actor.UserId, + businessLineId, + primary.ExamTargetProfileVersionId, + alternateProfileIds.ToHashSet(), + orderedReleaseIds.ToHashSet(), + orderedSegmentIds.ToHashSet(), + orderedManifestIds.ToHashSet(), + grantVersion, + contentVersion, + strongRevocationVersion, + validUntil, + now); + } + + private async Task ValidateManifestTargetsAsync( + Guid tenantId, + IReadOnlyCollection selections, + IReadOnlyCollection grants, + CancellationToken cancellationToken) + { + var manifestIds = grants.Select(item => item.ManifestVersionId).ToArray(); + var targetRows = await access.ProductManifestTargets.AsNoTracking() + .Where(item => item.TenantId == tenantId && + manifestIds.Contains(item.ProductAccessManifestVersionId)) + .ToArrayAsync(cancellationToken); + foreach (var selection in selections) + { + var permitted = targetRows.Any(target => + target.ExamTargetProfileVersionId == selection.ExamTargetProfileVersionId && + (selection.Role == TargetSelectionRole.Primary ? target.MayBePrimary : target.MayBeAlternate)); + if (!permitted) + throw Error("student_target_not_entitled", + "One or more selected exam targets are not covered by an active manifest."); + } + + var activeLimit = grants.Max(item => item.MaxActiveTargets); + var alternateLimit = grants.Max(item => item.MaxAlternateTargets); + if (selections.Count > activeLimit || selections.Count(item => item.Role == TargetSelectionRole.Alternate) > alternateLimit) + throw Error("student_target_product_limit_exceeded", + "The selected primary and alternate targets exceed the product limit."); + } + + private static LearningAccessException Error(string code, string message) => new(code, message); + + private Task ReadForOwnerAsync( + Guid ownerTenantId, + string reason, + Func> read, + CancellationToken cancellationToken) + { + return tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + ownerTenantId, + SystemScopeCallerType.PublicQuestionBank, + nameof(EffectiveLearningAccessService), + reason, + Guid.NewGuid().ToString("N")), + (provider, token) => read(provider.GetRequiredService(), token), + cancellationToken); + } + + internal static string CacheKey(Guid tenantId, Guid userId, Guid businessLineId) => + $"learning-access:v2:{tenantId:N}:{userId:N}:{businessLineId:N}"; + + private sealed record ManifestGrant( + Guid ManifestVersionId, + int MaxActiveTargets, + int MaxAlternateTargets, + DateTimeOffset? EndsAt, + Guid? ClassAssignmentGrantId); + + private sealed record ReleaseFact(Guid TenantId, Guid Id, int ReleaseNo); +} diff --git a/Tiku.Infrastructure/Learning/Access/LearningAccessAdministrationService.cs b/Tiku.Infrastructure/Learning/Access/LearningAccessAdministrationService.cs deleted file mode 100644 index 4fc4833..0000000 --- a/Tiku.Infrastructure/Learning/Access/LearningAccessAdministrationService.cs +++ /dev/null @@ -1,396 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Tiku.Application.Learning; -using Tiku.Domain.Learning; -using Tiku.Domain.Tenancy; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.Infrastructure.Learning; - -internal sealed class LearningAccessAdministrationService( - ILearningAccessPersistence persistence, - ILearningAccessService accessService) : ILearningAccessAdministrationService -{ - public async Task GetTargetRegionAsync( - LearningActor actor, - CancellationToken cancellationToken = default) - { - var business = await ( - from license in persistence.TenantLearningLicenses.AsNoTracking() - join item in persistence.BusinessLines.AsNoTracking() - on license.BusinessLineId equals item.Id - where license.TenantId == actor.TenantId && license.IsPrimary - select new { BusinessLineId = item.Id, item.TargetRegionCooldownDays }) - .FirstOrDefaultAsync(cancellationToken); - if (business is null) return null; - return await ( - from history in persistence.StudentTargetRegionHistory.AsNoTracking() - join region in persistence.MarketRegions.AsNoTracking() - on history.MarketRegionId equals region.Id - where history.TenantId == actor.TenantId && - history.UserId == actor.UserId && - history.BusinessLineId == business.BusinessLineId && - history.IsCurrent - select new StudentTargetRegionItem( - region.Id, - region.Code, - region.Name, - history.EffectiveAt, - history.EffectiveAt.AddDays(business.TargetRegionCooldownDays))) - .SingleOrDefaultAsync(cancellationToken); - } - - public async Task ChangeTargetRegionAsync( - LearningActor actor, - ChangeStudentTargetRegionCommand command, - CancellationToken cancellationToken = default) - { - var now = DateTimeOffset.UtcNow; - var license = await ( - from item in persistence.TenantLearningLicenses - join business in persistence.BusinessLines on item.BusinessLineId equals business.Id - where item.TenantId == actor.TenantId && - item.IsPrimary && - item.Status == LearningLicenseStatus.Active - select new - { - License = item, - business.RegionAccessStrategy, - business.TargetRegionCooldownDays - }) - .SingleOrDefaultAsync(cancellationToken); - if (license is null || license.RegionAccessStrategy != - LearningRegionAccessStrategy.NationalWithStudentTargetRegion) - throw new LearningAccessException( - "student_target_region_not_supported", - "The tenant business does not use personal target regions."); - - var region = await persistence.MarketRegions.AsNoTracking() - .SingleOrDefaultAsync(item => item.Id == command.MarketRegionId && item.IsActive, cancellationToken) - ?? throw new LearningAccessException("market_region_not_found", "The target region was not found."); - var regionLicensed = license.License.AllowsAnyTargetRegion || - await persistence.TenantLearningLicenseRegions.AsNoTracking().AnyAsync( - item => item.TenantId == actor.TenantId && - item.LicenseId == license.License.Id && - item.MarketRegionId == region.Id, - cancellationToken); - if (!regionLicensed) - throw new LearningAccessException( - "student_target_region_not_licensed", - "The selected target region is not covered by the tenant license."); - - await using var transaction = await persistence.Database.BeginTransactionAsync(cancellationToken); - var current = await persistence.StudentTargetRegionHistory.SingleOrDefaultAsync( - item => item.TenantId == actor.TenantId && - item.UserId == actor.UserId && - item.BusinessLineId == license.License.BusinessLineId && - item.IsCurrent, - cancellationToken); - if (current?.MarketRegionId == region.Id) - { - await transaction.RollbackAsync(cancellationToken); - return new StudentTargetRegionItem( - region.Id, region.Code, region.Name, current.EffectiveAt, - current.EffectiveAt.AddDays(license.TargetRegionCooldownDays)); - } - - if (current is not null && current.EffectiveAt.AddDays(license.TargetRegionCooldownDays) > now) - throw new LearningAccessException( - "student_target_region_cooldown", - $"The target region cannot be changed before {current.EffectiveAt.AddDays(license.TargetRegionCooldownDays):O}."); - if (current is not null) - { - current.IsCurrent = false; - current.EndedAt = now; - } - - persistence.StudentTargetRegionHistory.Add(new StudentTargetRegionHistory - { - TenantId = actor.TenantId, - UserId = actor.UserId, - BusinessLineId = license.License.BusinessLineId, - MarketRegionId = region.Id, - EffectiveAt = now, - IsCurrent = true, - ChangedBy = actor.UserId, - Reason = "student_self_service" - }); - await BumpVersionAsync(actor.TenantId, actor.UserId, false, cancellationToken); - await persistence.SaveChangesAsync(cancellationToken); - await transaction.CommitAsync(cancellationToken); - await accessService.InvalidateAsync(actor.TenantId, actor.UserId, cancellationToken); - return new StudentTargetRegionItem( - region.Id, region.Code, region.Name, now, now.AddDays(license.TargetRegionCooldownDays)); - } - - public async Task OverrideTargetRegionAsync( - Guid tenantId, - OverrideStudentTargetRegionCommand command, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(command.Reason)) - throw new LearningAccessException( - "student_target_region_override_reason_required", - "An override reason is required."); - - var studentExists = await persistence.TenantMemberships.AsNoTracking().AnyAsync( - item => item.TenantId == tenantId && - item.UserId == command.UserId && - item.Role == TenantRole.Student && - item.Status == MembershipStatus.Active, - cancellationToken); - if (!studentExists) - throw new LearningAccessException("student_not_found", "The active tenant student was not found."); - - var license = await ( - from item in persistence.TenantLearningLicenses - join business in persistence.BusinessLines on item.BusinessLineId equals business.Id - where item.TenantId == tenantId && - item.IsPrimary && - item.Status == LearningLicenseStatus.Active - select new - { - License = item, - business.RegionAccessStrategy, - business.TargetRegionCooldownDays - }) - .SingleOrDefaultAsync(cancellationToken); - if (license is null || license.RegionAccessStrategy != - LearningRegionAccessStrategy.NationalWithStudentTargetRegion) - throw new LearningAccessException( - "student_target_region_not_supported", - "The tenant business does not use personal target regions."); - - var region = await persistence.MarketRegions.AsNoTracking() - .SingleOrDefaultAsync(item => item.Id == command.MarketRegionId && item.IsActive, cancellationToken) - ?? throw new LearningAccessException("market_region_not_found", "The target region was not found."); - var regionLicensed = license.License.AllowsAnyTargetRegion || - await persistence.TenantLearningLicenseRegions.AsNoTracking().AnyAsync( - item => item.TenantId == tenantId && - item.LicenseId == license.License.Id && - item.MarketRegionId == region.Id, - cancellationToken); - if (!regionLicensed) - throw new LearningAccessException( - "student_target_region_not_licensed", - "The selected target region is not covered by the tenant license."); - - var now = DateTimeOffset.UtcNow; - await using var transaction = await persistence.Database.BeginTransactionAsync(cancellationToken); - var current = await persistence.StudentTargetRegionHistory.SingleOrDefaultAsync( - item => item.TenantId == tenantId && - item.UserId == command.UserId && - item.BusinessLineId == license.License.BusinessLineId && - item.IsCurrent, - cancellationToken); - if (current?.MarketRegionId != region.Id) - { - if (current is not null) - { - current.IsCurrent = false; - current.EndedAt = now; - } - - persistence.StudentTargetRegionHistory.Add(new StudentTargetRegionHistory - { - TenantId = tenantId, - UserId = command.UserId, - BusinessLineId = license.License.BusinessLineId, - MarketRegionId = region.Id, - EffectiveAt = now, - IsCurrent = true, - ChangedBy = command.ChangedBy, - Reason = command.Reason.Trim() - }); - await BumpVersionAsync(tenantId, command.UserId, false, cancellationToken); - await persistence.SaveChangesAsync(cancellationToken); - } - - await transaction.CommitAsync(cancellationToken); - await accessService.InvalidateAsync(tenantId, command.UserId, cancellationToken); - var effectiveAt = current?.MarketRegionId == region.Id ? current.EffectiveAt : now; - return new StudentTargetRegionItem( - region.Id, region.Code, region.Name, effectiveAt, - effectiveAt.AddDays(license.TargetRegionCooldownDays)); - } - - public async Task> GetClassAssignmentsAsync( - Guid tenantId, - Guid classId, - CancellationToken cancellationToken = default) - { - return await persistence.ClassContentAssignments.AsNoTracking() - .Where(item => item.TenantId == tenantId && item.ClassId == classId) - .OrderByDescending(item => item.CreatedAt) - .Select(item => ToItem(item)) - .ToArrayAsync(cancellationToken); - } - - public async Task UpsertClassAssignmentAsync( - Guid tenantId, - Guid actorUserId, - UpsertClassContentAssignmentCommand command, - CancellationToken cancellationToken = default) - { - var now = DateTimeOffset.UtcNow; - if (command.EndsAt.HasValue && command.EndsAt <= (command.StartsAt ?? now)) - throw new LearningAccessException("class_assignment_period_invalid", "Assignment end must follow its start."); - var tenantClass = await persistence.TenantClasses.AsNoTracking().AnyAsync( - item => item.TenantId == tenantId && - item.Id == command.ClassId && - item.Status == TenantRecordStatus.Active, - cancellationToken); - if (!tenantClass) throw new LearningAccessException("class_not_found", "The class was not found."); - - var slice = await persistence.ContentSlices.AsNoTracking().SingleOrDefaultAsync( - item => item.TenantId == tenantId && - item.Id == command.ContentSliceId && - item.ResourceType == command.ResourceType && - item.ResourceId == command.ResourceId && - item.Status == ContentSliceStatus.Active, - cancellationToken) ?? throw new LearningAccessException( - "content_slice_not_found", "The active content slice was not found."); - var license = await persistence.TenantLearningLicenses.AsNoTracking().SingleOrDefaultAsync( - item => item.TenantId == tenantId && - item.IsPrimary && - item.Status == LearningLicenseStatus.Active && - item.BusinessLineId == slice.BusinessLineId, - cancellationToken) ?? throw new LearningAccessException( - "learning_license_scope_mismatch", "The content is outside the tenant learning license."); - if (slice.RegionScope == LearningRegionScopeKind.National && !license.IncludesNational) - throw new LearningAccessException( - "learning_license_scope_mismatch", "The tenant license does not include national content."); - if (slice.MarketRegionId.HasValue && !license.AllowsAnyTargetRegion && - !await persistence.TenantLearningLicenseRegions.AsNoTracking().AnyAsync( - item => item.TenantId == tenantId && - item.LicenseId == license.Id && - item.MarketRegionId == slice.MarketRegionId, - cancellationToken)) - throw new LearningAccessException( - "learning_license_scope_mismatch", "The tenant license does not include the content region."); - - var assignment = command.Id.HasValue - ? await persistence.ClassContentAssignments.SingleOrDefaultAsync( - item => item.TenantId == tenantId && item.Id == command.Id.Value, - cancellationToken) - : null; - assignment ??= new ClassContentAssignment - { - TenantId = tenantId, - ClassId = command.ClassId, - ContentSliceOwnerTenantId = tenantId, - ContentSliceId = slice.Id, - CreatedBy = actorUserId - }; - var isNew = persistence.Entry(assignment).State == EntityState.Detached; - assignment.ClassId = command.ClassId; - assignment.ContentSliceOwnerTenantId = tenantId; - assignment.ContentSliceId = slice.Id; - assignment.ResourceType = command.ResourceType; - assignment.ResourceId = command.ResourceId; - assignment.StartsAt = command.StartsAt ?? now; - assignment.EndsAt = command.EndsAt; - assignment.Status = ClassContentAssignmentStatus.Active; - assignment.RevokedBy = null; - assignment.RevokedReason = null; - if (isNew) persistence.ClassContentAssignments.Add(assignment); - await persistence.SaveChangesAsync(cancellationToken); - await InvalidateClassMembersAsync(tenantId, command.ClassId, false, cancellationToken); - return ToItem(assignment); - } - - public async Task RevokeClassAssignmentAsync( - Guid tenantId, - Guid actorUserId, - Guid assignmentId, - string? reason, - CancellationToken cancellationToken = default) - { - var assignment = await persistence.ClassContentAssignments.SingleOrDefaultAsync( - item => item.TenantId == tenantId && item.Id == assignmentId, - cancellationToken) ?? throw new LearningAccessException("class_assignment_not_found", "Assignment was not found."); - assignment.Status = ClassContentAssignmentStatus.Revoked; - assignment.RevokedBy = actorUserId; - assignment.RevokedReason = string.IsNullOrWhiteSpace(reason) ? "revoked_by_teacher" : reason.Trim(); - assignment.EndsAt = DateTimeOffset.UtcNow; - await persistence.SaveChangesAsync(cancellationToken); - await InvalidateClassMembersAsync(tenantId, assignment.ClassId, true, cancellationToken); - return ToItem(assignment); - } - - private async Task InvalidateClassMembersAsync( - Guid tenantId, - Guid classId, - bool strongRevocation, - CancellationToken cancellationToken) - { - var userIds = await persistence.TenantClassMembers.AsNoTracking() - .Where(item => item.TenantId == tenantId && - item.ClassId == classId && - item.MemberType == TenantClassMemberType.Student && - item.Status == TenantClassMemberStatus.Active) - .Select(item => item.UserId) - .Distinct() - .ToArrayAsync(cancellationToken); - var versions = await persistence.LearningAccessVersions - .Where(item => item.TenantId == tenantId && userIds.Contains(item.UserId)) - .ToDictionaryAsync(item => item.UserId, cancellationToken); - foreach (var userId in userIds) - { - if (!versions.TryGetValue(userId, out var version)) - { - persistence.LearningAccessVersions.Add(new LearningAccessVersion - { - TenantId = tenantId, - UserId = userId, - StrongRevocationVersion = strongRevocation ? 2 : 1 - }); - continue; - } - - version.GrantVersion++; - if (strongRevocation) version.StrongRevocationVersion++; - version.UpdatedAt = DateTimeOffset.UtcNow; - } - - await persistence.SaveChangesAsync(cancellationToken); - await Task.WhenAll(userIds.Select(userId => - accessService.InvalidateAsync(tenantId, userId, cancellationToken))); - } - - private async Task BumpVersionAsync( - Guid tenantId, - Guid userId, - bool strongRevocation, - CancellationToken cancellationToken) - { - var version = await persistence.LearningAccessVersions.SingleOrDefaultAsync( - item => item.TenantId == tenantId && item.UserId == userId, - cancellationToken); - if (version is null) - { - version = new LearningAccessVersion - { - TenantId = tenantId, - UserId = userId, - StrongRevocationVersion = strongRevocation ? 2 : 1 - }; - persistence.LearningAccessVersions.Add(version); - } - else - { - version.GrantVersion++; - if (strongRevocation) version.StrongRevocationVersion++; - version.UpdatedAt = DateTimeOffset.UtcNow; - } - } - - private static ClassContentAssignmentItem ToItem(ClassContentAssignment item) => new( - item.Id, - item.ClassId, - item.ContentSliceId, - item.ResourceType, - item.ResourceId, - item.StartsAt, - item.EndsAt, - item.Status); -} diff --git a/Tiku.Infrastructure/Learning/Access/LearningStrongRevocationService.cs b/Tiku.Infrastructure/Learning/Access/LearningStrongRevocationService.cs new file mode 100644 index 0000000..1d56964 --- /dev/null +++ b/Tiku.Infrastructure/Learning/Access/LearningStrongRevocationService.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tiku.Application.Learning; +using Tiku.Domain.Identity; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Caching; +using Tiku.Infrastructure.Persistence; +using ZiggyCreatures.Caching.Fusion; + +namespace Tiku.Infrastructure.Learning; + +internal sealed class LearningStrongRevocationService( + ILearningAccessPersistence persistence, + [FromKeyedServices(BusinessCachingServiceCollectionExtensions.CacheName)] + IFusionCache cache) : ILearningStrongRevocationService +{ + private static readonly TimeSpan Lifetime = TimeSpan.FromSeconds(1); + + public async Task EnsureVersionAsync( + LearningActor actor, + long expectedVersion, + CancellationToken cancellationToken = default) + { + var current = await cache.GetOrSetAsync( + CacheKey(actor.TenantId, actor.UserId), + (_, token) => LoadAsync(actor, token), + options => options.SetDuration(Lifetime).SetFailSafe(false), + token: cancellationToken); + if (current != expectedVersion) + throw new LearningAccessException( + "practice_access_revoked", + "The practice session was strongly revoked and can no longer be used."); + } + + public Task InvalidateAsync( + Guid tenantId, + Guid userId, + CancellationToken cancellationToken = default) + { + return cache.RemoveAsync(CacheKey(tenantId, userId), token: cancellationToken).AsTask(); + } + + private async Task LoadAsync(LearningActor actor, CancellationToken cancellationToken) + { + var active = await ( + from tenant in persistence.Tenants.AsNoTracking() + join user in persistence.Users.AsNoTracking() on actor.UserId equals user.Id + where tenant.Id == actor.TenantId && + tenant.Status == TenantStatus.Active && + user.Status == UserStatus.Active + select tenant.Id) + .AnyAsync(cancellationToken); + if (!active) + throw new LearningAccessException( + "practice_access_revoked", + "The tenant or student account is inactive."); + return await persistence.LearningAccessVersions.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId) + .Select(item => (long?)item.StrongRevocationVersion) + .SingleOrDefaultAsync(cancellationToken) ?? 1; + } + + internal static string CacheKey(Guid tenantId, Guid userId) => + $"learning-strong-revocation:v1:{tenantId:N}:{userId:N}"; +} diff --git a/Tiku.Infrastructure/Learning/Access/StudentLearningCatalogService.cs b/Tiku.Infrastructure/Learning/Access/StudentLearningCatalogService.cs new file mode 100644 index 0000000..e0f9a0e --- /dev/null +++ b/Tiku.Infrastructure/Learning/Access/StudentLearningCatalogService.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore; +using Tiku.Application.Learning; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure.Learning; + +internal sealed class StudentLearningCatalogService( + ILearningAccessPersistence access, + IEffectiveLearningAccessService effectiveAccess) : IStudentLearningCatalogService +{ + public async Task GetAsync( + LearningActor actor, + Guid businessLineId, + CancellationToken cancellationToken = default) + { + var snapshot = await effectiveAccess.GetSnapshotAsync(actor, businessLineId, cancellationToken); + var manifestIds = snapshot.ManifestVersionIds.ToArray(); + var rows = await access.ProductManifestResources.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && + manifestIds.Contains(item.ProductAccessManifestVersionId)) + .OrderBy(item => item.SortOrder) + .ThenBy(item => item.Id) + .ToArrayAsync(cancellationToken); + var resources = rows + .GroupBy(item => new { item.ResourceType, item.ResourceId }) + .Select(group => + { + var first = group.OrderBy(item => item.SortOrder).ThenBy(item => item.Id).First(); + return new StudentCatalogResourceItem( + first.ResourceType, + first.ResourceId, + string.IsNullOrWhiteSpace(first.DisplayName) + ? first.ResourceType.ToString() + : first.DisplayName, + first.SortOrder, + group.Select(item => item.ProductAccessManifestVersionId).Distinct().Order().ToArray()); + }) + .OrderBy(item => item.SortOrder) + .ThenBy(item => item.DisplayName) + .ThenBy(item => item.ResourceId) + .ToArray(); + return new StudentLearningCatalogItem( + businessLineId, + snapshot.PrimaryProfileVersionId, + snapshot.AlternateProfileVersionIds.Order().ToArray(), + resources, + snapshot.GrantVersion, + snapshot.ContentVersion); + } +} diff --git a/Tiku.Infrastructure/Learning/Access/StudentLearningTargetService.cs b/Tiku.Infrastructure/Learning/Access/StudentLearningTargetService.cs new file mode 100644 index 0000000..17ff394 --- /dev/null +++ b/Tiku.Infrastructure/Learning/Access/StudentLearningTargetService.cs @@ -0,0 +1,493 @@ +using System.Data; +using Microsoft.EntityFrameworkCore; +using Tiku.Application.Learning; +using Tiku.Domain.Content; +using Tiku.Domain.Learning; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure.Learning; + +internal sealed class StudentLearningTargetService( + ILearningAccessPersistence access, + IQuestionBankPersistence content, + IEffectiveLearningAccessService effectiveAccess) : IStudentLearningTargetService +{ + public async Task GetContextAsync( + LearningActor actor, + CancellationToken cancellationToken = default) + { + await EnsureTenantActiveAsync(actor.TenantId, cancellationToken); + var now = DateTimeOffset.UtcNow; + var licenses = await ( + from license in access.TenantBusinessLicenses.AsNoTracking() + join business in access.BusinessLines.AsNoTracking() on license.BusinessLineId equals business.Id + where license.TenantId == actor.TenantId && + license.Status == LicenseLifecycleStatus.Active && + license.StartsAt <= now && + (license.EndsAt == null || license.EndsAt > now) && + business.IsActive + orderby business.Name, business.Code + select new { License = license, Business = business }) + .ToArrayAsync(cancellationToken); + + var items = new List(licenses.Length); + foreach (var row in licenses) + { + var policy = await GetPolicyAsync(row.Business.Id, cancellationToken); + var selections = await CurrentSelectionsAsync(actor, row.Business.Id, cancellationToken); + items.Add(new LearningBusinessContextItem( + row.Business.Id, + row.Business.Code, + row.Business.Name, + row.License.Id, + row.License.EndsAt, + selections.SingleOrDefault(item => item.Role == TargetSelectionRole.Primary) + ?.ExamTargetProfileVersionId, + selections.Where(item => item.Role == TargetSelectionRole.Alternate) + .Select(item => item.ExamTargetProfileVersionId).Order().ToArray(), + policy.MaxActiveTargets, + policy.MaxAlternateTargets, + policy.TargetChangeCooldownDays)); + } + + return new LearningContextItem(items); + } + + public async Task> GetTargetsAsync( + LearningActor actor, + Guid businessLineId, + CancellationToken cancellationToken = default) + { + var now = DateTimeOffset.UtcNow; + var license = await RequireLicenseAsync(actor.TenantId, businessLineId, now, cancellationToken); + var selections = await CurrentSelectionsAsync(actor, businessLineId, cancellationToken); + var selectedIds = selections.Select(item => item.ExamTargetProfileVersionId).ToHashSet(); + var targetAccess = await LoadTargetAccessAsync(actor, businessLineId, now, cancellationToken); + var manifestTargets = targetAccess.Targets; + var targetRoles = manifestTargets + .GroupBy(item => item.ExamTargetProfileVersionId) + .ToDictionary(group => group.Key, group => new + { + Primary = group.Any(item => item.MayBePrimary), + Alternate = group.Any(item => item.MayBeAlternate) + }); + + var licensed = await access.TenantLicensedTargets.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && + item.TenantBusinessLicenseId == license.Id && + item.StartsAt <= now && + (item.EndsAt == null || item.EndsAt > now)) + .ToArrayAsync(cancellationToken); + var profileIds = licensed.Select(item => item.ExamTargetProfileVersionId).Distinct().ToArray(); + var profiles = await ( + from version in content.ExamTargetProfileVersions.AsNoTracking() + join profile in content.ExamTargetProfiles.AsNoTracking() + on version.ExamTargetProfileId equals profile.Id + where profileIds.Contains(version.Id) && + profile.BusinessLineId == businessLineId && + profile.IsActive && + version.Status == ContentDefinitionStatus.Published + select new { Version = version, Profile = profile }) + .ToArrayAsync(cancellationToken); + + return profiles.Select(row => + { + targetRoles.TryGetValue(row.Version.Id, out var role); + var selection = selections.SingleOrDefault(item => item.ExamTargetProfileVersionId == row.Version.Id); + return new LearningTargetItem( + row.Version.Id, + row.Profile.Code, + row.Version.DisplayName, + row.Version.ExamYear, + licensed.Single(item => item.ExamTargetProfileVersionId == row.Version.Id).IsBaseTarget, + role?.Primary == true, + role?.Alternate == true, + selection?.Role == TargetSelectionRole.Primary, + selection?.Role == TargetSelectionRole.Alternate); + }).Where(item => selectedIds.Contains(item.ExamTargetProfileVersionId) || + item.MayBePrimary || item.MayBeAlternate) + .OrderByDescending(item => item.ExamYear) + .ThenBy(item => item.DisplayName) + .ToArray(); + } + + public async Task ChangeTargetsAsync( + LearningActor actor, + Guid businessLineId, + ChangeLearningTargetsCommand command, + CancellationToken cancellationToken = default) + { + return await ChangeTargetsCoreAsync( + actor, + businessLineId, + command, + actor.UserId, + "student_self_service", + true, + cancellationToken); + } + + public async Task OverrideTargetsAsync( + LearningActor student, + Guid businessLineId, + ChangeLearningTargetsCommand command, + Guid changedBy, + string reason, + CancellationToken cancellationToken = default) + { + if (changedBy == Guid.Empty || string.IsNullOrWhiteSpace(reason)) + throw Error("student_target_override_reason_required", + "An administrator and a non-empty reason are required for a target override."); + + return await ChangeTargetsCoreAsync( + student, + businessLineId, + command, + changedBy, + reason.Trim(), + false, + cancellationToken); + } + + private async Task ChangeTargetsCoreAsync( + LearningActor actor, + Guid businessLineId, + ChangeLearningTargetsCommand command, + Guid changedBy, + string reason, + bool enforceCooldown, + CancellationToken cancellationToken) + { + var alternateIds = command.AlternateProfileVersionIds.Distinct().ToArray(); + if (alternateIds.Length != command.AlternateProfileVersionIds.Count || + alternateIds.Contains(command.PrimaryProfileVersionId)) + throw Error("student_target_selection_duplicate", "Primary and alternate targets must be unique."); + + var now = DateTimeOffset.UtcNow; + await using var transaction = await access.Database.BeginTransactionAsync( + IsolationLevel.ReadCommitted, + cancellationToken); + var lockKey = $"student-target:{actor.TenantId:N}:{actor.UserId:N}:{businessLineId:N}"; + await access.Database.ExecuteSqlInterpolatedAsync( + $"SELECT pg_advisory_xact_lock(hashtextextended({lockKey}, 0))", + cancellationToken); + var license = await RequireLicenseAsync(actor.TenantId, businessLineId, now, cancellationToken); + var policy = await GetPolicyAsync(businessLineId, cancellationToken); + if (policy.MaxPrimaryTargets < 1 || + 1 + alternateIds.Length > Math.Min(16, policy.MaxActiveTargets) || + alternateIds.Length > policy.MaxAlternateTargets) + throw Error("student_target_policy_limit_exceeded", + "The requested primary and alternate targets exceed the business policy."); + + var requestedIds = alternateIds.Append(command.PrimaryProfileVersionId).ToArray(); + var licensedIds = await access.TenantLicensedTargets.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && + item.TenantBusinessLicenseId == license.Id && + item.StartsAt <= now && + (item.EndsAt == null || item.EndsAt > now) && + requestedIds.Contains(item.ExamTargetProfileVersionId)) + .Select(item => item.ExamTargetProfileVersionId) + .Distinct() + .ToArrayAsync(cancellationToken); + if (licensedIds.Length != requestedIds.Length) + throw Error("student_target_not_licensed", "One or more requested targets are not tenant licensed."); + + var targetAccess = await LoadTargetAccessAsync(actor, businessLineId, now, cancellationToken); + var entitledTargets = targetAccess.Targets; + if (1 + alternateIds.Length > targetAccess.MaxActiveTargets || + alternateIds.Length > targetAccess.MaxAlternateTargets) + throw Error("student_target_product_limit_exceeded", + "The requested primary and alternate targets exceed the active product limit."); + if (!entitledTargets.Any(item => item.ExamTargetProfileVersionId == command.PrimaryProfileVersionId && + item.MayBePrimary) || + alternateIds.Any(id => !entitledTargets.Any(item => + item.ExamTargetProfileVersionId == id && item.MayBeAlternate))) + throw Error("student_target_not_entitled", "One or more requested targets are not product entitled."); + + var current = await access.StudentTargetSelectionHistory + .Where(item => item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.BusinessLineId == businessLineId && + item.IsCurrent) + .ToArrayAsync(cancellationToken); + if (SameSelection(current, command.PrimaryProfileVersionId, alternateIds)) + { + await transaction.CommitAsync(cancellationToken); + var changedAt = current.Max(item => item.EffectiveAt); + return Result(businessLineId, command.PrimaryProfileVersionId, alternateIds, changedAt, policy); + } + + var lastChangeAt = current.Length == 0 + ? await access.StudentTargetSelectionHistory.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.BusinessLineId == businessLineId) + .Select(item => (DateTimeOffset?)item.EffectiveAt) + .MaxAsync(cancellationToken) + : current.Max(item => item.EffectiveAt); + var nextAllowedAt = lastChangeAt?.AddDays(policy.TargetChangeCooldownDays); + if (enforceCooldown && nextAllowedAt > now) + throw Error("student_target_change_cooldown", + $"Targets cannot be changed before {nextAllowedAt:O}."); + + foreach (var selection in current) + { + selection.IsCurrent = false; + selection.EndedAt = now; + selection.UpdatedAt = now; + } + + var primarySource = ResolveSelectionSource( + targetAccess, command.PrimaryProfileVersionId, TargetSelectionRole.Primary); + access.StudentTargetSelectionHistory.Add(NewSelection( + actor, businessLineId, command.PrimaryProfileVersionId, TargetSelectionRole.Primary, primarySource, + changedBy, reason, now)); + foreach (var alternateId in alternateIds) + { + var alternateSource = ResolveSelectionSource( + targetAccess, alternateId, TargetSelectionRole.Alternate); + access.StudentTargetSelectionHistory.Add(NewSelection( + actor, businessLineId, alternateId, TargetSelectionRole.Alternate, alternateSource, + changedBy, reason, now)); + } + + var version = await access.LearningAccessVersions.SingleOrDefaultAsync( + item => item.TenantId == actor.TenantId && item.UserId == actor.UserId, + cancellationToken); + version ??= new LearningAccessVersion { TenantId = actor.TenantId, UserId = actor.UserId }; + if (access.Entry(version).State == EntityState.Detached) access.LearningAccessVersions.Add(version); + version.GrantVersion++; + version.UpdatedAt = now; + await access.EffectiveAccessProjections + .Where(item => item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.BusinessLineId == businessLineId) + .ExecuteDeleteAsync(cancellationToken); + await access.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + await effectiveAccess.InvalidateAsync(actor.TenantId, actor.UserId, businessLineId, cancellationToken); + return Result(businessLineId, command.PrimaryProfileVersionId, alternateIds, now, policy); + } + + private async Task LoadTargetAccessAsync( + LearningActor actor, + Guid businessLineId, + DateTimeOffset now, + CancellationToken cancellationToken) + { + var entitlementManifests = await ( + from entitlement in access.StudentEntitlementsV2.AsNoTracking() + join product in access.LearningProducts.AsNoTracking() + on new { entitlement.TenantId, Id = entitlement.LearningProductId } + equals new { product.TenantId, product.Id } + join manifest in access.ProductAccessManifestVersions.AsNoTracking() + on new { entitlement.TenantId, Id = entitlement.ProductAccessManifestVersionId } + equals new { manifest.TenantId, manifest.Id } + where entitlement.TenantId == actor.TenantId && + entitlement.UserId == actor.UserId && + entitlement.Status == StudentEntitlementStatus.Active && + entitlement.StartsAt <= now && + (entitlement.EndsAt == null || entitlement.EndsAt > now) && + product.BusinessLineId == businessLineId && + product.IsActive && + manifest.Status == AccessManifestStatus.Published + select new ManifestGrant( + manifest.Id, + manifest.MaxActiveTargets, + manifest.MaxAlternateTargets, + "student_entitlement", + entitlement.Id)) + .ToArrayAsync(cancellationToken); + + var classManifests = await ( + from member in access.TenantClassMembers.AsNoTracking() + join grant in access.ClassAssignmentGrants.AsNoTracking() + on new { member.TenantId, member.ClassId } equals new { grant.TenantId, grant.ClassId } + join manifest in access.ProductAccessManifestVersions.AsNoTracking() + on new { grant.TenantId, Id = grant.ProductAccessManifestVersionId } + equals new { manifest.TenantId, manifest.Id } + join definition in access.ProductAccessManifests.AsNoTracking() + on new { manifest.TenantId, Id = manifest.ProductAccessManifestId } + equals new { definition.TenantId, definition.Id } + join product in access.LearningProducts.AsNoTracking() + on new { definition.TenantId, Id = definition.LearningProductId } + equals new { product.TenantId, product.Id } + where member.TenantId == actor.TenantId && + member.UserId == actor.UserId && + member.MemberType == TenantClassMemberType.Student && + member.Status == TenantClassMemberStatus.Active && + grant.Status == ClassAssignmentGrantStatus.Active && + grant.StartsAt <= now && + (grant.EndsAt == null || grant.EndsAt > now) && + manifest.Status == AccessManifestStatus.Published && + product.BusinessLineId == businessLineId && + product.IsActive + select new ManifestGrant( + manifest.Id, + manifest.MaxActiveTargets, + manifest.MaxAlternateTargets, + "class_assignment_grant", + grant.Id)) + .ToArrayAsync(cancellationToken); + + var manifests = entitlementManifests.Concat(classManifests) + .DistinctBy(item => new { item.ManifestVersionId, item.SourceType, item.SourceId }) + .ToArray(); + if (manifests.Length == 0) return new TargetAccess([], [], 0, 0); + var manifestIds = manifests.Select(item => item.ManifestVersionId).ToArray(); + var targets = await access.ProductManifestTargets.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && manifestIds.Contains(item.ProductAccessManifestVersionId)) + .ToArrayAsync(cancellationToken); + return new TargetAccess( + targets, + manifests, + manifests.Max(item => item.MaxActiveTargets), + manifests.Max(item => item.MaxAlternateTargets)); + } + + private async Task RequireLicenseAsync( + Guid tenantId, + Guid businessLineId, + DateTimeOffset now, + CancellationToken cancellationToken) + { + await EnsureTenantActiveAsync(tenantId, cancellationToken); + return await access.TenantBusinessLicenses.AsNoTracking().SingleOrDefaultAsync( + item => item.TenantId == tenantId && + item.BusinessLineId == businessLineId && + item.Status == LicenseLifecycleStatus.Active && + item.StartsAt <= now && + (item.EndsAt == null || item.EndsAt > now), + cancellationToken) ?? throw Error("learning_license_required", + "The tenant has no active license for the requested business."); + } + + private async Task EnsureTenantActiveAsync(Guid tenantId, CancellationToken cancellationToken) + { + if (!await access.Tenants.AsNoTracking().AnyAsync( + item => item.Id == tenantId && item.Status == TenantStatus.Active, + cancellationToken)) + throw Error("learning_tenant_inactive", "The tenant is not active."); + } + + private async Task GetPolicyAsync( + Guid businessLineId, + CancellationToken cancellationToken) + { + return await ( + from policy in content.BusinessTargetPolicies.AsNoTracking() + join version in content.BusinessTargetPolicyVersions.AsNoTracking() + on new { PolicyId = policy.Id, VersionId = policy.CurrentVersionId } + equals new { PolicyId = version.BusinessTargetPolicyId, VersionId = (Guid?)version.Id } + where policy.BusinessLineId == businessLineId && + policy.IsActive && + version.Status == ContentDefinitionStatus.Published + select version) + .SingleOrDefaultAsync(cancellationToken) ?? throw Error("business_target_policy_required", + "The business has no published target policy."); + } + + private Task CurrentSelectionsAsync( + LearningActor actor, + Guid businessLineId, + CancellationToken cancellationToken) + { + return access.StudentTargetSelectionHistory.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.BusinessLineId == businessLineId && + item.IsCurrent) + .ToArrayAsync(cancellationToken); + } + + private static bool SameSelection( + IReadOnlyCollection current, + Guid primaryId, + IReadOnlyCollection alternateIds) + { + return current.Count(item => item.Role == TargetSelectionRole.Primary) == 1 && + current.Single(item => item.Role == TargetSelectionRole.Primary).ExamTargetProfileVersionId == primaryId && + current.Where(item => item.Role == TargetSelectionRole.Alternate) + .Select(item => item.ExamTargetProfileVersionId).ToHashSet().SetEquals(alternateIds); + } + + private static StudentTargetSelectionHistory NewSelection( + LearningActor actor, + Guid businessLineId, + Guid profileVersionId, + TargetSelectionRole role, + SelectionSource source, + Guid changedBy, + string reason, + DateTimeOffset now) + { + return new StudentTargetSelectionHistory + { + TenantId = actor.TenantId, + UserId = actor.UserId, + BusinessLineId = businessLineId, + ExamTargetProfileVersionId = profileVersionId, + Role = role, + EntitlementId = source.SourceType == "student_entitlement" ? source.SourceId : null, + SourceType = source.SourceType, + SourceId = source.SourceId, + EffectiveAt = now, + IsCurrent = true, + ChangedBy = changedBy, + Reason = reason + }; + } + + private static LearningTargetSelectionItem Result( + Guid businessLineId, + Guid primaryId, + IReadOnlyCollection alternateIds, + DateTimeOffset changedAt, + BusinessTargetPolicyVersion policy) + { + return new LearningTargetSelectionItem( + businessLineId, + primaryId, + alternateIds.Order().ToArray(), + changedAt, + policy.TargetChangeCooldownDays == 0 ? changedAt : changedAt.AddDays(policy.TargetChangeCooldownDays)); + } + + private static LearningAccessException Error(string code, string message) => new(code, message); + + private static SelectionSource ResolveSelectionSource( + TargetAccess access, + Guid profileVersionId, + TargetSelectionRole role) + { + var manifestIds = access.Targets + .Where(item => item.ExamTargetProfileVersionId == profileVersionId && + (role == TargetSelectionRole.Primary ? item.MayBePrimary : item.MayBeAlternate)) + .Select(item => item.ProductAccessManifestVersionId) + .ToHashSet(); + var grant = access.Grants + .Where(item => manifestIds.Contains(item.ManifestVersionId)) + .OrderBy(item => item.SourceType == "student_entitlement" ? 0 : 1) + .ThenBy(item => item.SourceId) + .FirstOrDefault() ?? throw Error( + "student_target_source_missing", + "The selected target has no auditable active entitlement source."); + return new SelectionSource(grant.SourceType, grant.SourceId); + } + + private sealed record ManifestGrant( + Guid ManifestVersionId, + int MaxActiveTargets, + int MaxAlternateTargets, + string SourceType, + Guid SourceId); + + private sealed record SelectionSource(string SourceType, Guid SourceId); + + private sealed record TargetAccess( + ProductManifestTarget[] Targets, + ManifestGrant[] Grants, + int MaxActiveTargets, + int MaxAlternateTargets); +} diff --git a/Tiku.Infrastructure/Learning/Access/V2LearningAccessAdministrationService.cs b/Tiku.Infrastructure/Learning/Access/V2LearningAccessAdministrationService.cs new file mode 100644 index 0000000..7119aa1 --- /dev/null +++ b/Tiku.Infrastructure/Learning/Access/V2LearningAccessAdministrationService.cs @@ -0,0 +1,262 @@ +using System.Data; +using Microsoft.EntityFrameworkCore; +using Tiku.Application.Learning; +using Tiku.Domain.Learning; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure.Learning; + +internal sealed class V2LearningAccessAdministrationService( + ILearningAccessPersistence access, + IEffectiveLearningAccessService effectiveAccess) : IV2LearningAccessAdministrationService +{ + public async Task> GetClassGrantsAsync( + Guid tenantId, + Guid classId, + CancellationToken cancellationToken = default) + { + await RequireClassAsync(tenantId, classId, cancellationToken); + var items = await access.ClassAssignmentGrants.AsNoTracking() + .Where(item => item.TenantId == tenantId && item.ClassId == classId) + .OrderByDescending(item => item.CreatedAt) + .ToArrayAsync(cancellationToken); + return items.Select(ToItem).ToArray(); + } + + public async Task UpsertClassGrantAsync( + Guid tenantId, + Guid actorUserId, + UpsertClassAssignmentGrantCommand command, + CancellationToken cancellationToken = default) + { + var now = DateTimeOffset.UtcNow; + var startsAt = command.StartsAt ?? now; + if (command.EndsAt <= startsAt) + throw Error("class_assignment_period_invalid", "The assignment end must be after its start."); + + await using var transaction = await access.Database.BeginTransactionAsync( + IsolationLevel.ReadCommitted, + cancellationToken); + await RequireClassAsync(tenantId, command.ClassId, cancellationToken); + var businessLineId = await EnsureManifestResourceLicensedAsync(tenantId, command, now, cancellationToken); + + ClassAssignmentGrant item; + if (command.Id.HasValue) + { + item = await access.ClassAssignmentGrants.SingleOrDefaultAsync(value => + value.TenantId == tenantId && value.Id == command.Id.Value, + cancellationToken) + ?? throw Error("class_assignment_not_found", "The class assignment was not found."); + if (item.Status != ClassAssignmentGrantStatus.Active) + throw Error("class_assignment_not_active", "Only an active class assignment can be changed."); + if (item.ClassId != command.ClassId) + throw Error("class_assignment_class_immutable", "The assignment class cannot be changed."); + } + else + { + item = new ClassAssignmentGrant + { + TenantId = tenantId, + ClassId = command.ClassId, + CreatedBy = actorUserId + }; + access.ClassAssignmentGrants.Add(item); + } + + item.ProductAccessManifestVersionId = command.ProductAccessManifestVersionId; + item.ResourceType = command.ResourceType; + item.ResourceId = command.ResourceId; + item.StartsAt = startsAt; + item.EndsAt = command.EndsAt; + item.UpdatedAt = now; + await access.SaveChangesAsync(cancellationToken); + var affectedUsers = await BumpAffectedStudentsAsync(tenantId, command.ClassId, now, cancellationToken); + await transaction.CommitAsync(cancellationToken); + await InvalidateAsync(tenantId, affectedUsers, businessLineId, cancellationToken); + return ToItem(item); + } + + public async Task RevokeClassGrantAsync( + Guid tenantId, + Guid actorUserId, + Guid classId, + Guid grantId, + string reason, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(reason)) + throw Error("class_assignment_revoke_reason_required", "A revoke reason is required."); + + var now = DateTimeOffset.UtcNow; + await using var transaction = await access.Database.BeginTransactionAsync(cancellationToken); + var item = await access.ClassAssignmentGrants.SingleOrDefaultAsync(value => + value.TenantId == tenantId && value.ClassId == classId && value.Id == grantId, + cancellationToken) + ?? throw Error("class_assignment_not_found", "The class assignment was not found."); + var businessLineId = await ResolveManifestBusinessLineAsync( + tenantId, item.ProductAccessManifestVersionId, cancellationToken); + if (item.Status == ClassAssignmentGrantStatus.Revoked) + { + await transaction.CommitAsync(cancellationToken); + return ToItem(item); + } + + item.Status = ClassAssignmentGrantStatus.Revoked; + item.RevokedBy = actorUserId; + item.RevokedReason = reason.Trim(); + item.UpdatedAt = now; + await access.SaveChangesAsync(cancellationToken); + var affectedUsers = await BumpAffectedStudentsAsync(tenantId, item.ClassId, now, cancellationToken); + await transaction.CommitAsync(cancellationToken); + await InvalidateAsync(tenantId, affectedUsers, businessLineId, cancellationToken); + return ToItem(item); + } + + private async Task EnsureManifestResourceLicensedAsync( + Guid tenantId, + UpsertClassAssignmentGrantCommand command, + DateTimeOffset now, + CancellationToken cancellationToken) + { + var manifest = await ( + from version in access.ProductAccessManifestVersions.AsNoTracking() + join definition in access.ProductAccessManifests.AsNoTracking() + on new { version.TenantId, Id = version.ProductAccessManifestId } + equals new { definition.TenantId, definition.Id } + join product in access.LearningProducts.AsNoTracking() + on new { definition.TenantId, Id = definition.LearningProductId } + equals new { product.TenantId, product.Id } + join resource in access.ProductManifestResources.AsNoTracking() + on new { version.TenantId, VersionId = version.Id } + equals new { resource.TenantId, VersionId = resource.ProductAccessManifestVersionId } + where version.TenantId == tenantId && + version.Id == command.ProductAccessManifestVersionId && + version.Status == AccessManifestStatus.Published && + definition.IsActive && product.IsActive && + resource.ResourceType == command.ResourceType && + resource.ResourceId == command.ResourceId + select new { product.BusinessLineId }) + .SingleOrDefaultAsync(cancellationToken) + ?? throw Error("class_assignment_manifest_resource_invalid", + "The resource is not present in an active published manifest."); + + var license = await access.TenantBusinessLicenses.AsNoTracking().SingleOrDefaultAsync(item => + item.TenantId == tenantId && + item.BusinessLineId == manifest.BusinessLineId && + item.Status == LicenseLifecycleStatus.Active && + item.StartsAt <= now && + (item.EndsAt == null || item.EndsAt > now), cancellationToken) + ?? throw Error("class_assignment_tenant_license_required", + "The tenant is not licensed for the manifest business."); + + var manifestTargetIds = await access.ProductManifestTargets.AsNoTracking() + .Where(item => item.TenantId == tenantId && + item.ProductAccessManifestVersionId == command.ProductAccessManifestVersionId) + .Select(item => item.ExamTargetProfileVersionId) + .Distinct() + .ToArrayAsync(cancellationToken); + var licensedTargetCount = await access.TenantLicensedTargets.AsNoTracking() + .Where(item => item.TenantId == tenantId && + item.TenantBusinessLicenseId == license.Id && + item.StartsAt <= now && + (item.EndsAt == null || item.EndsAt > now) && + manifestTargetIds.Contains(item.ExamTargetProfileVersionId)) + .Select(item => item.ExamTargetProfileVersionId) + .Distinct() + .CountAsync(cancellationToken); + if (licensedTargetCount != manifestTargetIds.Length) + throw Error("class_assignment_target_not_licensed", + "The class assignment manifest exceeds the tenant licensed targets."); + return manifest.BusinessLineId; + } + + private async Task BumpAffectedStudentsAsync( + Guid tenantId, + Guid classId, + DateTimeOffset now, + CancellationToken cancellationToken) + { + var userIds = await access.TenantClassMembers.AsNoTracking() + .Where(item => item.TenantId == tenantId && + item.ClassId == classId && + item.MemberType == TenantClassMemberType.Student && + item.Status == TenantClassMemberStatus.Active) + .Select(item => item.UserId) + .Distinct() + .ToArrayAsync(cancellationToken); + if (userIds.Length == 0) return userIds; + + var versions = await access.LearningAccessVersions + .Where(item => item.TenantId == tenantId && userIds.Contains(item.UserId)) + .ToDictionaryAsync(item => item.UserId, cancellationToken); + foreach (var userId in userIds) + { + if (!versions.TryGetValue(userId, out var version)) + { + version = new LearningAccessVersion { TenantId = tenantId, UserId = userId }; + access.LearningAccessVersions.Add(version); + } + + version.GrantVersion++; + version.UpdatedAt = now; + } + + await access.EffectiveAccessProjections + .Where(item => item.TenantId == tenantId && userIds.Contains(item.UserId)) + .ExecuteDeleteAsync(cancellationToken); + await access.SaveChangesAsync(cancellationToken); + return userIds; + } + + private async Task InvalidateAsync( + Guid tenantId, + IEnumerable userIds, + Guid businessLineId, + CancellationToken cancellationToken) + { + foreach (var userId in userIds) + await effectiveAccess.InvalidateAsync(tenantId, userId, businessLineId, cancellationToken); + } + + private async Task ResolveManifestBusinessLineAsync( + Guid tenantId, + Guid manifestVersionId, + CancellationToken cancellationToken) + { + return await ( + from version in access.ProductAccessManifestVersions.AsNoTracking() + join definition in access.ProductAccessManifests.AsNoTracking() + on new { version.TenantId, Id = version.ProductAccessManifestId } + equals new { definition.TenantId, definition.Id } + join product in access.LearningProducts.AsNoTracking() + on new { definition.TenantId, Id = definition.LearningProductId } + equals new { product.TenantId, product.Id } + where version.TenantId == tenantId && version.Id == manifestVersionId + select product.BusinessLineId) + .SingleAsync(cancellationToken); + } + + private async Task RequireClassAsync(Guid tenantId, Guid classId, CancellationToken cancellationToken) + { + if (!await access.TenantClasses.AsNoTracking().AnyAsync( + item => item.TenantId == tenantId && item.Id == classId, + cancellationToken)) + throw Error("class_not_found", "The class was not found."); + } + + private static ClassAssignmentGrantItem ToItem(ClassAssignmentGrant item) => new( + item.Id, + item.ClassId, + item.ProductAccessManifestVersionId, + item.ResourceType, + item.ResourceId, + item.StartsAt, + item.EndsAt, + item.Status, + item.CreatedBy, + item.RevokedBy, + item.RevokedReason); + + private static LearningAccessException Error(string code, string message) => new(code, message); +} diff --git a/Tiku.Infrastructure/Learning/Answering/AnsweringService.cs b/Tiku.Infrastructure/Learning/Answering/AnsweringService.cs index 6a3a819..2e6270d 100644 --- a/Tiku.Infrastructure/Learning/Answering/AnsweringService.cs +++ b/Tiku.Infrastructure/Learning/Answering/AnsweringService.cs @@ -1,9 +1,13 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; using Npgsql; using Tiku.Application.Learning; +using Tiku.Application.QuestionBanks; +using Tiku.Application.Security; using Tiku.Domain.Learning; +using Tiku.Infrastructure.Persistence; namespace Tiku.Infrastructure.Learning; @@ -38,13 +42,19 @@ internal sealed class AnsweringService(LearningServiceDependencies dependencies) if (session is null) throw new LearningResourceNotFoundException("practice_session_not_found", "Practice session was not found."); - await learningAccessService.EnsureStrongRevocationVersionAsync( + await strongRevocationService.EnsureVersionAsync( actor, session.StrongRevocationVersion, cancellationToken); - var deliveryVersion = await LoadDeliveryVersionAsync(actor.TenantId, sessionQuestion, cancellationToken); - var revealedSolution = DefersSolutionUntilSubmission(session.Mode) ? null : ToSolutionItem(deliveryVersion); + if (!sessionQuestion.QuestionRevisionId.HasValue) + throw new LearningValidationException( + "practice_session_delivery_unsupported", + "The practice session uses a retired delivery model."); + var deliveryFacts = ReadV2GradingFacts(sessionQuestion); + var revealedSolution = DefersSolutionUntilSubmission(session.Mode) + ? null + : await LoadV2SolutionAsync(sessionQuestion, cancellationToken); var requestHash = HashAnswer(command); - var existingSubmission = await learningPersistence.AnswerSubmissionIdempotencies.AsNoTracking() + var existingSubmission = await learningPersistence.AnswerRecords.AsNoTracking() .SingleOrDefaultAsync( item => item.TenantId == actor.TenantId && @@ -62,7 +72,7 @@ internal sealed class AnsweringService(LearningServiceDependencies dependencies) } IdempotencyReplays.Add(1); - return await ReplayAsync(existingSubmission, revealedSolution, cancellationToken); + return ToItem(existingSubmission, revealedSolution); } if (session.ExpiresAt.HasValue && session.ExpiresAt <= now) @@ -90,10 +100,10 @@ internal sealed class AnsweringService(LearningServiceDependencies dependencies) try { grading = QuestionGrader.Grade(new QuestionGradingInput( - deliveryVersion.QuestionType, - deliveryVersion.CorrectOptionIndex, - deliveryVersion.CorrectOptionIndices, - deliveryVersion.AnswerText, + deliveryFacts.QuestionType, + deliveryFacts.CorrectOptionIndex, + deliveryFacts.CorrectOptionIndices, + deliveryFacts.AnswerText, sessionQuestion.GradingRulesSnapshot, selectedIndices, command.AnswerText, @@ -151,17 +161,6 @@ internal sealed class AnsweringService(LearningServiceDependencies dependencies) current.UpdatedAt = now; } var response = ToItem(record, revealedSolution); - learningPersistence.AnswerSubmissionIdempotencies.Add(new AnswerSubmissionIdempotency - { - TenantId = actor.TenantId, - UserId = actor.UserId, - PracticeSessionId = session.Id, - IdempotencyKey = idempotencyKey, - RequestHash = requestHash, - AnswerRecordId = record.Id, - ResponseSnapshot = JsonSerializer.SerializeToElement(response), - CreatedAt = now - }); try { await unitOfWork.SaveChangesAsync(cancellationToken); @@ -176,7 +175,7 @@ internal sealed class AnsweringService(LearningServiceDependencies dependencies) postgresException.SqlState == PostgresErrorCodes.UniqueViolation) { unitOfWork.ChangeTracker.Clear(); - var replay = await learningPersistence.AnswerSubmissionIdempotencies.AsNoTracking() + var replay = await learningPersistence.AnswerRecords.AsNoTracking() .SingleOrDefaultAsync(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId && @@ -185,7 +184,7 @@ internal sealed class AnsweringService(LearningServiceDependencies dependencies) if (replay is not null && string.Equals(replay.RequestHash, requestHash, StringComparison.Ordinal)) { IdempotencyReplays.Add(1); - return await ReplayAsync(replay, revealedSolution, cancellationToken); + return ToItem(replay, revealedSolution); } AnswerConflicts.Add(1); @@ -196,22 +195,64 @@ internal sealed class AnsweringService(LearningServiceDependencies dependencies) return response; } - private async Task ReplayAsync( - AnswerSubmissionIdempotency submission, - QuestionSolutionItem? revealedSolution, + private async Task LoadV2SolutionAsync( + PracticeSessionQuestion sessionQuestion, CancellationToken cancellationToken) { - if (submission.ResponseSnapshot.ValueKind == JsonValueKind.Object && - (submission.ResponseSnapshot.TryGetProperty(nameof(AnswerRecordItem.Id), out _) || - submission.ResponseSnapshot.TryGetProperty("id", out _))) - return submission.ResponseSnapshot.Deserialize() - ?? throw new InvalidOperationException("The stored answer response is invalid."); - - var historicalRecord = await learningPersistence.AnswerRecords.AsNoTracking().SingleOrDefaultAsync( - item => item.TenantId == submission.TenantId && item.Id == submission.AnswerRecordId, + return await tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + sessionQuestion.QuestionOwnerTenantId, + SystemScopeCallerType.PublicQuestionBank, + nameof(AnsweringService), + "Reveal a locked V2 question solution after an accepted answer", + Guid.NewGuid().ToString("N")), + async (provider, token) => + { + var persistence = provider.GetRequiredService(); + var revision = await persistence.QuestionRevisions.AsNoTracking().SingleOrDefaultAsync(item => + item.TenantId == sessionQuestion.QuestionOwnerTenantId && + item.Id == sessionQuestion.QuestionRevisionId, + token) ?? throw new LearningValidationException( + "practice_question_revision_missing", "The locked question revision is missing."); + return new QuestionSolutionItem( + revision.CorrectOptionIndex, + revision.CorrectOptionIndices, + revision.AnswerText, + revision.Explanation); + }, cancellationToken); - return historicalRecord is null - ? throw new InvalidOperationException("The idempotent answer record no longer exists.") - : ToItem(historicalRecord, revealedSolution); } + + private static AnswerDeliveryFacts ReadV2GradingFacts(PracticeSessionQuestion question) + { + var snapshot = question.GradingRulesSnapshot; + if (snapshot.ValueKind != JsonValueKind.Object || + !snapshot.TryGetProperty("version", out var version) || + version.GetInt32() != 2) + throw new LearningValidationException( + "practice_grading_snapshot_invalid", "The V2 grading snapshot is invalid."); + var correctOptionIndex = snapshot.TryGetProperty("correctOptionIndex", out var single) && + single.ValueKind == JsonValueKind.Number + ? single.GetInt32() + : (int?)null; + var correctOptionIndices = snapshot.TryGetProperty("correctOptionIndices", out var multiple) + ? multiple.Clone() + : JsonDocument.Parse("[]").RootElement.Clone(); + var answerText = snapshot.TryGetProperty("answerText", out var answer) && + answer.ValueKind == JsonValueKind.String + ? answer.GetString() + : null; + return new AnswerDeliveryFacts( + question.QuestionType, + correctOptionIndex, + correctOptionIndices, + answerText); + } + + private sealed record AnswerDeliveryFacts( + string QuestionType, + int? CorrectOptionIndex, + JsonElement CorrectOptionIndices, + string? AnswerText); + } diff --git a/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.Helpers.cs b/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.Helpers.cs index ba8c0e1..325889c 100644 --- a/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.Helpers.cs +++ b/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.Helpers.cs @@ -16,264 +16,62 @@ namespace Tiku.Infrastructure.Learning; internal abstract partial class LearningActivityServiceBase { - protected async Task BuildPracticeAssemblyAsync( - Guid tenantId, - PracticeSessionCommand command, - CancellationToken cancellationToken) - { - var mode = NormalizeMode(command.Mode); - var assembly = new PracticeAssembly( - mode, - command.TargetType, - command.TargetId, - command.BlueprintId, - command.CollectionId, - command.EntryId, - command.ContentNodeId, - Math.Clamp(command.QuestionLimit ?? 100, 1, MaxLimit), - command.DurationMinutes, - command.TotalScore); - - if (!command.BlueprintId.HasValue) return assembly; - - var blueprint = await questionBankPersistence.PracticeBlueprints - .AsNoTracking() - .SingleOrDefaultAsync( - item => - item.TenantId == tenantId && - item.Id == command.BlueprintId.Value && - item.Status == ContentStatus.Active, - cancellationToken); - - if (blueprint is null) - throw new LearningResourceNotFoundException("practice_blueprint_not_found", - "Practice blueprint was not found."); - - return assembly with - { - Mode = NormalizeMode(blueprint.Mode.ToString()), - TargetType = command.TargetType ?? "blueprint", - TargetId = command.TargetId ?? blueprint.Id, - CollectionId = command.CollectionId ?? blueprint.CollectionId, - EntryId = command.EntryId ?? blueprint.EntryId, - ContentNodeId = command.ContentNodeId ?? blueprint.NodeId, - QuestionLimit = Math.Clamp(command.QuestionLimit ?? blueprint.QuestionLimit ?? 100, 1, MaxLimit), - DurationMinutes = command.DurationMinutes ?? blueprint.DurationMinutes, - TotalScore = command.TotalScore ?? blueprint.TotalScore - }; - } - - protected async Task> CollectQuestionReferenceIdsAsync( - LearningActor actor, - PracticeAssembly assembly, - CancellationToken cancellationToken) - { - if (assembly.Mode == "wrong_review") - return await learningPersistence.WrongQuestions - .AsNoTracking() - .Where(item => - item.TenantId == actor.TenantId && - item.UserId == actor.UserId && - item.ResolvedAt == null) - .OrderByDescending(item => item.WrongCount) - .ThenBy(item => item.LastWrongAt) - .Take(assembly.QuestionLimit) - .Select(item => item.QuestionReferenceId) - .ToListAsync(cancellationToken); - - if (assembly.Mode == "favorite_review") - return await learningPersistence.FavoriteQuestions - .AsNoTracking() - .Where(item => - item.TenantId == actor.TenantId && - item.UserId == actor.UserId) - .OrderByDescending(item => item.CreatedAt) - .Take(assembly.QuestionLimit) - .Select(item => item.QuestionReferenceId) - .ToListAsync(cancellationToken); - - if (assembly.CollectionId.HasValue) - return await questionBankPersistence.QuestionCollectionItems - .AsNoTracking() - .Where(item => - item.TenantId == actor.TenantId && - item.CollectionId == assembly.CollectionId.Value) - .OrderBy(item => item.SortOrder) - .Take(assembly.QuestionLimit) - .Select(item => item.QuestionReferenceId) - .ToListAsync(cancellationToken); - - var query = questionBankPersistence.Questions - .AsNoTracking() - .Where(question => - question.TenantId == actor.TenantId && - question.Status == QuestionStatus.Published); - - if (assembly.ContentNodeId.HasValue) - query = query.Where(question => question.ContentNodeId == assembly.ContentNodeId.Value); - else if (assembly.EntryId.HasValue) - query = query.Where(question => question.EntryId == assembly.EntryId.Value); - else if (assembly.TargetId.HasValue && !string.IsNullOrWhiteSpace(assembly.TargetType)) - query = ApplyLegacyTargetFilter(query, assembly.TargetType, assembly.TargetId.Value); - else - throw new LearningValidationException("practice_target_required", "Practice target is required."); - - var questionIds = await query - .OrderBy(question => question.CreatedAt) - .Take(assembly.QuestionLimit) - .Select(question => question.Id) - .ToListAsync(cancellationToken); - var existingReferences = await questionBankPersistence.TenantQuestionReferences - .Where(reference => - reference.TenantId == actor.TenantId && - reference.QuestionOwnerTenantId == actor.TenantId && - questionIds.Contains(reference.QuestionId)) - .ToDictionaryAsync(reference => reference.QuestionId, cancellationToken); - var referenceIds = new List(questionIds.Count); - foreach (var questionId in questionIds) - { - if (!existingReferences.TryGetValue(questionId, out var reference)) - { - reference = new TenantQuestionReference - { - TenantId = actor.TenantId, - QuestionOwnerTenantId = actor.TenantId, - QuestionId = questionId, - Source = QuestionSource.Tenant, - CreatedBy = actor.UserId - }; - questionBankPersistence.TenantQuestionReferences.Add(reference); - } - - referenceIds.Add(reference.Id); - } - - return referenceIds; - } - - protected static IQueryable ApplyLegacyTargetFilter( - IQueryable query, - string? targetType, - Guid targetId) - { - return NormalizeEnumValue(targetType) switch - { - "subject" => query.Where(question => question.SubjectId == targetId), - "category" => query.Where(question => question.CategoryId == targetId), - "node" => query.Where(question => question.NodeId == targetId), - "questionbank" => query.Where(question => question.QuestionBankId == targetId), - "contentnode" => query.Where(question => question.ContentNodeId == targetId), - "entry" => query.Where(question => question.EntryId == targetId), - _ => query.Where(_ => false) - }; - } - - protected async Task> LoadQuestionSelectionsAsync( - Guid tenantId, - IReadOnlyCollection questionReferenceIds, - CancellationToken cancellationToken) - { - var rows = await tenantExecutionScope.ExecuteAsync( - new SystemScopeRequest( - tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(LearningActivityServiceBase), - "Lock published question versions for a new practice session", Guid.NewGuid().ToString("N")), - async (provider, token) => - { - var systemQuestionBank = provider.GetRequiredService(); - var systemLearning = provider.GetRequiredService(); - return await ( - from reference in systemQuestionBank.TenantQuestionReferences.AsNoTracking() - join question in systemQuestionBank.Questions.AsNoTracking() - on new { TenantId = reference.QuestionOwnerTenantId, Id = reference.QuestionId } - equals new { question.TenantId, question.Id } - join version in systemQuestionBank.QuestionVersions.AsNoTracking() - on new - { - TenantId = reference.QuestionOwnerTenantId, - reference.QuestionId, - Id = question.CurrentVersionId - } - equals new - { - version.TenantId, - version.QuestionId, - Id = (Guid?)version.Id - } - where reference.TenantId == tenantId && - questionReferenceIds.Contains(reference.Id) && - question.Status == QuestionStatus.Published - select new QuestionSelection( - reference.Id, - reference.QuestionOwnerTenantId, - reference.QuestionId, - version.Id, - version.QuestionType, - version.TypeLabel, - version.Difficulty, - version.Tags, - version.Content, - version.Options, - version.CorrectOptionIndex, - version.CorrectOptionIndices, - version.AnswerText, - version.Explanation)) - .ToArrayAsync(token); - }, - cancellationToken); - - var byReference = rows.ToDictionary(row => row.QuestionReferenceId); - if (byReference.Count != questionReferenceIds.Distinct().Count()) - throw new LearningValidationException( - "practice_question_unavailable", - "One or more practice questions have no published version."); - - return questionReferenceIds.Select(referenceId => byReference[referenceId]).ToArray(); - } - - protected Task LoadSessionQuestionItemsAsync( + protected async Task LoadSessionQuestionItemsAsync( Guid tenantId, Guid practiceSessionId, CancellationToken cancellationToken) { - return tenantExecutionScope.ExecuteAsync( - new SystemScopeRequest( - tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(LearningActivityServiceBase), - "Read locked question versions for a tenant practice session", Guid.NewGuid().ToString("N")), - async (provider, token) => - { - var systemQuestionBank = provider.GetRequiredService(); - var systemLearning = provider.GetRequiredService(); - return await ( - from sessionQuestion in systemLearning.PracticeSessionQuestions.AsNoTracking() - join version in systemQuestionBank.QuestionVersions.AsNoTracking() - on new - { - TenantId = sessionQuestion.QuestionOwnerTenantId, - Id = sessionQuestion.QuestionVersionId - } - equals new { version.TenantId, version.Id } - where sessionQuestion.TenantId == tenantId && - sessionQuestion.PracticeSessionId == practiceSessionId - orderby sessionQuestion.Position - select new PracticeSessionQuestionItem( - sessionQuestion.Id, - sessionQuestion.QuestionReferenceId, - new QuestionLocator( - sessionQuestion.QuestionOwnerTenantId == tenantId - ? QuestionSource.Tenant - : QuestionSource.Platform, - sessionQuestion.QuestionId), - sessionQuestion.QuestionId, - version.QuestionType, - version.TypeLabel, - version.Difficulty, - version.Tags, - sessionQuestion.QuestionVersionId, - version.Content, - version.Options)) - .ToArrayAsync(token); - }, - cancellationToken); + var sessionQuestions = await learningPersistence.PracticeSessionQuestions.AsNoTracking() + .Where(item => item.TenantId == tenantId && item.PracticeSessionId == practiceSessionId) + .OrderBy(item => item.Position) + .ToArrayAsync(cancellationToken); + if (sessionQuestions.Any(item => !item.QuestionRevisionId.HasValue)) + throw new LearningValidationException( + "practice_session_delivery_unsupported", + "The practice session uses a retired delivery model."); + + var revisions = new List(sessionQuestions.Length); + foreach (var ownerGroup in sessionQuestions.GroupBy(item => item.QuestionOwnerTenantId)) + { + var revisionIds = ownerGroup.Select(item => item.QuestionRevisionId!.Value).Distinct().ToArray(); + revisions.AddRange(await tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + ownerGroup.Key, + SystemScopeCallerType.PublicQuestionBank, + nameof(LearningActivityServiceBase), + "Read explicitly locked V2 prompts for a practice session", + Guid.NewGuid().ToString("N")), + async (provider, token) => + { + var persistence = provider.GetRequiredService(); + return await persistence.QuestionRevisions.AsNoTracking() + .Where(item => item.TenantId == ownerGroup.Key && revisionIds.Contains(item.Id)) + .ToArrayAsync(token); + }, + cancellationToken)); + } + + return sessionQuestions.Select(item => + { + var revision = revisions.Single(version => + version.TenantId == item.QuestionOwnerTenantId && version.Id == item.QuestionRevisionId); + return new PracticeSessionQuestionItem( + item.Id, + null, + null, + null, + item.QuestionAssetId, + revision.Id, + item.QuestionPlacementId, + item.AssessmentPolicyVersionId, + revision.QuestionType, + revision.TypeLabel, + item.DifficultySnapshot, + item.TagsSnapshot, + revision.Id, + revision.Content, + revision.Options); + }).ToArray(); } protected async Task GetPracticeSessionAsync( @@ -299,6 +97,48 @@ internal abstract partial class LearningActivityServiceBase return session; } + protected async Task> LoadV2ReportSolutionsAsync( + IReadOnlyCollection sessionQuestions, + CancellationToken cancellationToken) + { + if (sessionQuestions.Count == 0) return []; + var revisions = new List(); + foreach (var ownerGroup in sessionQuestions.GroupBy(item => item.QuestionOwnerTenantId)) + { + var ownerTenantId = ownerGroup.Key; + var revisionIds = ownerGroup.Select(item => item.QuestionRevisionId!.Value).Distinct().ToArray(); + revisions.AddRange(await tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + ownerTenantId, + SystemScopeCallerType.PublicQuestionBank, + nameof(LearningActivityServiceBase), + "Read explicitly locked V2 solutions for final reporting", + Guid.NewGuid().ToString("N")), + async (provider, token) => + { + var persistence = provider.GetRequiredService(); + return await persistence.QuestionRevisions.AsNoTracking() + .Where(item => item.TenantId == ownerTenantId && revisionIds.Contains(item.Id)) + .ToArrayAsync(token); + }, + cancellationToken)); + } + + return sessionQuestions.ToDictionary( + item => item.Id, + item => + { + var revision = revisions.Single(version => + version.TenantId == item.QuestionOwnerTenantId && + version.Id == item.QuestionRevisionId); + return new QuestionSolutionItem( + revision.CorrectOptionIndex, + revision.CorrectOptionIndices, + revision.AnswerText, + revision.Explanation); + }); + } + protected async Task BuildPracticeSessionReportAsync( LearningActor actor, PracticeSession session, @@ -313,8 +153,10 @@ internal abstract partial class LearningActivityServiceBase if (sessionQuestions.Length == 0) throw new LearningValidationException("practice_session_empty", "Practice session has no question snapshot."); - var deliveryVersions = await LoadDeliveryVersionsAsync( - actor.TenantId, sessionQuestions, cancellationToken); + if (sessionQuestions.Any(item => !item.QuestionRevisionId.HasValue)) + throw new LearningValidationException( + "practice_session_delivery_unsupported", + "The practice session uses a retired delivery model."); var answers = await learningPersistence.AnswerRecords .AsNoTracking() @@ -351,18 +193,28 @@ internal abstract partial class LearningActivityServiceBase .Where(question => latestAnswers.TryGetValue(question.Id, out var answer) && answer.GradingStatus == AnswerGradingStatus.Incorrect) - .Select(question => question.QuestionReferenceId) + .Select(question => question.QuestionReferenceId ?? question.QuestionAssetId) + .Where(id => id.HasValue) + .Select(id => id!.Value) .ToArray(); + var v2Solutions = isFinal + ? await LoadV2ReportSolutionsAsync( + sessionQuestions.Where(item => item.QuestionRevisionId.HasValue).ToArray(), cancellationToken) + : new Dictionary(); var questionResults = sessionQuestions .Select(question => { latestAnswers.TryGetValue(question.Id, out var answer); - var deliveryVersion = deliveryVersions[question.QuestionVersionId]; + v2Solutions.TryGetValue(question.Id, out var v2Solution); return new { sessionQuestionId = question.Id, questionReferenceId = question.QuestionReferenceId, questionId = question.QuestionId, + questionAssetId = question.QuestionAssetId, + questionRevisionId = question.QuestionRevisionId, + questionPlacementId = question.QuestionPlacementId, + assessmentPolicyVersionId = question.AssessmentPolicyVersionId, source = question.QuestionOwnerTenantId == actor.TenantId ? "tenant" : "platform", answered = answer is not null, gradingStatus = answer?.GradingStatus.ToString(), @@ -370,10 +222,12 @@ internal abstract partial class LearningActivityServiceBase score = answer?.AwardedScore, totalScore = question.Score ?? 1, answeredAt = answer?.AnsweredAt, - correctOptionIndex = isFinal ? deliveryVersion.CorrectOptionIndex : null, - correctOptionIndices = isFinal ? deliveryVersion.CorrectOptionIndices : JsonDefaults.Array(), - answerText = isFinal ? deliveryVersion.AnswerText : null, - explanation = isFinal ? deliveryVersion.Explanation : null + correctOptionIndex = isFinal ? v2Solution?.CorrectOptionIndex : null, + correctOptionIndices = isFinal + ? v2Solution?.CorrectOptionIndices ?? JsonDefaults.Array() + : JsonDefaults.Array(), + answerText = isFinal ? v2Solution?.AnswerText : null, + explanation = isFinal ? v2Solution?.Explanation : null }; }) .ToArray(); @@ -465,21 +319,6 @@ internal abstract partial class LearningActivityServiceBase return report; } - protected async Task EnsureQuestionExistsAsync( - Guid tenantId, - Guid questionId, - CancellationToken cancellationToken) - { - var exists = await questionBankPersistence.Questions.AnyAsync( - question => - question.TenantId == tenantId && - question.Id == questionId && - question.Status == QuestionStatus.Published, - cancellationToken); - - if (!exists) throw new LearningResourceNotFoundException("question_not_found", "Question was not found."); - } - protected async Task EnsureWordExistsAsync( Guid tenantId, Guid wordId, @@ -510,55 +349,6 @@ internal abstract partial class LearningActivityServiceBase solution); } - protected async Task LoadDeliveryVersionAsync( - Guid tenantId, - PracticeSessionQuestion sessionQuestion, - CancellationToken cancellationToken) - { - return await tenantExecutionScope.ExecuteAsync( - new SystemScopeRequest( - tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(LearningActivityServiceBase), - "Read immutable question delivery version", Guid.NewGuid().ToString("N")), - async (provider, token) => - { - var questions = provider.GetRequiredService(); - return await questions.QuestionVersions.AsNoTracking().SingleAsync(version => - version.TenantId == sessionQuestion.QuestionOwnerTenantId && - version.QuestionId == sessionQuestion.QuestionId && - version.Id == sessionQuestion.QuestionVersionId, token); - }, - cancellationToken); - } - - protected async Task> LoadDeliveryVersionsAsync( - Guid tenantId, - IReadOnlyCollection sessionQuestions, - CancellationToken cancellationToken) - { - var versionIds = sessionQuestions.Select(item => item.QuestionVersionId).Distinct().ToArray(); - return await tenantExecutionScope.ExecuteAsync( - new SystemScopeRequest( - tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(LearningActivityServiceBase), - "Read immutable question delivery versions", Guid.NewGuid().ToString("N")), - async (provider, token) => - { - var questions = provider.GetRequiredService(); - return await questions.QuestionVersions.AsNoTracking() - .Where(version => versionIds.Contains(version.Id)) - .ToDictionaryAsync(version => version.Id, token); - }, - cancellationToken); - } - - protected static QuestionSolutionItem ToSolutionItem(QuestionVersion version) - { - return new QuestionSolutionItem( - version.CorrectOptionIndex, - version.CorrectOptionIndices, - version.AnswerText, - version.Explanation); - } - protected static bool DefersSolutionUntilSubmission(string mode) { var normalized = NormalizeEnumValue(mode); @@ -664,17 +454,6 @@ internal abstract partial class LearningActivityServiceBase "Only an active practice session accepts answers."); } - protected static JsonElement BuildGradingRules(QuestionSelection selection) - { - return JsonSerializer.SerializeToElement(new - { - version = 1, - normalization = selection.QuestionType.Equals("fill_blank", StringComparison.OrdinalIgnoreCase) - ? "nfkc_trim_casefold_whitespace" - : "exact" - }); - } - protected static string HashAnswer(SubmitAnswerCommand command) { return Hash(JsonSerializer.Serialize(new @@ -690,8 +469,7 @@ internal abstract partial class LearningActivityServiceBase { return Hash(JsonSerializer.Serialize(new { - command.PracticeSessionId, - command.ExpectedSessionVersion + command.PracticeSessionId })); } @@ -743,31 +521,4 @@ internal abstract partial class LearningActivityServiceBase .Replace("-", string.Empty, StringComparison.Ordinal); } - protected sealed record PracticeAssembly( - string Mode, - string? TargetType, - Guid? TargetId, - Guid? BlueprintId, - Guid? CollectionId, - Guid? EntryId, - Guid? ContentNodeId, - int QuestionLimit, - int? DurationMinutes, - decimal? TotalScore); - - protected sealed record QuestionSelection( - Guid QuestionReferenceId, - Guid QuestionOwnerTenantId, - Guid QuestionId, - Guid QuestionVersionId, - string QuestionType, - string? TypeLabel, - int? Difficulty, - JsonElement Tags, - string? Content, - JsonElement Options, - int? CorrectOptionIndex, - JsonElement CorrectOptionIndices, - string? AnswerText, - string? Explanation); } diff --git a/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.cs b/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.cs index e479ec9..c57db14 100644 --- a/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.cs +++ b/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.cs @@ -1,6 +1,7 @@ using System.Diagnostics.Metrics; using Microsoft.Extensions.Logging; using Tiku.Application.Learning; +using Tiku.Application.Content; using Tiku.Application.QuestionBanks; using Tiku.Application.Security; using Tiku.Infrastructure.Persistence; @@ -9,18 +10,23 @@ namespace Tiku.Infrastructure.Learning; internal sealed record LearningServiceDependencies( ILearningPersistence LearningPersistence, + ILearningAccessPersistence LearningAccessPersistence, IQuestionBankPersistence QuestionBankPersistence, IContentAssetPersistence ContentAssetPersistence, IIdentityPersistence IdentityPersistence, IQuestionReferenceService QuestionReferenceService, IPublicQuestionAccessPolicy PublicQuestionAccessPolicy, ILearningAccessService LearningAccessService, + IEffectiveLearningAccessService EffectiveLearningAccessService, + ILearningStrongRevocationService StrongRevocationService, + IContentCandidateReader ContentCandidateReader, ITenantExecutionScope TenantExecutionScope, ILogger Logger); internal abstract partial class LearningActivityServiceBase(LearningServiceDependencies dependencies) { protected ILearningPersistence learningPersistence { get; } = dependencies.LearningPersistence; + protected ILearningAccessPersistence learningAccessPersistence { get; } = dependencies.LearningAccessPersistence; protected IQuestionBankPersistence questionBankPersistence { get; } = dependencies.QuestionBankPersistence; protected IContentAssetPersistence contentAssetPersistence { get; } = dependencies.ContentAssetPersistence; protected IIdentityPersistence identityPersistence { get; } = dependencies.IdentityPersistence; @@ -28,6 +34,10 @@ internal abstract partial class LearningActivityServiceBase(LearningServiceDepen protected IQuestionReferenceService questionReferenceService { get; } = dependencies.QuestionReferenceService; protected IPublicQuestionAccessPolicy publicQuestionAccessPolicy { get; } = dependencies.PublicQuestionAccessPolicy; protected ILearningAccessService learningAccessService { get; } = dependencies.LearningAccessService; + protected IEffectiveLearningAccessService effectiveLearningAccessService { get; } = + dependencies.EffectiveLearningAccessService; + protected IContentCandidateReader contentCandidateReader { get; } = dependencies.ContentCandidateReader; + protected ILearningStrongRevocationService strongRevocationService { get; } = dependencies.StrongRevocationService; protected ITenantExecutionScope tenantExecutionScope { get; } = dependencies.TenantExecutionScope; protected ILogger logger { get; } = dependencies.Logger; diff --git a/Tiku.Infrastructure/Learning/Outbox/LearningOutboxProcessor.cs b/Tiku.Infrastructure/Learning/Outbox/LearningOutboxProcessor.cs index 2455048..c3524fa 100644 --- a/Tiku.Infrastructure/Learning/Outbox/LearningOutboxProcessor.cs +++ b/Tiku.Infrastructure/Learning/Outbox/LearningOutboxProcessor.cs @@ -1,7 +1,9 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Tiku.Application.Content; using Tiku.Application.Learning; +using Tiku.Domain.Content; using Tiku.Domain.Learning; using Tiku.Infrastructure.Persistence; @@ -11,10 +13,13 @@ internal static class LearningOutboxEventTypes { internal const string PracticeSessionSubmitted = "practice_session_submitted"; internal const string WrongQuestionProjection = "wrong_questions_v1"; + internal const string ContentReleasePublished = "content_release_published"; + internal const string LocalContentReleaseProjection = "local_content_release_v1"; } internal sealed class LearningOutboxProcessor( ILearningPersistence learningPersistence, + IQuestionBankPersistence contentPersistence, ILogger logger) : ILearningOutboxProcessor { private const int MaxAttempts = 10; @@ -85,10 +90,11 @@ internal sealed class LearningOutboxProcessor( return false; } + var projectionName = ResolveProjectionName(message); var alreadyProjected = await learningPersistence.LearningProjectionReceipts.AnyAsync( item => item.TenantId == message.TenantId && item.OutboxMessageId == message.Id && - item.ProjectionName == LearningOutboxEventTypes.WrongQuestionProjection, + item.ProjectionName == projectionName, cancellationToken); if (!alreadyProjected) { @@ -97,7 +103,7 @@ internal sealed class LearningOutboxProcessor( { TenantId = message.TenantId, OutboxMessageId = message.Id, - ProjectionName = LearningOutboxEventTypes.WrongQuestionProjection, + ProjectionName = projectionName, ProcessedAt = DateTimeOffset.UtcNow }); } @@ -127,10 +133,30 @@ internal sealed class LearningOutboxProcessor( private async Task DispatchAsync(LearningOutboxMessage message, CancellationToken cancellationToken) { - if (!string.Equals(message.EventType, LearningOutboxEventTypes.PracticeSessionSubmitted, - StringComparison.Ordinal) || message.SchemaVersion != 1) - throw new InvalidOperationException( - $"Unsupported learning outbox event '{message.EventType}' schema {message.SchemaVersion}."); + if (message.SchemaVersion != 1) + throw UnsupportedEvent(message); + + if (string.Equals(message.EventType, LearningOutboxEventTypes.PracticeSessionSubmitted, + StringComparison.Ordinal)) + { + await ProjectWrongQuestionsAsync(message, cancellationToken); + return; + } + + if (string.Equals(message.EventType, LearningOutboxEventTypes.ContentReleasePublished, + StringComparison.Ordinal)) + { + await VerifyLocalContentReleaseAsync(message, cancellationToken); + return; + } + + throw UnsupportedEvent(message); + } + + private async Task ProjectWrongQuestionsAsync( + LearningOutboxMessage message, + CancellationToken cancellationToken) + { var payload = message.Payload.Deserialize(JsonSerializerOptions.Web) ?? throw new InvalidOperationException("The practice submission outbox payload is invalid."); @@ -150,33 +176,123 @@ internal sealed class LearningOutboxProcessor( var questions = await learningPersistence.PracticeSessionQuestions.AsNoTracking() .Where(item => item.TenantId == message.TenantId && item.PracticeSessionId == payload.PracticeSessionId && - wrongQuestionIds.Contains(item.QuestionReferenceId)) + ((item.QuestionReferenceId.HasValue && + wrongQuestionIds.Contains(item.QuestionReferenceId.Value)) || + (item.QuestionAssetId.HasValue && + wrongQuestionIds.Contains(item.QuestionAssetId.Value)))) .Select(item => new { item.QuestionReferenceId, item.QuestionOwnerTenantId, - item.QuestionId + item.QuestionId, + item.QuestionAssetId, + item.QuestionRevisionId, + item.QuestionPlacementId }) .ToArrayAsync(cancellationToken); - if (questions.Length != wrongQuestionIds.Distinct().Count()) + var projectedKeys = questions.Select(item => item.QuestionReferenceId ?? item.QuestionAssetId) + .Where(item => item.HasValue).Select(item => item!.Value).Distinct().ToArray(); + if (projectedKeys.Length != wrongQuestionIds.Distinct().Count()) throw new InvalidOperationException("One or more wrong-question references could not be projected."); - var referenceIds = questions.Select(item => item.QuestionReferenceId).ToArray(); - var ownerTenantIds = questions.Select(item => item.QuestionOwnerTenantId).ToArray(); - var questionIds = questions.Select(item => item.QuestionId).ToArray(); - await learningPersistence.Database.ExecuteSqlInterpolatedAsync($""" - INSERT INTO wrong_questions - (tenant_id, user_id, question_reference_id, question_owner_tenant_id, question_id, - wrong_count, last_wrong_at, resolved_at) - SELECT {message.TenantId}, {payload.UserId}, source.question_reference_id, - source.question_owner_tenant_id, source.question_id, 1, {report.SubmittedAt}, NULL - FROM unnest({referenceIds}, {ownerTenantIds}, {questionIds}) - AS source(question_reference_id, question_owner_tenant_id, question_id) - ON CONFLICT (tenant_id, user_id, question_reference_id) DO UPDATE SET - wrong_count = wrong_questions.wrong_count + 1, - last_wrong_at = GREATEST(wrong_questions.last_wrong_at, EXCLUDED.last_wrong_at), - resolved_at = NULL - """, cancellationToken); + var v1 = questions.Where(item => item.QuestionReferenceId.HasValue && item.QuestionId.HasValue) + .DistinctBy(item => item.QuestionReferenceId).ToArray(); + if (v1.Length > 0) + { + var referenceIds = v1.Select(item => item.QuestionReferenceId!.Value).ToArray(); + var ownerTenantIds = v1.Select(item => item.QuestionOwnerTenantId).ToArray(); + var questionIds = v1.Select(item => item.QuestionId!.Value).ToArray(); + await learningPersistence.Database.ExecuteSqlInterpolatedAsync($""" + INSERT INTO wrong_questions + (tenant_id, user_id, question_reference_id, question_owner_tenant_id, question_id, + wrong_count, last_wrong_at, resolved_at) + SELECT {message.TenantId}, {payload.UserId}, source.question_reference_id, + source.question_owner_tenant_id, source.question_id, 1, {report.SubmittedAt}, NULL + FROM unnest({referenceIds}, {ownerTenantIds}, {questionIds}) + AS source(question_reference_id, question_owner_tenant_id, question_id) + ON CONFLICT (tenant_id, user_id, question_reference_id) DO UPDATE SET + wrong_count = wrong_questions.wrong_count + 1, + last_wrong_at = GREATEST(wrong_questions.last_wrong_at, EXCLUDED.last_wrong_at), + resolved_at = NULL + """, cancellationToken); + } + + var v2 = questions.Where(item => item.QuestionAssetId.HasValue && + item.QuestionRevisionId.HasValue && + item.QuestionPlacementId.HasValue) + .DistinctBy(item => new { item.QuestionOwnerTenantId, item.QuestionAssetId }).ToArray(); + if (v2.Length > 0) + { + var ownerTenantIds = v2.Select(item => item.QuestionOwnerTenantId).ToArray(); + var assetIds = v2.Select(item => item.QuestionAssetId!.Value).ToArray(); + var revisionIds = v2.Select(item => item.QuestionRevisionId!.Value).ToArray(); + var placementIds = v2.Select(item => item.QuestionPlacementId!.Value).ToArray(); + await learningPersistence.Database.ExecuteSqlInterpolatedAsync($""" + INSERT INTO wrong_questions_v2 + (tenant_id, user_id, question_asset_owner_tenant_id, question_asset_id, + last_question_revision_id, last_question_placement_id, wrong_count, last_wrong_at, resolved_at) + SELECT {message.TenantId}, {payload.UserId}, source.owner_tenant_id, source.asset_id, + source.revision_id, source.placement_id, 1, {report.SubmittedAt}, NULL + FROM unnest({ownerTenantIds}, {assetIds}, {revisionIds}, {placementIds}) + AS source(owner_tenant_id, asset_id, revision_id, placement_id) + ON CONFLICT (tenant_id, user_id, question_asset_owner_tenant_id, question_asset_id) DO UPDATE SET + last_question_revision_id = EXCLUDED.last_question_revision_id, + last_question_placement_id = EXCLUDED.last_question_placement_id, + wrong_count = wrong_questions_v2.wrong_count + 1, + last_wrong_at = GREATEST(wrong_questions_v2.last_wrong_at, EXCLUDED.last_wrong_at), + resolved_at = NULL + """, cancellationToken); + } + } + + private async Task VerifyLocalContentReleaseAsync( + LearningOutboxMessage message, + CancellationToken cancellationToken) + { + var payload = message.Payload.Deserialize(JsonSerializerOptions.Web) + ?? throw new InvalidOperationException("The content release outbox payload is invalid."); + if (payload.ReleaseId != message.AggregateId) + throw new InvalidOperationException("The content release outbox aggregate does not match its payload."); + + var release = await contentPersistence.ContentReleases.AsNoTracking().SingleOrDefaultAsync( + item => item.TenantId == message.TenantId && + item.Id == payload.ReleaseId && + item.CurriculumVersionId == payload.CurriculumVersionId && + item.BusinessLineId == payload.BusinessLineId && + item.ReleaseNo == payload.ReleaseNo && + item.SourceFingerprint == payload.SourceFingerprint && + item.Status == ContentReleaseStatus.Published, + cancellationToken) ?? throw new InvalidOperationException( + "The published content release is missing or differs from its outbox payload."); + + var candidateCount = await contentPersistence.ContentReleaseQuestions.AsNoTracking().CountAsync( + item => item.TenantId == message.TenantId && + item.ContentReleaseId == release.Id && + item.Status == ContentReleaseQuestionStatus.Active, + cancellationToken); + if (candidateCount == 0) + throw new InvalidOperationException("The published content release contains no active candidates."); + } + + private static string ResolveProjectionName(LearningOutboxMessage message) + { + if (message.SchemaVersion != 1) + throw UnsupportedEvent(message); + + return message.EventType switch + { + LearningOutboxEventTypes.PracticeSessionSubmitted => + LearningOutboxEventTypes.WrongQuestionProjection, + LearningOutboxEventTypes.ContentReleasePublished => + LearningOutboxEventTypes.LocalContentReleaseProjection, + _ => throw UnsupportedEvent(message) + }; + } + + private static InvalidOperationException UnsupportedEvent(LearningOutboxMessage message) + { + return new InvalidOperationException( + $"Unsupported learning outbox event '{message.EventType}' schema {message.SchemaVersion}."); } private async Task ReleaseAfterFailureAsync( @@ -204,4 +320,11 @@ internal sealed class LearningOutboxProcessor( } private sealed record PracticeSessionSubmittedPayload(Guid ReportId, Guid PracticeSessionId, Guid UserId); + + private sealed record ContentReleasePublishedPayload( + Guid ReleaseId, + Guid CurriculumVersionId, + Guid BusinessLineId, + int ReleaseNo, + string SourceFingerprint); } diff --git a/Tiku.Infrastructure/Learning/PracticeSessions/PracticeSessionService.cs b/Tiku.Infrastructure/Learning/PracticeSessions/PracticeSessionService.cs index 1e97d27..78048b9 100644 --- a/Tiku.Infrastructure/Learning/PracticeSessions/PracticeSessionService.cs +++ b/Tiku.Infrastructure/Learning/PracticeSessions/PracticeSessionService.cs @@ -2,13 +2,13 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore; using Npgsql; using Tiku.Application.Learning; -using Tiku.Domain.Common; -using Tiku.Domain.Content; using Tiku.Domain.Learning; namespace Tiku.Infrastructure.Learning; -internal sealed class PracticeSessionService(LearningServiceDependencies dependencies) +internal sealed class PracticeSessionService( + LearningServiceDependencies dependencies, + IV2PracticeSessionService v2PracticeSessionService) : LearningActivityServiceBase(dependencies), IPracticeSessionService { public async Task CreatePracticeSessionAsync( @@ -16,126 +16,7 @@ internal sealed class PracticeSessionService(LearningServiceDependencies depende PracticeSessionCommand command, CancellationToken cancellationToken = default) { - var assembly = await BuildPracticeAssemblyAsync(actor.TenantId, command, cancellationToken); - var access = await ResolvePracticeAccessAsync(actor, assembly, cancellationToken); - var questionReferenceIds = await CollectQuestionReferenceIdsAsync(actor, assembly, cancellationToken); - if (questionReferenceIds.Count == 0) - throw new LearningValidationException("no_practice_questions", - "No published questions are available for this practice target."); - - - var containsPlatformQuestion = await questionBankPersistence.TenantQuestionReferences.AsNoTracking().AnyAsync( - reference => - reference.TenantId == actor.TenantId && - questionReferenceIds.Contains(reference.Id) && - reference.Source == QuestionSource.Platform, - cancellationToken); - if (containsPlatformQuestion) - await publicQuestionAccessPolicy.EnsureCanStartAsync(actor.TenantId, cancellationToken); - - var now = DateTimeOffset.UtcNow; - var sessionExpiry = assembly.DurationMinutes.HasValue - ? now.AddMinutes(Math.Min(240, assembly.DurationMinutes.Value + 15)) - : now.AddHours(2); - var session = new PracticeSession - { - TenantId = actor.TenantId, - UserId = actor.UserId, - Mode = assembly.Mode, - TargetType = assembly.TargetType, - TargetId = assembly.TargetId, - BlueprintId = assembly.BlueprintId, - CollectionId = assembly.CollectionId, - EntryId = assembly.EntryId, - ContentNodeId = assembly.ContentNodeId, - QuestionCount = questionReferenceIds.Count, - DurationMinutes = assembly.DurationMinutes, - TotalScore = assembly.TotalScore, - ExpiresAt = sessionExpiry, - AccessMode = access.AccessMode, - AccessEntitlementId = access.EntitlementId, - AccessClassAssignmentId = access.ClassAssignmentId, - ConsumedFreeQuota = 0, - AccessGrantVersion = access.Snapshot.GrantVersion, - StrongRevocationVersion = access.Snapshot.StrongRevocationVersion, - AuthorizationExpiresAt = sessionExpiry, - AccessSnapshot = JsonSerializer.SerializeToElement(new - { - strategy = "compiled_access_v1", - contentSliceId = access.ContentSliceId, - businessLineId = access.Snapshot.BusinessLineId, - regionAccessStrategy = access.Snapshot.RegionAccessStrategy, - targetRegionId = access.Snapshot.TargetRegionId, - grantVersion = access.Snapshot.GrantVersion, - contentVersion = access.Snapshot.ContentVersion, - strongRevocationVersion = access.Snapshot.StrongRevocationVersion, - requestedCount = assembly.QuestionLimit, - grantedCount = questionReferenceIds.Count - }), - Metadata = command.Metadata.ValueKind is JsonValueKind.Undefined - ? JsonDefaults.Object() - : command.Metadata - }; - learningPersistence.PracticeSessions.Add(session); - - 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; - learningPersistence.PracticeSessionQuestions.AddRange(selections.Select((selection, index) => - new PracticeSessionQuestion - { - TenantId = actor.TenantId, - PracticeSessionId = session.Id, - QuestionReferenceId = selection.QuestionReferenceId, - QuestionOwnerTenantId = selection.QuestionOwnerTenantId, - QuestionId = selection.QuestionId, - QuestionVersionId = selection.QuestionVersionId, - Position = index, - Score = scorePerQuestion, - QuestionType = selection.QuestionType, - TypeLabelSnapshot = selection.TypeLabel, - DifficultySnapshot = selection.Difficulty, - TagsSnapshot = selection.Tags, - GradingRulesSnapshot = BuildGradingRules(selection), - SnapshotVersion = 1 - })); - learningPersistence.PracticeAccessEvents.Add(new PracticeAccessEvent - { - TenantId = actor.TenantId, - UserId = actor.UserId, - PracticeSessionId = session.Id, - EventType = PracticeAccessEventType.SessionCreated, - AccessMode = access.AccessMode switch - { - PracticeAccessMode.Package => PracticeAccessEventMode.Package, - PracticeAccessMode.ClassAssignment => PracticeAccessEventMode.ClassAssignment, - PracticeAccessMode.Staff => PracticeAccessEventMode.Staff, - PracticeAccessMode.Svip => PracticeAccessEventMode.Svip, - _ => PracticeAccessEventMode.Free - }, - RequestedCount = assembly.QuestionLimit, - GrantedCount = selections.Count, - ConsumedFreeQuota = 0, - EntitlementId = access.EntitlementId, - Metadata = session.AccessSnapshot - }); - - await unitOfWork.SaveChangesAsync(cancellationToken); - return ToItem(session); + return await v2PracticeSessionService.CreateAsync(actor, command, cancellationToken); } public async Task GetPracticeSessionDetailAsync( @@ -144,7 +25,7 @@ internal sealed class PracticeSessionService(LearningServiceDependencies depende CancellationToken cancellationToken = default) { var session = await GetPracticeSessionAsync(actor, filter.PracticeSessionId, cancellationToken); - await learningAccessService.EnsureStrongRevocationVersionAsync( + await strongRevocationService.EnsureVersionAsync( actor, session.StrongRevocationVersion, cancellationToken); var orderedQuestions = await LoadSessionQuestionItemsAsync( actor.TenantId, @@ -190,10 +71,12 @@ internal sealed class PracticeSessionService(LearningServiceDependencies depende item.PracticeSessionId == sessionId && revealQuestionIds.Contains(item.Id)) .ToArrayAsync(cancellationToken); - var versions = await LoadDeliveryVersionsAsync(tenantId, sessionQuestions, cancellationToken); - return sessionQuestions.ToDictionary( - item => item.Id, - item => ToSolutionItem(versions[item.QuestionVersionId])); + if (sessionQuestions.Any(item => !item.QuestionRevisionId.HasValue)) + throw new LearningValidationException( + "practice_session_delivery_unsupported", + "The practice session uses a retired delivery model."); + + return await v2PracticeSessionService.LoadSolutionsAsync(sessionQuestions, cancellationToken); } public async Task SubmitPracticeSessionAsync( @@ -205,8 +88,17 @@ internal sealed class PracticeSessionService(LearningServiceDependencies depende throw new LearningValidationException("idempotency_key_required", "An idempotency key is required."); await using var transaction = await unitOfWork.Database.BeginTransactionAsync(cancellationToken); - var session = await GetPracticeSessionAsync(actor, command.PracticeSessionId, cancellationToken); - await learningAccessService.EnsureStrongRevocationVersionAsync( + var submissionLockKey = $"practice-submit:{actor.TenantId:N}:{actor.UserId:N}:{command.PracticeSessionId:N}"; + await unitOfWork.Database.ExecuteSqlInterpolatedAsync( + $"SELECT pg_advisory_xact_lock(hashtextextended({submissionLockKey}, 0))", + cancellationToken); + var session = await learningPersistence.PracticeSessions.SingleOrDefaultAsync(item => + item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.Id == command.PracticeSessionId, + cancellationToken) ?? throw new LearningResourceNotFoundException( + "practice_session_not_found", "Practice session was not found."); + await strongRevocationService.EnsureVersionAsync( actor, session.StrongRevocationVersion, cancellationToken); var requestHash = HashSubmission(command); var existingOperation = await learningPersistence.LearningOperationIdempotencies.AsNoTracking().SingleOrDefaultAsync( @@ -242,10 +134,6 @@ internal sealed class PracticeSessionService(LearningServiceDependencies depende 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); @@ -285,43 +173,4 @@ internal sealed class PracticeSessionService(LearningServiceDependencies depende return response; } - private async Task ResolvePracticeAccessAsync( - LearningActor actor, - PracticeAssembly assembly, - CancellationToken cancellationToken) - { - if (assembly.BlueprintId.HasValue) - return await learningAccessService.EnsureResourceAccessAsync( - actor, LearningContentResourceType.Blueprint, assembly.BlueprintId.Value, cancellationToken); - if (assembly.CollectionId.HasValue) - return await learningAccessService.EnsureResourceAccessAsync( - actor, LearningContentResourceType.Collection, assembly.CollectionId.Value, cancellationToken); - if (assembly.ContentNodeId.HasValue) - return await learningAccessService.EnsureResourceAccessAsync( - actor, LearningContentResourceType.ContentNode, assembly.ContentNodeId.Value, cancellationToken); - if (assembly.EntryId.HasValue) - return await learningAccessService.EnsureResourceAccessAsync( - actor, LearningContentResourceType.ContentEntry, assembly.EntryId.Value, cancellationToken); - if (assembly.TargetId.HasValue && - string.Equals(assembly.TargetType?.Replace("_", string.Empty), "questionbank", - StringComparison.OrdinalIgnoreCase)) - return await learningAccessService.EnsureResourceAccessAsync( - actor, LearningContentResourceType.QuestionBank, assembly.TargetId.Value, cancellationToken); - - var snapshot = await learningAccessService.GetSnapshotAsync(actor, cancellationToken); - if (snapshot.ContentSliceIds.Count == 0) - throw new LearningAccessException( - "learning_content_not_entitled", - "The current student has no active learning content grant."); - return new LearningResourceAccessDecision( - snapshot.ContentSliceIds.First(), - snapshot.ClassAssignmentIds.Count > 0 - ? PracticeAccessMode.ClassAssignment - : PracticeAccessMode.Package, - null, - snapshot.ClassAssignmentIds.FirstOrDefault() is { } assignmentId && assignmentId != Guid.Empty - ? assignmentId - : null, - snapshot); - } } diff --git a/Tiku.Infrastructure/Learning/PracticeSessions/V2PracticeSessionService.cs b/Tiku.Infrastructure/Learning/PracticeSessions/V2PracticeSessionService.cs new file mode 100644 index 0000000..95e3014 --- /dev/null +++ b/Tiku.Infrastructure/Learning/PracticeSessions/V2PracticeSessionService.cs @@ -0,0 +1,643 @@ +using System.Buffers.Binary; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tiku.Application.Content; +using Tiku.Application.Learning; +using Tiku.Application.Security; +using Tiku.Domain.Common; +using Tiku.Domain.Content; +using Tiku.Domain.Learning; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure.Learning; + +internal interface IV2PracticeSessionService +{ + + Task CreateAsync( + LearningActor actor, + PracticeSessionCommand command, + CancellationToken cancellationToken); + + Task> LoadSolutionsAsync( + IReadOnlyCollection sessionQuestions, + CancellationToken cancellationToken); +} + +internal sealed class V2PracticeSessionService(LearningServiceDependencies dependencies) + : LearningActivityServiceBase(dependencies), IV2PracticeSessionService +{ + public async Task CreateAsync( + LearningActor actor, + PracticeSessionCommand command, + CancellationToken cancellationToken) + { + var resourceType = command.ResourceType; + var resourceId = command.ResourceId; + var now = DateTimeOffset.UtcNow; + var resourceManifests = await ( + from resource in learningAccessPersistence.ProductManifestResources.AsNoTracking() + join version in learningAccessPersistence.ProductAccessManifestVersions.AsNoTracking() + on new { resource.TenantId, Id = resource.ProductAccessManifestVersionId } + equals new { version.TenantId, version.Id } + join manifest in learningAccessPersistence.ProductAccessManifests.AsNoTracking() + on new { version.TenantId, Id = version.ProductAccessManifestId } + equals new { manifest.TenantId, manifest.Id } + join product in learningAccessPersistence.LearningProducts.AsNoTracking() + on new { manifest.TenantId, Id = manifest.LearningProductId } + equals new { product.TenantId, product.Id } + where resource.TenantId == actor.TenantId && + resource.ResourceType == resourceType && + resource.ResourceId == resourceId && + version.Status == AccessManifestStatus.Published && + manifest.IsActive && + product.IsActive + select new { resource.ProductAccessManifestVersionId, product.BusinessLineId }) + .ToArrayAsync(cancellationToken); + var businessLineIds = resourceManifests.Select(item => item.BusinessLineId).Distinct().ToArray(); + if (businessLineIds.Length != 1) + throw new LearningAccessException("practice_resource_not_entitled", + "The requested resource is unavailable in the current learning context."); + + var snapshot = await effectiveLearningAccessService.GetSnapshotAsync( + actor, businessLineIds[0], cancellationToken); + var resourceManifestIds = resourceManifests.Select(item => item.ProductAccessManifestVersionId) + .Where(snapshot.ManifestVersionIds.Contains) + .Distinct() + .ToArray(); + var grant = await ResolveV2GrantAsync( + actor, resourceType, resourceId, resourceManifestIds, now, cancellationToken); + if (grant.ManifestVersionIds.Length == 0) + throw new LearningAccessException("practice_resource_not_entitled", + "The requested resource is not covered by an active product or class grant."); + + var releaseLinks = await learningAccessPersistence.ProductManifestReleases.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && + grant.ManifestVersionIds.Contains(item.ProductAccessManifestVersionId) && + snapshot.ContentReleaseIds.Contains(item.ContentReleaseId)) + .ToArrayAsync(cancellationToken); + releaseLinks = await RestrictReleaseLinksAsync( + actor.TenantId, resourceType, resourceId, releaseLinks, cancellationToken); + if (releaseLinks.Length == 0) + throw new LearningAccessException("practice_resource_release_missing", + "The requested resource has no ready content release in this Cell."); + + const int limit = 100; + var seed = StableSeed(actor.UserId, resourceType, resourceId); + var candidates = new List(limit); + var ownerGroups = releaseLinks.GroupBy(item => item.ContentOwnerTenantId) + .OrderBy(group => group.Key) + .ToArray(); + var rotatedGroups = Rotate(ownerGroups, seed); + foreach (var group in rotatedGroups) + { + var remaining = limit - candidates.Count; + if (remaining == 0) break; + var ownerCandidates = await contentCandidateReader.GetCandidatesAsync( + new ContentCandidateQuery( + group.Key, + group.Select(item => item.ContentReleaseId).Distinct().ToArray(), + snapshot.AudienceSegmentIds, + resourceType == AccessResourceType.CurriculumNode ? resourceId : null, + resourceType is AccessResourceType.CollectionRelease or AccessResourceType.BlueprintRelease + ? await ResolveCollectionReleaseIdsAsync(group.Key, resourceType, resourceId, cancellationToken) + : null, + remaining, + seed), + cancellationToken); + candidates.AddRange(ownerCandidates); + } + + var distinctCandidates = candidates.DistinctBy(item => item.ContentReleaseQuestionId).Take(limit).ToArray(); + if (distinctCandidates.Length == 0) + throw new LearningValidationException("no_practice_questions", + "No published questions match the current targets and resource."); + var deliveries = await LoadV2DeliveriesAsync(distinctCandidates, cancellationToken); + var specification = resourceType == AccessResourceType.BlueprintRelease + ? await LoadBlueprintSpecificationAsync(releaseLinks, resourceId, cancellationToken) + : null; + var mode = NormalizeMode(command.Mode ?? (resourceType == AccessResourceType.BlueprintRelease ? "mock_exam" : "chapter")); + var durationMinutes = specification?.DurationMinutes; + var totalScore = specification?.TotalScore ?? deliveries.Sum(item => item.Policy.DefaultScore); + var sessionExpiry = durationMinutes.HasValue + ? now.AddMinutes(Math.Min(240, durationMinutes.Value + 15)) + : now.AddHours(2); + await using var transaction = await unitOfWork.Database.BeginTransactionAsync(cancellationToken); + var consumedQuota = await ReserveV2QuestionQuotaAsync( + actor, + grant, + distinctCandidates.Length, + cancellationToken); + var session = new PracticeSession + { + TenantId = actor.TenantId, + UserId = actor.UserId, + Mode = mode, + TargetType = resourceType.ToString(), + TargetId = resourceId, + BlueprintId = resourceType == AccessResourceType.BlueprintRelease ? resourceId : null, + CollectionId = resourceType == AccessResourceType.CollectionRelease ? resourceId : null, + ContentNodeId = resourceType == AccessResourceType.CurriculumNode ? resourceId : null, + QuestionCount = distinctCandidates.Length, + DurationMinutes = durationMinutes, + TotalScore = totalScore, + ExpiresAt = sessionExpiry, + AccessMode = grant.ClassAssignmentGrantId.HasValue + ? PracticeAccessMode.ClassAssignment + : PracticeAccessMode.Package, + AccessEntitlementId = grant.StudentEntitlementId, + AccessClassAssignmentId = grant.ClassAssignmentGrantId, + ConsumedFreeQuota = consumedQuota, + AccessGrantVersion = snapshot.GrantVersion, + StrongRevocationVersion = snapshot.StrongRevocationVersion, + AuthorizationExpiresAt = sessionExpiry, + AccessSnapshot = JsonSerializer.SerializeToElement(new + { + strategy = "effective_access_v2", + businessLineId = snapshot.BusinessLineId, + primaryProfileVersionId = snapshot.PrimaryProfileVersionId, + alternateProfileVersionIds = snapshot.AlternateProfileVersionIds.Order().ToArray(), + releaseIds = releaseLinks.Select(item => item.ContentReleaseId).Distinct().Order().ToArray(), + audienceSegmentIds = snapshot.AudienceSegmentIds.Order().ToArray(), + manifestVersionIds = grant.ManifestVersionIds.Order().ToArray(), + grantVersion = snapshot.GrantVersion, + contentVersion = snapshot.ContentVersion, + strongRevocationVersion = snapshot.StrongRevocationVersion, + resourceType, + resourceId, + seed, + requestedCount = limit, + grantedCount = distinctCandidates.Length + }), + Metadata = JsonDefaults.Object() + }; + learningPersistence.PracticeSessions.Add(session); + + foreach (var delivery in deliveries.OrderBy(item => + Array.FindIndex(distinctCandidates, candidate => + candidate.ContentReleaseQuestionId == item.Candidate.ContentReleaseQuestionId))) + { + if (!QuestionGrader.HasValidAuthoritativeAnswer( + delivery.Revision.QuestionType, + delivery.Revision.CorrectOptionIndex, + delivery.Revision.CorrectOptionIndices, + delivery.Revision.AnswerText)) + throw new LearningValidationException("practice_question_grading_rule_invalid", + $"Question revision '{delivery.Revision.Id}' has no valid authoritative grading rule."); + learningPersistence.PracticeSessionQuestions.Add(new PracticeSessionQuestion + { + TenantId = actor.TenantId, + PracticeSessionId = session.Id, + ContentReleaseQuestionId = delivery.Candidate.ContentReleaseQuestionId, + QuestionOwnerTenantId = delivery.Candidate.QuestionAssetOwnerTenantId, + QuestionAssetId = delivery.Candidate.QuestionAssetId, + QuestionRevisionId = delivery.Candidate.QuestionRevisionId, + QuestionPlacementId = delivery.Candidate.QuestionPlacementId, + AssessmentPolicyVersionId = delivery.Candidate.AssessmentPolicyVersionId, + Position = Array.FindIndex(distinctCandidates, candidate => + candidate.ContentReleaseQuestionId == delivery.Candidate.ContentReleaseQuestionId), + Score = delivery.Policy.DefaultScore, + QuestionType = delivery.Revision.QuestionType, + TypeLabelSnapshot = delivery.Revision.TypeLabel, + DifficultySnapshot = delivery.Candidate.Difficulty, + TagsSnapshot = JsonDefaults.Array(), + GradingRulesSnapshot = BuildV2GradingSnapshot(delivery), + SnapshotVersion = 2 + }); + } + + learningPersistence.PracticeAccessEvents.Add(new PracticeAccessEvent + { + TenantId = actor.TenantId, + UserId = actor.UserId, + PracticeSessionId = session.Id, + EventType = PracticeAccessEventType.SessionCreated, + AccessMode = grant.ClassAssignmentGrantId.HasValue + ? PracticeAccessEventMode.ClassAssignment + : PracticeAccessEventMode.Package, + RequestedCount = limit, + GrantedCount = distinctCandidates.Length, + ConsumedFreeQuota = consumedQuota, + EntitlementId = grant.StudentEntitlementId, + Metadata = session.AccessSnapshot + }); + await unitOfWork.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + return ToItem(session); + } + + private async Task ReserveV2QuestionQuotaAsync( + LearningActor actor, + V2Grant grant, + int questionCount, + CancellationToken cancellationToken) + { + if (questionCount <= 0) return 0; + + if (grant.TotalQuestionLimit.HasValue && grant.StudentEntitlementId.HasValue) + { + var updated = await learningAccessPersistence.StudentEntitlementsV2 + .Where(item => item.TenantId == actor.TenantId && + item.Id == grant.StudentEntitlementId.Value && + item.Status == StudentEntitlementStatus.Active && + item.UsedQuestionCount + questionCount <= grant.TotalQuestionLimit.Value) + .ExecuteUpdateAsync(setters => setters + .SetProperty(item => item.UsedQuestionCount, item => item.UsedQuestionCount + questionCount) + .SetProperty(item => item.UpdatedAt, DateTimeOffset.UtcNow), cancellationToken); + if (updated != 1) + throw new LearningAccessException("practice_total_quota_exhausted", + "The product question quota has been exhausted."); + } + + if (!grant.DailyQuestionLimit.HasValue) return grant.TotalQuestionLimit.HasValue ? questionCount : 0; + + var usageId = Guid.NewGuid(); + var scopeType = "product_manifest"; + var emptyMetadata = "{}"; + var reserved = await learningPersistence.Database.ExecuteSqlInterpolatedAsync($""" + INSERT INTO practice_daily_usage + (id, tenant_id, user_id, usage_date, scope_type, scope_id, + free_limit, used_count, metadata, created_at, updated_at) + VALUES + ({usageId}, {actor.TenantId}, {actor.UserId}, current_date, {scopeType}, + {grant.ManifestVersionIds[0]}, {grant.DailyQuestionLimit.Value}, {questionCount}, + {emptyMetadata}::jsonb, now(), now()) + ON CONFLICT (tenant_id, user_id, usage_date, scope_type, scope_id) + DO UPDATE SET + used_count = practice_daily_usage.used_count + {questionCount}, + updated_at = now() + WHERE practice_daily_usage.used_count + {questionCount} <= EXCLUDED.free_limit + """, cancellationToken); + if (reserved != 1) + throw new LearningAccessException("practice_daily_quota_exhausted", + "The daily product question quota has been exhausted."); + return questionCount; + } + + private async Task ResolveV2GrantAsync( + LearningActor actor, + AccessResourceType resourceType, + Guid resourceId, + Guid[] resourceManifestIds, + DateTimeOffset now, + CancellationToken cancellationToken) + { + var entitlement = await ( + from item in learningAccessPersistence.StudentEntitlementsV2.AsNoTracking() + join manifest in learningAccessPersistence.ProductAccessManifestVersions.AsNoTracking() + on new { item.TenantId, Id = item.ProductAccessManifestVersionId } + equals new { manifest.TenantId, manifest.Id } + where item.TenantId == actor.TenantId && + item.UserId == actor.UserId && + item.Status == StudentEntitlementStatus.Active && + item.StartsAt <= now && + (item.EndsAt == null || item.EndsAt > now) && + resourceManifestIds.Contains(item.ProductAccessManifestVersionId) + orderby item.EndsAt + select new + { + item.Id, + item.ProductAccessManifestVersionId, + manifest.DailyQuestionLimit, + manifest.TotalQuestionLimit + }) + .FirstOrDefaultAsync(cancellationToken); + if (entitlement is not null) + return new V2Grant( + [entitlement.ProductAccessManifestVersionId], + entitlement.Id, + null, + entitlement.DailyQuestionLimit, + entitlement.TotalQuestionLimit); + + var classGrant = await ( + from member in learningAccessPersistence.TenantClassMembers.AsNoTracking() + join grant in learningAccessPersistence.ClassAssignmentGrants.AsNoTracking() + on new { member.TenantId, member.ClassId } equals new { grant.TenantId, grant.ClassId } + join manifest in learningAccessPersistence.ProductAccessManifestVersions.AsNoTracking() + on new { grant.TenantId, Id = grant.ProductAccessManifestVersionId } + equals new { manifest.TenantId, manifest.Id } + where member.TenantId == actor.TenantId && + member.UserId == actor.UserId && + member.MemberType == TenantClassMemberType.Student && + member.Status == TenantClassMemberStatus.Active && + grant.Status == ClassAssignmentGrantStatus.Active && + grant.StartsAt <= now && + (grant.EndsAt == null || grant.EndsAt > now) && + grant.ResourceType == resourceType && + grant.ResourceId == resourceId && + resourceManifestIds.Contains(grant.ProductAccessManifestVersionId) + select new + { + grant.Id, + grant.ProductAccessManifestVersionId, + manifest.DailyQuestionLimit, + manifest.TotalQuestionLimit + }) + .FirstOrDefaultAsync(cancellationToken); + return classGrant is null + ? new V2Grant([], null, null, null, null) + : new V2Grant( + [classGrant.ProductAccessManifestVersionId], + null, + classGrant.Id, + classGrant.DailyQuestionLimit, + null); + } + + private async Task RestrictReleaseLinksAsync( + Guid tenantId, + AccessResourceType resourceType, + Guid resourceId, + ProductManifestRelease[] links, + CancellationToken cancellationToken) + { + if (resourceType == AccessResourceType.ContentRelease) + links = links.Where(item => item.ContentReleaseId == resourceId).ToArray(); + else if (resourceType == AccessResourceType.CollectionRelease) + links = await FilterLinksByCollectionAsync(links, resourceId, cancellationToken); + else if (resourceType == AccessResourceType.BlueprintRelease) + links = await FilterLinksByBlueprintAsync(links, resourceId, cancellationToken); + + var packageLinks = links.Where(item => item.PlatformContentPackageVersionId.HasValue).ToArray(); + if (packageLinks.Length == 0) return links; + var cellId = await learningAccessPersistence.Tenants.AsNoTracking() + .Where(item => item.Id == tenantId) + .Select(item => item.CellId) + .SingleAsync(cancellationToken); + var packageIds = packageLinks.Select(item => item.PlatformContentPackageVersionId!.Value).ToArray(); + var readyPackageIds = await questionBankPersistence.PlatformContentPackageCellProjections.AsNoTracking() + .Where(item => item.CellId == cellId && + packageIds.Contains(item.PlatformContentPackageVersionId) && + item.Status == CellProjectionStatus.Ready) + .Select(item => item.PlatformContentPackageVersionId) + .ToArrayAsync(cancellationToken); + return links.Where(item => !item.PlatformContentPackageVersionId.HasValue || + readyPackageIds.Contains(item.PlatformContentPackageVersionId.Value)) + .ToArray(); + } + + private async Task FilterLinksByCollectionAsync( + ProductManifestRelease[] links, + Guid collectionReleaseId, + CancellationToken cancellationToken) + { + var releaseIds = links.Select(item => item.ContentReleaseId).Distinct().ToArray(); + var matches = new List<(Guid TenantId, Guid ContentReleaseId)>(); + foreach (var ownerTenantId in links.Select(item => item.ContentOwnerTenantId).Distinct()) + matches.AddRange(await ReadForContentOwnerAsync( + ownerTenantId, + "Resolve an authorized V2 collection release", + async (persistence, token) => (await persistence.CollectionReleases.AsNoTracking() + .Where(item => item.TenantId == ownerTenantId && + item.Id == collectionReleaseId && + releaseIds.Contains(item.ContentReleaseId) && + item.Status == ContentReleaseStatus.Published) + .Select(item => new { item.TenantId, item.ContentReleaseId }) + .ToArrayAsync(token)) + .Select(item => (item.TenantId, item.ContentReleaseId)).ToArray(), + cancellationToken)); + return links.Where(link => matches.Any(match => + match.TenantId == link.ContentOwnerTenantId && match.ContentReleaseId == link.ContentReleaseId)).ToArray(); + } + + private async Task FilterLinksByBlueprintAsync( + ProductManifestRelease[] links, + Guid blueprintReleaseId, + CancellationToken cancellationToken) + { + var releaseIds = links.Select(item => item.ContentReleaseId).Distinct().ToArray(); + var matches = new List<(Guid TenantId, Guid ContentReleaseId)>(); + foreach (var ownerTenantId in links.Select(item => item.ContentOwnerTenantId).Distinct()) + matches.AddRange(await ReadForContentOwnerAsync( + ownerTenantId, + "Resolve an authorized V2 blueprint release", + async (persistence, token) => (await persistence.BlueprintReleases.AsNoTracking() + .Where(item => item.TenantId == ownerTenantId && + item.Id == blueprintReleaseId && + releaseIds.Contains(item.ContentReleaseId) && + item.Status == ContentReleaseStatus.Published) + .Select(item => new { item.TenantId, item.ContentReleaseId }) + .ToArrayAsync(token)) + .Select(item => (item.TenantId, item.ContentReleaseId)).ToArray(), + cancellationToken)); + return links.Where(link => matches.Any(match => + match.TenantId == link.ContentOwnerTenantId && match.ContentReleaseId == link.ContentReleaseId)).ToArray(); + } + + private async Task?> ResolveCollectionReleaseIdsAsync( + Guid ownerTenantId, + AccessResourceType resourceType, + Guid resourceId, + CancellationToken cancellationToken) + { + if (resourceType == AccessResourceType.CollectionRelease) return [resourceId]; + if (resourceType != AccessResourceType.BlueprintRelease) return null; + var collectionId = (await ReadForContentOwnerAsync( + ownerTenantId, + "Resolve the collection pinned by an authorized V2 blueprint", + (persistence, token) => persistence.BlueprintReleases.AsNoTracking() + .Where(item => item.TenantId == ownerTenantId && item.Id == resourceId) + .Select(item => item.CollectionReleaseId) + .ToArrayAsync(token), + cancellationToken)).SingleOrDefault(); + return collectionId.HasValue ? [collectionId.Value] : null; + } + + private async Task LoadV2DeliveriesAsync( + IReadOnlyCollection candidates, + CancellationToken cancellationToken) + { + var revisions = new List(); + foreach (var ownerGroup in candidates.GroupBy(item => item.QuestionAssetOwnerTenantId)) + { + var revisionIds = ownerGroup.Select(item => item.QuestionRevisionId).Distinct().ToArray(); + revisions.AddRange(await ReadForContentOwnerAsync( + ownerGroup.Key, + "Load explicitly authorized immutable V2 question revisions", + (persistence, token) => persistence.QuestionRevisions.AsNoTracking() + .Where(item => item.TenantId == ownerGroup.Key && revisionIds.Contains(item.Id)) + .ToArrayAsync(token), + cancellationToken)); + } + + var policies = new List(); + var objectiveRules = new List(); + foreach (var ownerGroup in candidates.GroupBy(item => item.ContentOwnerTenantId)) + { + var policyIds = ownerGroup.Select(item => item.AssessmentPolicyVersionId).Distinct().ToArray(); + policies.AddRange(await ReadForContentOwnerAsync( + ownerGroup.Key, + "Load explicitly authorized immutable V2 assessment policies", + (persistence, token) => persistence.AssessmentPolicyVersions.AsNoTracking() + .Where(item => item.TenantId == ownerGroup.Key && policyIds.Contains(item.Id) && + item.Status == ContentDefinitionStatus.Published) + .ToArrayAsync(token), + cancellationToken)); + objectiveRules.AddRange(await ReadForContentOwnerAsync( + ownerGroup.Key, + "Load explicitly authorized immutable V2 objective rules", + (persistence, token) => persistence.ObjectiveGradingRules.AsNoTracking() + .Where(item => item.TenantId == ownerGroup.Key && + policyIds.Contains(item.AssessmentPolicyVersionId)) + .ToArrayAsync(token), + cancellationToken)); + } + + return candidates.Select(candidate => + { + var revision = revisions.SingleOrDefault(item => + item.TenantId == candidate.QuestionAssetOwnerTenantId && + item.Id == candidate.QuestionRevisionId) ?? throw new LearningValidationException( + "practice_question_revision_missing", "An immutable question revision is missing."); + var policy = policies.SingleOrDefault(item => + item.TenantId == candidate.ContentOwnerTenantId && + item.Id == candidate.AssessmentPolicyVersionId) ?? throw new LearningValidationException( + "practice_assessment_policy_missing", "A published assessment policy is missing."); + return new V2Delivery( + candidate, + revision, + policy, + objectiveRules.Where(item => + item.TenantId == candidate.ContentOwnerTenantId && + item.AssessmentPolicyVersionId == policy.Id) + .OrderBy(item => item.Id) + .ToArray()); + }).ToArray(); + } + + private Task ReadForContentOwnerAsync( + Guid ownerTenantId, + string reason, + Func> read, + CancellationToken cancellationToken) + { + return tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + ownerTenantId, + SystemScopeCallerType.PublicQuestionBank, + nameof(PracticeSessionService), + reason, + Guid.NewGuid().ToString("N")), + (provider, token) => read(provider.GetRequiredService(), token), + cancellationToken); + } + + private async Task LoadBlueprintSpecificationAsync( + IReadOnlyCollection links, + Guid blueprintReleaseId, + CancellationToken cancellationToken) + { + var owners = links.Select(item => item.ContentOwnerTenantId).Distinct().ToArray(); + return await tenantExecutionScope.ExecuteAsync( + new SystemScopeRequest( + null, + SystemScopeCallerType.PublicQuestionBank, + nameof(PracticeSessionService), + "Load an explicitly authorized published blueprint specification", + Guid.NewGuid().ToString("N"), + true), + async (provider, token) => + { + var persistence = provider.GetRequiredService(); + return await ( + from blueprint in persistence.BlueprintReleases.AsNoTracking() + join specification in persistence.ExamPaperSpecificationVersions.AsNoTracking() + on new + { + blueprint.TenantId, + Id = blueprint.ExamPaperSpecificationVersionId + } + equals new { specification.TenantId, specification.Id } + where owners.Contains(blueprint.TenantId) && + blueprint.Id == blueprintReleaseId && + blueprint.Status == ContentReleaseStatus.Published && + specification.Status == ContentDefinitionStatus.Published + select specification) + .SingleOrDefaultAsync(token); + }, + cancellationToken); + } + + public async Task> LoadSolutionsAsync( + IReadOnlyCollection sessionQuestions, + CancellationToken cancellationToken) + { + if (sessionQuestions.Count == 0) return []; + var revisions = new List(); + foreach (var ownerGroup in sessionQuestions.GroupBy(item => item.QuestionOwnerTenantId)) + { + var revisionIds = ownerGroup.Select(item => item.QuestionRevisionId!.Value).Distinct().ToArray(); + revisions.AddRange(await ReadForContentOwnerAsync( + ownerGroup.Key, + "Reveal explicitly locked V2 question solutions", + (persistence, token) => persistence.QuestionRevisions.AsNoTracking() + .Where(item => item.TenantId == ownerGroup.Key && revisionIds.Contains(item.Id)) + .ToArrayAsync(token), + cancellationToken)); + } + + return sessionQuestions.ToDictionary( + item => item.Id, + item => + { + var revision = revisions.Single(version => + version.TenantId == item.QuestionOwnerTenantId && + version.Id == item.QuestionRevisionId); + return new QuestionSolutionItem( + revision.CorrectOptionIndex, + revision.CorrectOptionIndices, + revision.AnswerText, + revision.Explanation); + }); + } + + private static JsonElement BuildV2GradingSnapshot(V2Delivery delivery) + { + return JsonSerializer.SerializeToElement(new + { + version = 2, + assessmentPolicyVersionId = delivery.Policy.Id, + gradingMode = delivery.Policy.GradingMode, + roundingScale = delivery.Policy.RoundingScale, + correctOptionIndex = delivery.Revision.CorrectOptionIndex, + correctOptionIndices = delivery.Revision.CorrectOptionIndices, + answerText = delivery.Revision.AnswerText, + objectiveRules = delivery.ObjectiveRules.Select(item => new + { + item.RuleType, + item.Configuration + }).ToArray() + }); + } + + private static int StableSeed(Guid userId, AccessResourceType resourceType, Guid resourceId) + { + var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{userId:N}:{resourceType}:{resourceId:N}")); + return BinaryPrimitives.ReadInt32LittleEndian(hash); + } + + private static T[] Rotate(T[] values, int seed) + { + if (values.Length <= 1) return values; + var start = (int)((uint)seed % (uint)values.Length); + return values.Skip(start).Concat(values.Take(start)).ToArray(); + } + + private sealed record V2Grant( + Guid[] ManifestVersionIds, + Guid? StudentEntitlementId, + Guid? ClassAssignmentGrantId, + int? DailyQuestionLimit, + int? TotalQuestionLimit); + + private sealed record V2Delivery( + ContentCandidateItem Candidate, + QuestionRevision Revision, + AssessmentPolicyVersion Policy, + ObjectiveGradingRule[] ObjectiveRules); +} diff --git a/Tiku.Infrastructure/Modules/ContentModule.cs b/Tiku.Infrastructure/Modules/ContentModule.cs index 2bcc237..f7b1af7 100644 --- a/Tiku.Infrastructure/Modules/ContentModule.cs +++ b/Tiku.Infrastructure/Modules/ContentModule.cs @@ -11,6 +11,7 @@ using Tiku.Application.StudyContent; using Tiku.Infrastructure.Assets; using Tiku.Infrastructure.Catalog; using Tiku.Infrastructure.Content; +using Tiku.Infrastructure.ContentV2; using Tiku.Infrastructure.Content.Exports; using Tiku.Infrastructure.Content.Imports; using Tiku.Infrastructure.Assets.Security; @@ -27,15 +28,7 @@ internal static class ContentModule internal static IServiceCollection AddContentModule(this IServiceCollection services) { services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -43,9 +36,13 @@ internal static class ContentModule services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Tiku.Infrastructure/Modules/LearningModule.cs b/Tiku.Infrastructure/Modules/LearningModule.cs index 4f654f4..bf18dfd 100644 --- a/Tiku.Infrastructure/Modules/LearningModule.cs +++ b/Tiku.Infrastructure/Modules/LearningModule.cs @@ -10,7 +10,12 @@ internal static class LearningModule { services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Tiku.Infrastructure/Modules/PlatformModule.cs b/Tiku.Infrastructure/Modules/PlatformModule.cs index f8658d1..efd75ae 100644 --- a/Tiku.Infrastructure/Modules/PlatformModule.cs +++ b/Tiku.Infrastructure/Modules/PlatformModule.cs @@ -25,11 +25,6 @@ internal static class PlatformModule services.AddScoped(); services.AddScoped(); services.AddOptions(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Tiku.Infrastructure/Persistence/Configurations/ContentV2AssessmentConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/ContentV2AssessmentConfigurations.cs new file mode 100644 index 0000000..88d4faa --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/ContentV2AssessmentConfigurations.cs @@ -0,0 +1,114 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Content; +using Tiku.Domain.Learning; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal sealed class AssessmentPolicyConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("assessment_policies"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Code).HasMaxLength(100); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.HasIndex(entity => new { entity.TenantId, entity.Code }).IsUnique(); + builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessLineId) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, AssessmentPolicyId = entity.Id, Id = entity.CurrentVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.AssessmentPolicyId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class AssessmentPolicyVersionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("assessment_policy_versions"); + builder.HasAlternateKey(entity => new { entity.TenantId, entity.AssessmentPolicyId, entity.Id }); + builder.Property(entity => entity.GradingMode).HasSnakeCaseEnum(); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); + builder.HasIndex(entity => new { entity.TenantId, entity.AssessmentPolicyId, entity.VersionNo }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.AssessmentPolicyId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.ToTable(table => + { + table.HasCheckConstraint("ck_assessment_policy_default_score", "default_score > 0"); + table.HasCheckConstraint("ck_assessment_policy_rounding", "rounding_scale between 0 and 6"); + }); + } +} + +internal sealed class ObjectiveGradingRuleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("objective_grading_rules"); + builder.Property(entity => entity.RuleType).HasMaxLength(100); + builder.Property(entity => entity.Configuration).IsJson("{}"); + builder.HasIndex(entity => new { entity.TenantId, entity.AssessmentPolicyVersionId, entity.RuleType }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.AssessmentPolicyVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + } +} + +internal sealed class SubjectiveRubricConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("subjective_rubrics"); + builder.Property(entity => entity.Code).HasMaxLength(100); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.Criteria).IsJson("[]"); + builder.HasIndex(entity => new { entity.TenantId, entity.AssessmentPolicyVersionId, entity.Code }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.AssessmentPolicyVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.ToTable(table => table.HasCheckConstraint("ck_subjective_rubric_max_score", "max_score > 0")); + } +} + +internal sealed class AssessmentScoreBandConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("assessment_score_bands"); + builder.Property(entity => entity.Label).HasMaxLength(100); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.AssessmentPolicyVersionId, + entity.MinScore, + entity.MaxScore + }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.AssessmentPolicyVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.ToTable(table => table.HasCheckConstraint("ck_assessment_score_band", "max_score >= min_score")); + } +} + +internal sealed class ScoreAdjustmentRuleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("score_adjustment_rules"); + builder.Property(entity => entity.RuleType).HasMaxLength(100); + builder.Property(entity => entity.Configuration).IsJson("{}"); + builder.HasIndex(entity => new { entity.TenantId, entity.AssessmentPolicyVersionId, entity.Priority }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.AssessmentPolicyVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/ContentV2ConfigurationSupport.cs b/Tiku.Infrastructure/Persistence/Configurations/ContentV2ConfigurationSupport.cs new file mode 100644 index 0000000..6745c0d --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/ContentV2ConfigurationSupport.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Common; +using Tiku.Domain.Tenancy; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal static class ContentV2ConfigurationSupport +{ + internal static void ConfigureTenantOwned( + this EntityTypeBuilder builder, + string tableName) + where TEntity : Entity, ITenantOwned + { + builder.ConfigureEntity(tableName); + builder.HasAlternateKey(nameof(ITenantOwned.TenantId), nameof(Entity.Id)); + builder.HasOne().WithMany() + .HasForeignKey(nameof(ITenantOwned.TenantId)) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/ContentV2CurriculumConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/ContentV2CurriculumConfigurations.cs new file mode 100644 index 0000000..cda70c5 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/ContentV2CurriculumConfigurations.cs @@ -0,0 +1,133 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Content; +using Tiku.Domain.Learning; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal sealed class KnowledgeConceptConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("knowledge_concepts"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Code).HasMaxLength(100); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.Description).HasMaxLength(2000); + builder.HasIndex(entity => new { entity.TenantId, entity.Code }).IsUnique(); + } +} + +internal sealed class CurriculumConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("curricula"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Code).HasMaxLength(100); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.HasIndex(entity => new { entity.TenantId, entity.Code }).IsUnique(); + builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessLineId) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, CurriculumId = entity.Id, Id = entity.CurrentVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.CurriculumId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class CurriculumVersionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("curriculum_versions"); + builder.HasAlternateKey(entity => new { entity.TenantId, entity.CurriculumId, entity.Id }); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); + builder.HasIndex(entity => new { entity.TenantId, entity.CurriculumId, entity.VersionNo }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.CurriculumId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + } +} + +internal sealed class CurriculumNodeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("curriculum_nodes"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Code).HasMaxLength(100); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.Kind).HasSnakeCaseEnum(); + builder.HasIndex(entity => new { entity.TenantId, entity.CurriculumVersionId, entity.Code }).IsUnique(); + builder.HasIndex(entity => new { entity.TenantId, entity.CurriculumVersionId, entity.ParentId, entity.SortOrder }); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.CurriculumVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ParentId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.KnowledgeConceptId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class CurriculumNodeClosureConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("curriculum_node_closure"); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.CurriculumVersionId, + entity.AncestorNodeId, + entity.DescendantNodeId + }).IsUnique(); + builder.HasIndex(entity => new { entity.TenantId, entity.CurriculumVersionId, entity.DescendantNodeId, entity.Depth }); + builder.ToTable(table => table.HasCheckConstraint("ck_curriculum_node_closure_depth", "depth >= 0")); + } +} + +internal sealed class ExamPaperSpecificationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("exam_paper_specifications"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Code).HasMaxLength(100); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.HasIndex(entity => new { entity.TenantId, entity.Code }).IsUnique(); + builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessLineId) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class ExamPaperSpecificationVersionConfiguration : + IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("exam_paper_specification_versions"); + builder.Property(entity => entity.Sections).IsJson("[]"); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); + builder.HasIndex(entity => new { entity.TenantId, entity.SpecificationId, entity.VersionNo }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.SpecificationId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.ToTable(table => + { + table.HasCheckConstraint("ck_exam_paper_spec_duration", "duration_minutes > 0"); + table.HasCheckConstraint("ck_exam_paper_spec_total_score", "total_score > 0"); + }); + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/ContentV2QuestionConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/ContentV2QuestionConfigurations.cs new file mode 100644 index 0000000..7a88b2a --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/ContentV2QuestionConfigurations.cs @@ -0,0 +1,159 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Content; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal sealed class QuestionAssetConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("question_assets"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.DeliveryFingerprint).HasMaxLength(64); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.HasIndex(entity => new { entity.TenantId, entity.DeliveryFingerprint }).IsUnique(); + builder.HasIndex(entity => new { entity.TenantId, entity.FamilyId }); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.FamilyId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, QuestionAssetId = entity.Id, Id = entity.CurrentRevisionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class QuestionRevisionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("question_revisions"); + builder.HasAlternateKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id }); + builder.Property(entity => entity.QuestionType).HasMaxLength(50); + builder.Property(entity => entity.TypeLabel).HasMaxLength(100); + builder.Property(entity => entity.NormalizedContent).HasColumnType("text"); + builder.Property(entity => entity.Options).IsJson("[]"); + builder.Property(entity => entity.CorrectOptionIndices).IsJson("[]"); + builder.Property(entity => entity.SubQuestions).IsJson("[]"); + builder.Property(entity => entity.CodeLang).HasMaxLength(50); + builder.Property(entity => entity.DeliveryFingerprint).HasMaxLength(64); + builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); + builder.HasIndex(entity => new { entity.TenantId, entity.QuestionAssetId, entity.RevisionNo }).IsUnique(); + builder.HasIndex(entity => new { entity.TenantId, entity.DeliveryFingerprint }); + builder.HasIndex(entity => entity.NormalizedContent).HasMethod("gin") + .HasOperators("gin_trgm_ops") + .HasDatabaseName("ix_question_revisions_content_trgm"); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.QuestionAssetId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + } +} + +internal sealed class QuestionFamilyConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("question_families"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.Description).HasMaxLength(2000); + } +} + +internal sealed class QuestionPlacementConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("question_placements"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.QuestionAssetOwnerTenantId, + entity.QuestionAssetId, + entity.CurriculumVersionId, + entity.CurriculumNodeId, + entity.AssessmentPolicyVersionId + }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { TenantId = entity.QuestionAssetOwnerTenantId, Id = entity.QuestionAssetId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.CurriculumVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.CurriculumNodeId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.AssessmentPolicyVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.ToTable(table => table.HasCheckConstraint( + "ck_question_placements_valid_period", "valid_to is null or valid_from is null or valid_to > valid_from")); + } +} + +internal sealed class QuestionPlacementConceptConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("question_placement_concepts"); + builder.HasIndex(entity => new { entity.TenantId, entity.PlacementId, entity.KnowledgeConceptId }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.PlacementId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.KnowledgeConceptId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class QuestionPlacementRuleGroupConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("question_placement_rule_groups"); + builder.HasIndex(entity => new { entity.TenantId, entity.PlacementId, entity.GroupOrder }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.PlacementId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + } +} + +internal sealed class QuestionPlacementRuleConditionConfiguration : + IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("question_placement_rule_conditions"); + builder.Property(entity => entity.Operator).HasSnakeCaseEnum(); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.RuleGroupId, + entity.TargetDimensionDefinitionId, + entity.TargetNodeId, + entity.Operator + }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.RuleGroupId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.TargetDimensionDefinitionId) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.TargetNodeId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/ContentV2ReleaseConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/ContentV2ReleaseConfigurations.cs new file mode 100644 index 0000000..cdeaca9 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/ContentV2ReleaseConfigurations.cs @@ -0,0 +1,238 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Content; +using Tiku.Domain.Learning; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal sealed class ContentReleaseConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("content_releases"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.CompilerVersion).HasMaxLength(100); + builder.Property(entity => entity.SourceFingerprint).HasMaxLength(64); + builder.HasIndex(entity => new { entity.TenantId, entity.CurriculumVersionId, entity.ReleaseNo }).IsUnique(); + builder.HasIndex(entity => new { entity.TenantId, entity.BusinessLineId, entity.Status, entity.PublishedAt }); + builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessLineId) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.CurriculumVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class AudienceSegmentConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("audience_segments"); + builder.Property(entity => entity.RuleHash).HasMaxLength(64); + builder.HasIndex(entity => new { entity.TenantId, entity.ContentReleaseId, entity.RuleHash }).IsUnique(); + builder.HasIndex(entity => new { entity.TenantId, entity.ContentReleaseId, entity.SegmentOrdinal }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ContentReleaseId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + } +} + +internal sealed class AudienceSegmentMemberConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("audience_segment_members"); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.ContentReleaseId, + entity.AudienceSegmentId, + entity.ExamTargetProfileVersionId + }).IsUnique(); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.ContentReleaseId, + entity.ExamTargetProfileVersionId, + entity.AudienceSegmentId + }); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.AudienceSegmentId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.ExamTargetProfileVersionId) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class ContentReleaseQuestionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("content_release_questions"); + builder.Property(entity => entity.QuestionType).HasMaxLength(50); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.ContentReleaseId, + entity.AudienceSegmentId, + entity.CurriculumNodeId, + entity.Ordinal + }).IsUnique(); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.ContentReleaseId, + entity.AudienceSegmentId, + entity.CurriculumNodeId, + entity.Status, + entity.Difficulty, + entity.QuestionType, + entity.Ordinal + }).HasDatabaseName("ix_content_release_question_candidates"); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ContentReleaseId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.AudienceSegmentId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.QuestionPlacementId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new + { + TenantId = entity.QuestionAssetOwnerTenantId, + entity.QuestionAssetId, + Id = entity.QuestionRevisionId + }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.AssessmentPolicyVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class CollectionReleaseConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("collection_releases"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.HasIndex(entity => new { entity.TenantId, entity.SourceCollectionId, entity.VersionNo }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ContentReleaseId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class CollectionReleaseQuestionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("collection_release_questions"); + builder.HasIndex(entity => new { entity.TenantId, entity.CollectionReleaseId, entity.SortOrder }) + .HasDatabaseName("ux_collection_release_question_order") + .IsUnique(); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.CollectionReleaseId, + entity.ContentReleaseQuestionId + }).HasDatabaseName("ux_collection_release_question_item").IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.CollectionReleaseId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ContentReleaseQuestionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class BlueprintReleaseConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("blueprint_releases"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.AssemblyRules).IsJson("{}"); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.HasIndex(entity => new { entity.TenantId, entity.SourceBlueprintId, entity.VersionNo }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ContentReleaseId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.CollectionReleaseId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ExamPaperSpecificationVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class PlatformContentPackageVersionConfiguration : + IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("platform_content_package_versions"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.PackageCode).HasMaxLength(100); + builder.Property(entity => entity.ContentHash).HasMaxLength(64); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.HasIndex(entity => new { entity.TenantId, entity.PackageCode, entity.VersionNo }).IsUnique(); + builder.HasIndex(entity => new { entity.TenantId, entity.ContentHash }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ContentReleaseId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class PlatformContentPackageCellProjectionConfiguration : + IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("platform_content_package_cell_projections"); + builder.Property(entity => entity.PackageCode).HasMaxLength(100); + builder.Property(entity => entity.CellId).HasMaxLength(100); + builder.Property(entity => entity.ContentHash).HasMaxLength(64); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.LastError).HasMaxLength(2000); + builder.HasIndex(entity => new { entity.PackageOwnerTenantId, entity.PackageCode, entity.CellId }) + .HasDatabaseName("ux_platform_package_cell_cursor").IsUnique(); + builder.HasIndex(entity => new { entity.PlatformContentPackageVersionId, entity.CellId }) + .HasDatabaseName("ix_platform_package_cell_version"); + builder.HasIndex(entity => new { entity.PackageOwnerTenantId, entity.PlatformContentPackageVersionId }) + .HasDatabaseName("ix_platform_package_cell_owner_version"); + builder.HasOne().WithMany() + .HasForeignKey(entity => new + { + TenantId = entity.PackageOwnerTenantId, + Id = entity.PlatformContentPackageVersionId + }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/ContentV2TargetConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/ContentV2TargetConfigurations.cs new file mode 100644 index 0000000..353fb73 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/ContentV2TargetConfigurations.cs @@ -0,0 +1,149 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Content; +using Tiku.Domain.Learning; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal sealed class TargetDimensionDefinitionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("target_dimension_definitions"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Code).HasMaxLength(100); + builder.Property(entity => entity.Name).HasMaxLength(200); + builder.HasIndex(entity => entity.Code).IsUnique(); + } +} + +internal sealed class TargetNodeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("target_nodes"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Code).HasMaxLength(100); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.Path).HasColumnType("ltree"); + builder.HasIndex(entity => new { entity.TargetDimensionDefinitionId, entity.Code }).IsUnique(); + builder.HasIndex(entity => entity.Path).HasMethod("gist"); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.TargetDimensionDefinitionId) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany().HasForeignKey(entity => entity.ParentId) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class ExamTargetProfileConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("exam_target_profiles"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Code).HasMaxLength(150); + builder.Property(entity => entity.Name).HasMaxLength(500); + builder.HasIndex(entity => new { entity.BusinessLineId, entity.Code }).IsUnique(); + builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessLineId) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { ExamTargetProfileId = entity.Id, Id = entity.CurrentVersionId }) + .HasPrincipalKey(entity => new { entity.ExamTargetProfileId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class ExamTargetProfileVersionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("exam_target_profile_versions"); + builder.HasAlternateKey(entity => new { entity.ExamTargetProfileId, entity.Id }); + builder.Property(entity => entity.DisplayName).HasMaxLength(500); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); + builder.HasIndex(entity => new { entity.ExamTargetProfileId, entity.VersionNo }).IsUnique(); + builder.HasOne().WithMany().HasForeignKey(entity => entity.ExamTargetProfileId) + .OnDelete(DeleteBehavior.Cascade); + } +} + +internal sealed class ExamTargetProfileValueConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("exam_target_profile_values"); + builder.HasIndex(entity => new + { + entity.ExamTargetProfileVersionId, + entity.TargetDimensionDefinitionId, + entity.TargetNodeId + }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.ExamTargetProfileVersionId).OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.TargetDimensionDefinitionId).OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany().HasForeignKey(entity => entity.TargetNodeId) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class BusinessTargetPolicyConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("business_target_policies"); + builder.ConfigureTimestamps(); + builder.HasIndex(entity => entity.BusinessLineId).IsUnique(); + builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessLineId) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { BusinessTargetPolicyId = entity.Id, Id = entity.CurrentVersionId }) + .HasPrincipalKey(entity => new { entity.BusinessTargetPolicyId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class BusinessTargetPolicyVersionConfiguration : + IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("business_target_policy_versions"); + builder.HasAlternateKey(entity => new { entity.BusinessTargetPolicyId, entity.Id }); + builder.Property(entity => entity.SelectionMode).HasSnakeCaseEnum(64); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); + builder.HasIndex(entity => new { entity.BusinessTargetPolicyId, entity.VersionNo }).IsUnique(); + builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessTargetPolicyId) + .OnDelete(DeleteBehavior.Cascade); + builder.ToTable(table => + { + table.HasCheckConstraint("ck_business_target_policy_active", "max_active_targets between 1 and 16"); + table.HasCheckConstraint("ck_business_target_policy_primary", "max_primary_targets between 0 and max_active_targets"); + table.HasCheckConstraint("ck_business_target_policy_alternate", "max_alternate_targets between 0 and max_active_targets"); + table.HasCheckConstraint("ck_business_target_policy_cooldown", "target_change_cooldown_days between 0 and 365"); + }); + } +} + +internal sealed class BusinessTargetDimensionRequirementConfiguration : + IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("business_target_dimension_requirements"); + builder.HasIndex(entity => new + { + entity.BusinessTargetPolicyVersionId, + entity.TargetDimensionDefinitionId + }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.BusinessTargetPolicyVersionId).OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.TargetDimensionDefinitionId).OnDelete(DeleteBehavior.Restrict); + builder.ToTable(table => table.HasCheckConstraint( + "ck_business_target_dimension_cardinality", "min_values >= 0 and max_values >= min_values")); + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/LearningAccessV2Configurations.cs b/Tiku.Infrastructure/Persistence/Configurations/LearningAccessV2Configurations.cs new file mode 100644 index 0000000..15eb9cd --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/LearningAccessV2Configurations.cs @@ -0,0 +1,303 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Content; +using Tiku.Domain.Identity; +using Tiku.Domain.Learning; +using Tiku.Domain.Tenancy; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal sealed class TenantBusinessLicenseConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("tenant_business_licenses"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.Version).IsConcurrencyToken().HasDefaultValue(1L); + builder.HasIndex(entity => new { entity.TenantId, entity.BusinessLineId }).IsUnique(); + builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessLineId) + .OnDelete(DeleteBehavior.Restrict); + builder.ToTable(table => table.HasCheckConstraint( + "ck_tenant_business_license_period", "ends_at is null or ends_at > starts_at")); + } +} + +internal sealed class TenantLicensedTargetConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("tenant_licensed_targets"); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.TenantBusinessLicenseId, + entity.ExamTargetProfileVersionId + }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.TenantBusinessLicenseId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.ExamTargetProfileVersionId) + .OnDelete(DeleteBehavior.Restrict); + builder.ToTable(table => table.HasCheckConstraint( + "ck_tenant_licensed_target_period", "ends_at is null or ends_at > starts_at")); + } +} + +internal sealed class ProductAccessManifestConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("product_access_manifests"); + builder.ConfigureTimestamps(); + builder.HasIndex(entity => new { entity.TenantId, entity.LearningProductId }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.LearningProductId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new + { + entity.TenantId, + ProductAccessManifestId = entity.Id, + Id = entity.CurrentVersionId + }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.ProductAccessManifestId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class ProductAccessManifestVersionConfiguration : + IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("product_access_manifest_versions"); + builder.HasAlternateKey(entity => new { entity.TenantId, entity.ProductAccessManifestId, entity.Id }); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); + builder.HasIndex(entity => new { entity.TenantId, entity.ProductAccessManifestId, entity.VersionNo }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.ToTable(table => + { + table.HasCheckConstraint("ck_product_manifest_active_targets", "max_active_targets between 1 and 16"); + table.HasCheckConstraint("ck_product_manifest_alternate_targets", "max_alternate_targets between 0 and max_active_targets"); + table.HasCheckConstraint("ck_product_manifest_daily_question_limit", + "daily_question_limit is null or daily_question_limit >= 0"); + table.HasCheckConstraint("ck_product_manifest_total_question_limit", + "total_question_limit is null or total_question_limit >= 0"); + }); + } +} + +internal sealed class ProductManifestReleaseConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("product_manifest_releases"); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.ProductAccessManifestVersionId, + entity.ContentOwnerTenantId, + entity.ContentReleaseId + }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { TenantId = entity.ContentOwnerTenantId, Id = entity.ContentReleaseId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new + { + TenantId = entity.PlatformContentPackageOwnerTenantId, + Id = entity.PlatformContentPackageVersionId + }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.ToTable(table => table.HasCheckConstraint( + "ck_product_manifest_release_platform_pair", + "(platform_content_package_owner_tenant_id is null) = (platform_content_package_version_id is null)")); + } +} + +internal sealed class ProductManifestTargetConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("product_manifest_targets"); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.ProductAccessManifestVersionId, + entity.ExamTargetProfileVersionId + }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.ExamTargetProfileVersionId) + .OnDelete(DeleteBehavior.Restrict); + builder.ToTable(table => table.HasCheckConstraint( + "ck_product_manifest_target_role", "may_be_primary or may_be_alternate")); + } +} + +internal sealed class ProductManifestResourceConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantOwned("product_manifest_resources"); + builder.Property(entity => entity.ResourceType).HasSnakeCaseEnum(); + builder.Property(entity => entity.DisplayName).HasMaxLength(300); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.ProductAccessManifestVersionId, + entity.ResourceType, + entity.ResourceId + }).HasDatabaseName("ux_product_manifest_resource").IsUnique(); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.ProductAccessManifestVersionId, + entity.SortOrder, + entity.Id + }).HasDatabaseName("ix_product_manifest_resource_order"); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + } +} + +internal sealed class StudentEntitlementConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("student_entitlements_v2"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.SourceType).HasMaxLength(100); + builder.Property(entity => entity.RevokedReason).HasMaxLength(1000); + builder.ToTable(table => table.HasCheckConstraint( + "ck_student_entitlements_v2_used_question_count", "used_question_count >= 0")); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.UserId, + entity.Status, + entity.StartsAt, + entity.EndsAt + }); + builder.HasIndex(entity => new { entity.TenantId, entity.SourceType, entity.SourceId }) + .HasFilter("source_id is not null"); + builder.HasOne().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.LearningProductId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.ToTable(table => table.HasCheckConstraint( + "ck_student_entitlement_v2_period", "ends_at is null or ends_at > starts_at")); + } +} + +internal sealed class StudentTargetSelectionHistoryConfiguration : + IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("student_target_selection_history"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Role).HasSnakeCaseEnum(); + builder.Property(entity => entity.SourceType).HasMaxLength(100); + builder.Property(entity => entity.Reason).HasMaxLength(1000); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.UserId, + entity.BusinessLineId, + entity.ExamTargetProfileVersionId + }).IsUnique().HasFilter("is_current"); + builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.BusinessLineId }) + .IsUnique() + .HasDatabaseName("ux_student_target_primary") + .HasFilter("is_current and role = 'primary'"); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.UserId, + entity.BusinessLineId, + entity.EffectiveAt + }).HasDatabaseName("ix_student_target_selection_history"); + builder.HasIndex(entity => new { entity.TenantId, entity.SourceType, entity.SourceId }); + builder.HasOne().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessLineId) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.ExamTargetProfileVersionId) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.EntitlementId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.ToTable(table => table.HasCheckConstraint( + "ck_student_target_selection_source", + "(source_type = 'student_entitlement' and entitlement_id = source_id) or " + + "(source_type = 'class_assignment_grant' and entitlement_id is null)")); + } +} + +internal sealed class ClassAssignmentGrantConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("class_assignment_grants"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.ResourceType).HasSnakeCaseEnum(); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.RevokedReason).HasMaxLength(1000); + builder.HasIndex(entity => new { entity.TenantId, entity.ClassId, entity.Status, entity.StartsAt }); + builder.HasIndex(entity => new { entity.TenantId, entity.ResourceType, entity.ResourceId, entity.Status }); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ClassId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, Id = entity.ProductAccessManifestVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.ToTable(table => table.HasCheckConstraint( + "ck_class_assignment_grant_period", "ends_at is null or ends_at > starts_at")); + } +} + +internal sealed class EffectiveAccessProjectionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("effective_access_projections"); + builder.Property(entity => entity.AlternateProfileVersionIds).HasColumnType("uuid[]"); + builder.Property(entity => entity.ContentReleaseIds).HasColumnType("uuid[]"); + builder.Property(entity => entity.AudienceSegmentIds).HasColumnType("uuid[]"); + builder.Property(entity => entity.ManifestVersionIds).HasColumnType("uuid[]"); + builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.BusinessLineId }).IsUnique(); + builder.HasIndex(entity => new { entity.TenantId, entity.ValidUntil }); + builder.HasOne().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany().HasForeignKey(entity => entity.BusinessLineId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs index a3f7ef9..e9fe543 100644 --- a/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs +++ b/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs @@ -65,6 +65,12 @@ internal sealed class PracticeSessionQuestionConfiguration : IEntityTypeConfigur builder.ConfigureTenantEntity("practice_session_questions"); builder.HasAlternateKey(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Id }); builder.HasIndex(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Position }).IsUnique(); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.PracticeSessionId, + entity.ContentReleaseQuestionId + }).IsUnique().HasFilter("content_release_question_id is not null"); builder.Property(entity => entity.Score).HasPrecision(8, 2); builder.Property(entity => entity.QuestionType).HasMaxLength(50).HasDefaultValue("choice"); builder.Property(entity => entity.TypeLabelSnapshot).HasMaxLength(100); @@ -134,6 +140,13 @@ internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration new + { + entity.TenantId, + entity.UserId, + entity.PracticeSessionId, + entity.IdempotencyKey + }).IsUnique().HasDatabaseName("ux_answer_records_idempotency"); + builder.HasIndex(entity => new { entity.TenantId, entity.UserId, @@ -176,45 +189,6 @@ internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ConfigureEntity("answer_submission_idempotencies"); - builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id }); - 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.IdempotencyKey - }).IsUnique().HasDatabaseName("ux_answer_submission_idempotency"); - builder.HasIndex(entity => new { entity.TenantId, entity.AnswerRecordId }); - - builder.HasOne().WithMany() - .HasForeignKey(entity => entity.UserId) - .OnDelete(DeleteBehavior.Cascade); - builder.HasOne().WithMany() - .HasForeignKey(entity => new - { - entity.TenantId, - entity.UserId, - entity.PracticeSessionId - }) - .HasPrincipalKey(entity => new - { - entity.TenantId, - entity.UserId, - entity.Id - }) - .OnDelete(DeleteBehavior.Cascade); - } -} - internal sealed class CurrentAnswerConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) @@ -336,6 +310,45 @@ internal sealed class WrongQuestionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("wrong_questions_v2"); + builder.HasKey(entity => new + { + entity.TenantId, + entity.UserId, + entity.QuestionAssetOwnerTenantId, + entity.QuestionAssetId + }); + builder.Property(entity => entity.WrongCount).HasDefaultValue(1); + builder.Property(entity => entity.LastWrongAt).HasDefaultValueSql("now()"); + builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.LastWrongAt }); + builder.HasOne().WithMany().HasForeignKey(entity => entity.TenantId) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany().HasForeignKey(entity => entity.UserId) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new + { + TenantId = entity.QuestionAssetOwnerTenantId, + Id = entity.QuestionAssetId + }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new + { + TenantId = entity.QuestionAssetOwnerTenantId, + entity.QuestionAssetId, + Id = entity.LastQuestionRevisionId + }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + internal sealed class RecentPracticeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) diff --git a/Tiku.Infrastructure/Persistence/Configurations/LearningOutboxConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/LearningOutboxConfigurations.cs index 88930cb..a0ab839 100644 --- a/Tiku.Infrastructure/Persistence/Configurations/LearningOutboxConfigurations.cs +++ b/Tiku.Infrastructure/Persistence/Configurations/LearningOutboxConfigurations.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Tiku.Domain.Learning; +using Tiku.Domain.Tenancy; namespace Tiku.Infrastructure.Persistence.Configurations; @@ -8,14 +9,26 @@ internal sealed class LearningOutboxMessageConfiguration : IEntityTypeConfigurat { public void Configure(EntityTypeBuilder builder) { - builder.ConfigureTenantEntity("learning_outbox_messages"); + builder.ToTable("learning_outbox_messages"); + builder.HasKey(entity => new { entity.OccurredAt, entity.Id }); + builder.Property(entity => entity.Id).HasDefaultValueSql("gen_random_uuid()"); + builder.HasAlternateKey(entity => new { entity.TenantId, entity.OccurredAt, entity.Id }); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.TenantId) + .OnDelete(DeleteBehavior.Cascade); builder.Property(entity => entity.EventType).HasMaxLength(100); builder.Property(entity => entity.Payload).IsJson("{}"); builder.Property(entity => entity.OccurredAt).HasDefaultValueSql("now()"); builder.Property(entity => entity.AvailableAt).HasDefaultValueSql("now()"); builder.Property(entity => entity.LockedBy).HasMaxLength(200); builder.Property(entity => entity.LastError).HasMaxLength(2000); - builder.HasIndex(entity => new { entity.TenantId, entity.EventType, entity.AggregateId }).IsUnique(); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.EventType, + entity.AggregateId, + entity.OccurredAt + }).IsUnique(); builder.HasIndex(entity => new { entity.AvailableAt, diff --git a/Tiku.Infrastructure/Persistence/Configurations/TaxonomyConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/TaxonomyConfigurations.cs deleted file mode 100644 index 6d02757..0000000 --- a/Tiku.Infrastructure/Persistence/Configurations/TaxonomyConfigurations.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Tiku.Domain.Catalog; -using Tiku.Domain.QuestionBanks; - -namespace Tiku.Infrastructure.Persistence.Configurations; - -internal sealed class TaxonomyNodeConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ConfigureTenantEntity("taxonomy_nodes"); - builder.ConfigureTimestamps(); - builder.Property(entity => entity.NodeType).HasSnakeCaseEnum(); - builder.Property(entity => entity.Code).HasMaxLength(100); - builder.Property(entity => entity.Name).HasMaxLength(300); - builder.Property(entity => entity.Path).HasColumnType("ltree"); - builder.Property(entity => entity.Metadata).IsJson("{}"); - builder.HasIndex(entity => new { entity.TenantId, entity.Code }).IsUnique(); - builder.HasOne().WithMany() - .HasForeignKey(entity => new { TenantId = entity.ParentOwnerTenantId, Id = entity.ParentId }) - .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) - .OnDelete(DeleteBehavior.Restrict); - builder.ToTable(table => table.HasCheckConstraint( - "ck_taxonomy_nodes_parent_pair", - "(parent_owner_tenant_id is null) = (parent_id is null)")); - } -} - -internal sealed class QuestionTaxonomyAssignmentConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ConfigureEntity("question_taxonomy_assignments"); - builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id }); - builder.HasIndex(entity => new - { - entity.TenantId, - entity.QuestionId, - entity.TaxonomyOwnerTenantId, - entity.TaxonomyNodeId - }).IsUnique(); - builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); - builder.HasOne().WithMany() - .HasForeignKey(entity => new { entity.TenantId, entity.QuestionId }) - .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) - .OnDelete(DeleteBehavior.Cascade); - builder.HasOne().WithMany() - .HasForeignKey(entity => new { TenantId = entity.TaxonomyOwnerTenantId, Id = entity.TaxonomyNodeId }) - .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) - .OnDelete(DeleteBehavior.Restrict); - } -} \ No newline at end of file diff --git a/Tiku.Infrastructure/Persistence/Migrations/20260805063748_PartitionLearningOutboxByMonth.Designer.cs b/Tiku.Infrastructure/Persistence/Migrations/20260805063748_PartitionLearningOutboxByMonth.Designer.cs new file mode 100644 index 0000000..2c7233e --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260805063748_PartitionLearningOutboxByMonth.Designer.cs @@ -0,0 +1,22589 @@ +// +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("20260805063748_PartitionLearningOutboxByMonth")] + partial class PartitionLearningOutboxByMonth + { + /// + 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("LearningProductId") + .HasColumnType("uuid") + .HasColumnName("learning_product_id"); + + 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", "LearningProductId") + .HasDatabaseName("ix_entitlements_tenant_id_learning_product_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("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", "Revision") + .IsUnique() + .HasDatabaseName("ux_answer_records_session_question_revision"); + + b.ToTable("answer_records", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.AnswerSubmissionIdempotency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerRecordId") + .HasColumnType("uuid") + .HasColumnName("answer_record_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("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_answer_submission_idempotencies"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_answer_submission_idempotencies_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_answer_submission_idempotencies_user_id"); + + b.HasIndex("TenantId", "AnswerRecordId") + .HasDatabaseName("ix_answer_submission_idempotencies_tenant_id_answer_record_id"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ux_answer_submission_idempotency"); + + b.ToTable("answer_submission_idempotencies", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.BusinessLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RegionAccessStrategy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("region_access_strategy"); + + b.Property("RequiresBaseRegion") + .HasColumnType("boolean") + .HasColumnName("requires_base_region"); + + b.Property("TargetRegionCooldownDays") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30) + .HasColumnName("target_region_cooldown_days"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_business_lines"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_business_lines_code"); + + b.ToTable("business_lines", null, t => + { + t.HasCheckConstraint("ck_business_lines_target_region_cooldown_days", "target_region_cooldown_days between 0 and 365"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ClassContentAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ClassId") + .HasColumnType("uuid") + .HasColumnName("class_id"); + + b.Property("ContentSliceId") + .HasColumnType("uuid") + .HasColumnName("content_slice_id"); + + b.Property("ContentSliceOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("content_slice_owner_tenant_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("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_type"); + + b.Property("RevokedBy") + .HasColumnType("uuid") + .HasColumnName("revoked_by"); + + b.Property("RevokedReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("revoked_reason"); + + 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_class_content_assignments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_class_content_assignments_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_class_content_assignments_created_by"); + + b.HasIndex("RevokedBy") + .HasDatabaseName("ix_class_content_assignments_revoked_by"); + + b.HasIndex("ContentSliceOwnerTenantId", "ContentSliceId") + .HasDatabaseName("ix_class_content_assignments_content_slice_owner_tenant_id_con~"); + + b.HasIndex("TenantId", "ClassId", "Status", "StartsAt") + .HasDatabaseName("ix_class_content_assignments_tenant_id_class_id_status_starts_~"); + + b.HasIndex("TenantId", "ResourceType", "ResourceId", "Status") + .HasDatabaseName("ix_class_content_assignments_tenant_id_resource_type_resource_~"); + + b.ToTable("class_content_assignments", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ContentSlice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ContentVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("content_version"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("MarketRegionId") + .HasColumnType("uuid") + .HasColumnName("market_region_id"); + + b.Property("RegionScope") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("region_scope"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_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_content_slices"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_slices_tenant_id_id"); + + b.HasIndex("MarketRegionId") + .HasDatabaseName("ix_content_slices_market_region_id"); + + b.HasIndex("BusinessLineId", "RegionScope", "MarketRegionId") + .HasDatabaseName("ix_content_slices_business_line_id_region_scope_market_region_~"); + + b.HasIndex("TenantId", "ResourceType", "ResourceId", "Status") + .HasDatabaseName("ix_content_slices_tenant_id_resource_type_resource_id_status"); + + b.ToTable("content_slices", null, t => + { + t.HasCheckConstraint("ck_content_slices_region_scope", "(region_scope = 'national' and market_region_id is null) or (region_scope = 'region' and market_region_id is not null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.CurrentAnswer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerRecordId") + .HasColumnType("uuid") + .HasColumnName("answer_record_id"); + + b.Property("ClientSequence") + .HasColumnType("bigint") + .HasColumnName("client_sequence"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("Revision") + .HasColumnType("integer") + .HasColumnName("revision"); + + b.Property("SessionQuestionId") + .HasColumnType("uuid") + .HasColumnName("session_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("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_current_answers"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_current_answers_tenant_id_id"); + + b.HasIndex("TenantId", "AnswerRecordId") + .HasDatabaseName("ix_current_answers_tenant_id_answer_record_id"); + + b.HasIndex("TenantId", "PracticeSessionId", "SessionQuestionId") + .HasDatabaseName("ix_current_answers_tenant_id_practice_session_id_session_quest~"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "SessionQuestionId") + .IsUnique() + .HasDatabaseName("ux_current_answers_session_question"); + + b.ToTable("current_answers", (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.LearningAccessVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentVersion") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("content_version"); + + b.Property("GrantVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("grant_version"); + + b.Property("StrongRevocationVersion") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("strong_revocation_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_learning_access_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_access_versions_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_learning_access_versions_user_id"); + + b.HasIndex("TenantId", "UserId") + .IsUnique() + .HasDatabaseName("ix_learning_access_versions_tenant_id_user_id"); + + b.ToTable("learning_access_versions", (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.LearningOutboxMessage", b => + { + b.Property("OccurredAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_at") + .HasDefaultValueSql("now()"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AggregateId") + .HasColumnType("uuid") + .HasColumnName("aggregate_id"); + + b.Property("AttemptCount") + .HasColumnType("integer") + .HasColumnName("attempt_count"); + + b.Property("AvailableAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("available_at") + .HasDefaultValueSql("now()"); + + b.Property("DeadLetteredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("dead_lettered_at"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("event_type"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .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("Payload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at"); + + b.Property("SchemaVersion") + .HasColumnType("integer") + .HasColumnName("schema_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("OccurredAt", "Id") + .HasName("pk_learning_outbox_messages"); + + b.HasAlternateKey("TenantId", "OccurredAt", "Id") + .HasName("ak_learning_outbox_messages_tenant_id_occurred_at_id"); + + b.HasIndex("AvailableAt", "LockExpiresAt", "OccurredAt", "Id") + .HasDatabaseName("ix_learning_outbox_pending") + .HasFilter("processed_at IS NULL AND dead_lettered_at IS NULL"); + + b.HasIndex("TenantId", "EventType", "AggregateId", "OccurredAt") + .IsUnique() + .HasDatabaseName("ix_learning_outbox_messages_tenant_id_event_type_aggregate_id_~"); + + b.ToTable("learning_outbox_messages", null, t => + { + t.HasCheckConstraint("ck_learning_outbox_messages_attempt_count", "attempt_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProduct", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + 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_learning_products"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_products_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_learning_products_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_learning_products_tenant_id_code"); + + b.ToTable("learning_products", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProductScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentSliceId") + .HasColumnType("uuid") + .HasColumnName("content_slice_id"); + + b.Property("ContentSliceOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("content_slice_owner_tenant_id"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasColumnName("product_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_learning_product_scopes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_product_scopes_tenant_id_id"); + + b.HasIndex("ContentSliceOwnerTenantId", "ContentSliceId") + .HasDatabaseName("ix_learning_product_scopes_content_slice_owner_tenant_id_conte~"); + + b.HasIndex("TenantId", "ProductId", "ContentSliceId") + .IsUnique() + .HasDatabaseName("ix_learning_product_scopes_tenant_id_product_id_content_slice_~"); + + b.ToTable("learning_product_scopes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProjectionReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("OutboxMessageId") + .HasColumnType("uuid") + .HasColumnName("outbox_message_id"); + + b.Property("ProcessedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at") + .HasDefaultValueSql("now()"); + + b.Property("ProjectionName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("projection_name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_learning_projection_receipts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_projection_receipts_tenant_id_id"); + + b.HasIndex("TenantId", "OutboxMessageId", "ProjectionName") + .IsUnique() + .HasDatabaseName("ix_learning_projection_receipts_tenant_id_outbox_message_id_pr~"); + + b.ToTable("learning_projection_receipts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.MarketRegion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("ParentCode") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("parent_code"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_market_regions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_market_regions_code"); + + b.ToTable("market_regions", (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("AccessClassAssignmentId") + .HasColumnType("uuid") + .HasColumnName("access_class_assignment_id"); + + b.Property("AccessEntitlementId") + .HasColumnType("uuid") + .HasColumnName("access_entitlement_id"); + + b.Property("AccessGrantVersion") + .HasColumnType("bigint") + .HasColumnName("access_grant_version"); + + 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("AuthorizationExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("authorization_expires_at"); + + 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("StrongRevocationVersion") + .HasColumnType("bigint") + .HasColumnName("strong_revocation_version"); + + 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", "StrongRevocationVersion") + .HasDatabaseName("ix_practice_sessions_tenant_id_user_id_strong_revocation_versi~"); + + 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("DifficultySnapshot") + .HasColumnType("integer") + .HasColumnName("difficulty_snapshot"); + + b.Property("GradingRulesSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("grading_rules_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.StudentTargetRegionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ChangedBy") + .HasColumnType("uuid") + .HasColumnName("changed_by"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("effective_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("IsCurrent") + .HasColumnType("boolean") + .HasColumnName("is_current"); + + b.Property("MarketRegionId") + .HasColumnType("uuid") + .HasColumnName("market_region_id"); + + b.Property("Reason") + .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.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_student_target_region_history"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_target_region_history_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_student_target_region_history_business_line_id"); + + b.HasIndex("ChangedBy") + .HasDatabaseName("ix_student_target_region_history_changed_by"); + + b.HasIndex("MarketRegionId") + .HasDatabaseName("ix_student_target_region_history_market_region_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_target_region_history_user_id"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ux_student_target_region_current") + .HasFilter("is_current"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId", "EffectiveAt") + .HasDatabaseName("ix_student_target_region_history"); + + b.ToTable("student_target_region_history", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowsAnyTargetRegion") + .HasColumnType("boolean") + .HasColumnName("allows_any_target_region"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("IncludesNational") + .HasColumnType("boolean") + .HasColumnName("includes_national"); + + b.Property("IsPrimary") + .HasColumnType("boolean") + .HasColumnName("is_primary"); + + 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.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_tenant_learning_licenses"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_learning_licenses_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_tenant_learning_licenses_business_line_id"); + + b.HasIndex("TenantId") + .IsUnique() + .HasDatabaseName("ix_tenant_learning_licenses_tenant_id") + .HasFilter("is_primary and status = 'active'"); + + b.HasIndex("TenantId", "BusinessLineId", "Status") + .HasDatabaseName("ix_tenant_learning_licenses_tenant_id_business_line_id_status"); + + b.ToTable("tenant_learning_licenses", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicenseRegion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("IsBaseRegion") + .HasColumnType("boolean") + .HasColumnName("is_base_region"); + + b.Property("LicenseId") + .HasColumnType("uuid") + .HasColumnName("license_id"); + + b.Property("MarketRegionId") + .HasColumnType("uuid") + .HasColumnName("market_region_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_tenant_learning_license_regions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_learning_license_regions_tenant_id_id"); + + b.HasIndex("MarketRegionId") + .HasDatabaseName("ix_tenant_learning_license_regions_market_region_id"); + + b.HasIndex("TenantId", "LicenseId", "MarketRegionId") + .IsUnique() + .HasDatabaseName("ix_tenant_learning_license_regions_tenant_id_license_id_market~"); + + b.ToTable("tenant_learning_license_regions", (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.PlatformApprovalPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AlwaysRequireApproval") + .HasColumnType("boolean") + .HasColumnName("always_require_approval"); + + b.Property("AmountThresholdCents") + .HasColumnType("integer") + .HasColumnName("amount_threshold_cents"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .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("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("ExpiresAfterHours") + .HasColumnType("integer") + .HasColumnName("expires_after_hours"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RequiredPermission") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("required_permission"); + + 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_platform_approval_policies"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_approval_policies_code"); + + b.ToTable("platform_approval_policies", null, t => + { + t.HasCheckConstraint("ck_platform_approval_policies_expiry", "expires_after_hours between 1 and 720"); + + t.HasCheckConstraint("ck_platform_approval_policies_threshold", "amount_threshold_cents is null or amount_threshold_cents > 0"); + + t.HasCheckConstraint("ck_platform_approval_policies_version", "version > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformApprovalRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("CommandType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("command_type"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("uuid") + .HasColumnName("concurrency_stamp"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DecidedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("decided_at"); + + b.Property("DecidedBy") + .HasColumnType("uuid") + .HasColumnName("decided_by"); + + b.Property("DecisionReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("decision_reason"); + + b.Property("Error") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("error"); + + b.Property("ExecutedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_at"); + + 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("PolicyCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("policy_code"); + + b.Property("PolicyVersion") + .HasColumnType("integer") + .HasColumnName("policy_version"); + + b.Property("RequestHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("request_hash"); + + b.Property("RequestNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("request_no"); + + b.Property("RequestReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("request_reason"); + + b.Property("RequestSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("request_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RequestedBy") + .HasColumnType("uuid") + .HasColumnName("requested_by"); + + b.Property("RequiredPermission") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("required_permission"); + + b.Property("ResultSnapshot") + .HasColumnType("jsonb") + .HasColumnName("result_snapshot"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TargetId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("target_id"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("target_type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_approval_requests"); + + b.HasIndex("DecidedBy") + .HasDatabaseName("ix_platform_approval_requests_decided_by"); + + b.HasIndex("RequestNo") + .IsUnique() + .HasDatabaseName("ix_platform_approval_requests_request_no"); + + b.HasIndex("RequestedBy", "CommandType", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_approval_requests_requested_by_command_type_idempo~"); + + b.HasIndex("Status", "ExpiresAt", "CreatedAt") + .HasDatabaseName("ix_platform_approval_requests_status_expires_at_created_at"); + + b.ToTable("platform_approval_requests", (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.PlatformConfigurationDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowRuntimeManagement") + .HasColumnType("boolean") + .HasColumnName("allow_runtime_management"); + + 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("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("IsSensitive") + .HasColumnType("boolean") + .HasColumnName("is_sensitive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("ValidationSchema") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("validation_schema") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ValueType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("value_type"); + + b.HasKey("Id") + .HasName("pk_platform_configuration_definitions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_configuration_definitions_code"); + + b.HasIndex("Category", "Code") + .HasDatabaseName("ix_platform_configuration_definitions_category_code"); + + b.ToTable("platform_configuration_definitions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformConfigurationVersion", 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("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id"); + + b.Property("Environment") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)") + .HasColumnName("environment"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("PublishedBy") + .HasColumnType("uuid") + .HasColumnName("published_by"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("RolledBackFromVersionId") + .HasColumnType("uuid") + .HasColumnName("rolled_back_from_version_id"); + + 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.Property("Value") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("value") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_platform_configuration_versions"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_platform_configuration_versions_created_by"); + + b.HasIndex("PublishedBy") + .HasDatabaseName("ix_platform_configuration_versions_published_by"); + + b.HasIndex("RolledBackFromVersionId") + .HasDatabaseName("ix_platform_configuration_versions_rolled_back_from_version_id"); + + b.HasIndex("DefinitionId", "Environment", "Status") + .HasDatabaseName("ix_platform_configuration_versions_definition_id_environment_s~"); + + b.HasIndex("DefinitionId", "Environment", "Version") + .IsUnique() + .HasDatabaseName("ix_platform_configuration_versions_definition_id_environment_v~"); + + b.ToTable("platform_configuration_versions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformNotificationDelivery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("body"); + + 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("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("LastError") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("last_error"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RecipientRoleCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("recipient_role_code"); + + b.Property("RecipientUserId") + .HasColumnType("uuid") + .HasColumnName("recipient_user_id"); + + 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("Subject") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("subject"); + + b.Property("TemplateId") + .HasColumnType("uuid") + .HasColumnName("template_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_notification_deliveries"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_platform_notification_deliveries_created_by"); + + b.HasIndex("RecipientUserId") + .HasDatabaseName("ix_platform_notification_deliveries_recipient_user_id"); + + b.HasIndex("Status", "CreatedAt") + .HasDatabaseName("ix_platform_notification_deliveries_status_created_at"); + + b.HasIndex("TemplateId", "RecipientUserId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_notification_deliveries_template_id_recipient_user~"); + + b.ToTable("platform_notification_deliveries", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformNotificationTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BodyTemplate") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("body_template"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("channel"); + + 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("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("SubjectTemplate") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("subject_template"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("variables") + .HasDefaultValueSql("'[]'::jsonb"); + + b.HasKey("Id") + .HasName("pk_platform_notification_templates"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_notification_templates_code"); + + b.ToTable("platform_notification_templates", (string)null); + }); + + 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("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + 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("QuestionType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("choice") + .HasColumnName("question_type"); + + 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("Tags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("tags") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TypeLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label"); + + 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("CellId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("cell-01") + .HasColumnName("cell_id"); + + 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.HasIndex("CellId", "Status") + .HasDatabaseName("ix_tenants_cell_id_status"); + + 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"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "LearningProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_entitlements_learning_products_tenant_id_learning_product_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.AnswerSubmissionIdempotency", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_answer_submission_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_answer_submission_idempotencies_practice_sessions_tenant_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ClassContentAssignment", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_class_content_assignments_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RevokedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_class_content_assignments_users_revoked_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_content_assignments_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ContentSlice", null) + .WithMany() + .HasForeignKey("ContentSliceOwnerTenantId", "ContentSliceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_class_content_assignments_content_slices_content_slice_owne~"); + + b.HasOne("Tiku.Domain.Tenancy.TenantClass", null) + .WithMany() + .HasForeignKey("TenantId", "ClassId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_content_assignments_tenant_classes_tenant_id_class_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ContentSlice", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_slices_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Learning.MarketRegion", null) + .WithMany() + .HasForeignKey("MarketRegionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_slices_market_regions_market_region_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_slices_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.CurrentAnswer", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_current_answers_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.AnswerRecord", null) + .WithMany() + .HasForeignKey("TenantId", "AnswerRecordId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_current_answers_answer_records_tenant_id_answer_record_id"); + + b.HasOne("Tiku.Domain.Learning.PracticeSessionQuestion", null) + .WithMany() + .HasForeignKey("TenantId", "PracticeSessionId", "SessionQuestionId") + .HasPrincipalKey("TenantId", "PracticeSessionId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_current_answers_practice_session_questions_tenant_id_practi~"); + }); + + 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.LearningAccessVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_access_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_access_versions_users_user_id"); + }); + + 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.LearningOutboxMessage", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_outbox_messages_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProduct", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_learning_products_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_products_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProductScope", b => + { + b.HasOne("Tiku.Domain.Learning.ContentSlice", null) + .WithMany() + .HasForeignKey("ContentSliceOwnerTenantId", "ContentSliceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_learning_product_scopes_content_slices_content_slice_owner_~"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "ProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_product_scopes_learning_products_tenant_id_product~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProjectionReceipt", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_projection_receipts_tenants_tenant_id"); + }); + + 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.StudentTargetRegionHistory", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_business_lines_business_line_~"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ChangedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_student_target_region_history_users_changed_by"); + + b.HasOne("Tiku.Domain.Learning.MarketRegion", null) + .WithMany() + .HasForeignKey("MarketRegionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_market_regions_market_region_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicense", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_learning_licenses_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_learning_licenses_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicenseRegion", b => + { + b.HasOne("Tiku.Domain.Learning.MarketRegion", null) + .WithMany() + .HasForeignKey("MarketRegionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_learning_license_regions_market_regions_market_regio~"); + + b.HasOne("Tiku.Domain.Learning.TenantLearningLicense", null) + .WithMany() + .HasForeignKey("TenantId", "LicenseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_learning_license_regions_tenant_learning_licenses_te~"); + }); + + 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.PlatformApprovalRequest", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("DecidedBy") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_approval_requests_users_decided_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RequestedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_approval_requests_users_requested_by"); + }); + + 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.PlatformConfigurationVersion", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_configuration_versions_users_created_by"); + + b.HasOne("Tiku.Domain.Platform.PlatformConfigurationDefinition", null) + .WithMany() + .HasForeignKey("DefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_configuration_versions_platform_configuration_defi~"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("PublishedBy") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_configuration_versions_users_published_by"); + + b.HasOne("Tiku.Domain.Platform.PlatformConfigurationVersion", null) + .WithMany() + .HasForeignKey("RolledBackFromVersionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_configuration_versions_platform_configuration_vers~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformNotificationDelivery", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_notification_deliveries_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RecipientUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_notification_deliveries_users_recipient_user_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformNotificationTemplate", null) + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_notification_deliveries_platform_notification_temp~"); + }); + + 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/20260805063748_PartitionLearningOutboxByMonth.cs b/Tiku.Infrastructure/Persistence/Migrations/20260805063748_PartitionLearningOutboxByMonth.cs new file mode 100644 index 0000000..4dd82a8 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260805063748_PartitionLearningOutboxByMonth.cs @@ -0,0 +1,74 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Tiku.Infrastructure.Persistence.Migrations +{ + /// + public partial class PartitionLearningOutboxByMonth : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "ak_learning_outbox_messages_tenant_id_id", + table: "learning_outbox_messages"); + + migrationBuilder.DropPrimaryKey( + name: "pk_learning_outbox_messages", + table: "learning_outbox_messages"); + + migrationBuilder.DropIndex( + name: "ix_learning_outbox_messages_tenant_id_event_type_aggregate_id", + table: "learning_outbox_messages"); + + migrationBuilder.AddUniqueConstraint( + name: "ak_learning_outbox_messages_tenant_id_occurred_at_id", + table: "learning_outbox_messages", + columns: new[] { "tenant_id", "occurred_at", "id" }); + + migrationBuilder.AddPrimaryKey( + name: "pk_learning_outbox_messages", + table: "learning_outbox_messages", + columns: new[] { "occurred_at", "id" }); + + migrationBuilder.CreateIndex( + name: "ix_learning_outbox_messages_tenant_id_event_type_aggregate_id_~", + table: "learning_outbox_messages", + columns: new[] { "tenant_id", "event_type", "aggregate_id", "occurred_at" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "ak_learning_outbox_messages_tenant_id_occurred_at_id", + table: "learning_outbox_messages"); + + migrationBuilder.DropPrimaryKey( + name: "pk_learning_outbox_messages", + table: "learning_outbox_messages"); + + migrationBuilder.DropIndex( + name: "ix_learning_outbox_messages_tenant_id_event_type_aggregate_id_~", + table: "learning_outbox_messages"); + + migrationBuilder.AddUniqueConstraint( + name: "ak_learning_outbox_messages_tenant_id_id", + table: "learning_outbox_messages", + columns: new[] { "tenant_id", "id" }); + + migrationBuilder.AddPrimaryKey( + name: "pk_learning_outbox_messages", + table: "learning_outbox_messages", + column: "id"); + + migrationBuilder.CreateIndex( + name: "ix_learning_outbox_messages_tenant_id_event_type_aggregate_id", + table: "learning_outbox_messages", + columns: new[] { "tenant_id", "event_type", "aggregate_id" }, + unique: true); + } + } +} diff --git a/Tiku.Infrastructure/Persistence/Migrations/20260805091745_ContentDomainV2Working.Designer.cs b/Tiku.Infrastructure/Persistence/Migrations/20260805091745_ContentDomainV2Working.Designer.cs new file mode 100644 index 0000000..1abc581 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260805091745_ContentDomainV2Working.Designer.cs @@ -0,0 +1,26732 @@ +// +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("20260805091745_ContentDomainV2Working")] + partial class ContentDomainV2Working + { + /// + 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("LearningProductId") + .HasColumnType("uuid") + .HasColumnName("learning_product_id"); + + 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", "LearningProductId") + .HasDatabaseName("ix_entitlements_tenant_id_learning_product_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.AssessmentPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_assessment_policies"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_assessment_policies_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_assessment_policies_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_assessment_policies_tenant_id_code"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_assessment_policies_tenant_id_id_current_version_id"); + + b.ToTable("assessment_policies", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentPolicyVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_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("DefaultScore") + .HasColumnType("numeric") + .HasColumnName("default_score"); + + b.Property("GradingMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("grading_mode"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("RoundingScale") + .HasColumnType("integer") + .HasColumnName("rounding_scale"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_assessment_policy_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_assessment_policy_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "AssessmentPolicyId", "Id") + .HasName("ak_assessment_policy_versions_tenant_id_assessment_policy_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_assessment_policy_versions_tenant_id_assessment_policy_id_v~"); + + b.ToTable("assessment_policy_versions", null, t => + { + t.HasCheckConstraint("ck_assessment_policy_default_score", "default_score > 0"); + + t.HasCheckConstraint("ck_assessment_policy_rounding", "rounding_scale between 0 and 6"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentScoreBand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("label"); + + b.Property("MaxScore") + .HasColumnType("numeric") + .HasColumnName("max_score"); + + b.Property("MinScore") + .HasColumnType("numeric") + .HasColumnName("min_score"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_assessment_score_bands"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_assessment_score_bands_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "MinScore", "MaxScore") + .IsUnique() + .HasDatabaseName("ix_assessment_score_bands_tenant_id_assessment_policy_version_~"); + + b.ToTable("assessment_score_bands", null, t => + { + t.HasCheckConstraint("ck_assessment_score_band", "max_score >= min_score"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("RuleHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("rule_hash"); + + b.Property("SegmentOrdinal") + .HasColumnType("integer") + .HasColumnName("segment_ordinal"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_audience_segments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_audience_segments_tenant_id_id"); + + b.HasIndex("TenantId", "ContentReleaseId", "RuleHash") + .IsUnique() + .HasDatabaseName("ix_audience_segments_tenant_id_content_release_id_rule_hash"); + + b.HasIndex("TenantId", "ContentReleaseId", "SegmentOrdinal") + .IsUnique() + .HasDatabaseName("ix_audience_segments_tenant_id_content_release_id_segment_ordi~"); + + b.ToTable("audience_segments", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegmentMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AudienceSegmentId") + .HasColumnType("uuid") + .HasColumnName("audience_segment_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_audience_segment_members"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_audience_segment_members_tenant_id_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_audience_segment_members_exam_target_profile_version_id"); + + b.HasIndex("TenantId", "AudienceSegmentId") + .HasDatabaseName("ix_audience_segment_members_tenant_id_audience_segment_id"); + + b.HasIndex("TenantId", "ContentReleaseId", "AudienceSegmentId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_audience_segment_members_tenant_id_content_release_id_audie~"); + + b.HasIndex("TenantId", "ContentReleaseId", "ExamTargetProfileVersionId", "AudienceSegmentId") + .HasDatabaseName("ix_audience_segment_members_tenant_id_content_release_id_exam_~"); + + b.ToTable("audience_segment_members", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BlueprintRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssemblyRules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("assembly_rules") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CollectionReleaseId") + .HasColumnType("uuid") + .HasColumnName("collection_release_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ExamPaperSpecificationVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_paper_specification_version_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("SourceBlueprintId") + .HasColumnType("uuid") + .HasColumnName("source_blueprint_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.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_blueprint_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_blueprint_releases_tenant_id_id"); + + b.HasIndex("TenantId", "CollectionReleaseId") + .HasDatabaseName("ix_blueprint_releases_tenant_id_collection_release_id"); + + b.HasIndex("TenantId", "ContentReleaseId") + .HasDatabaseName("ix_blueprint_releases_tenant_id_content_release_id"); + + b.HasIndex("TenantId", "ExamPaperSpecificationVersionId") + .HasDatabaseName("ix_blueprint_releases_tenant_id_exam_paper_specification_versi~"); + + b.HasIndex("TenantId", "SourceBlueprintId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_blueprint_releases_tenant_id_source_blueprint_id_version_no"); + + b.ToTable("blueprint_releases", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetDimensionRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessTargetPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("business_target_policy_version_id"); + + b.Property("MaxValues") + .HasColumnType("integer") + .HasColumnName("max_values"); + + b.Property("MinValues") + .HasColumnType("integer") + .HasColumnName("min_values"); + + b.Property("RequiredForProfile") + .HasColumnType("boolean") + .HasColumnName("required_for_profile"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.HasKey("Id") + .HasName("pk_business_target_dimension_requirements"); + + b.HasIndex("TargetDimensionDefinitionId") + .HasDatabaseName("ix_business_target_dimension_requirements_target_dimension_def~"); + + b.HasIndex("BusinessTargetPolicyVersionId", "TargetDimensionDefinitionId") + .IsUnique() + .HasDatabaseName("ix_business_target_dimension_requirements_business_target_poli~"); + + b.ToTable("business_target_dimension_requirements", null, t => + { + t.HasCheckConstraint("ck_business_target_dimension_cardinality", "min_values >= 0 and max_values >= min_values"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_business_target_policies"); + + b.HasIndex("BusinessLineId") + .IsUnique() + .HasDatabaseName("ix_business_target_policies_business_line_id"); + + b.HasIndex("Id", "CurrentVersionId") + .HasDatabaseName("ix_business_target_policies_id_current_version_id"); + + b.ToTable("business_target_policies", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicyVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessTargetPolicyId") + .HasColumnType("uuid") + .HasColumnName("business_target_policy_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IncludesBaseContent") + .HasColumnType("boolean") + .HasColumnName("includes_base_content"); + + b.Property("MaxActiveTargets") + .HasColumnType("integer") + .HasColumnName("max_active_targets"); + + b.Property("MaxAlternateTargets") + .HasColumnType("integer") + .HasColumnName("max_alternate_targets"); + + b.Property("MaxPrimaryTargets") + .HasColumnType("integer") + .HasColumnName("max_primary_targets"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("SelectionMode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("selection_mode"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TargetChangeCooldownDays") + .HasColumnType("integer") + .HasColumnName("target_change_cooldown_days"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_business_target_policy_versions"); + + b.HasAlternateKey("BusinessTargetPolicyId", "Id") + .HasName("ak_business_target_policy_versions_business_target_policy_id_id"); + + b.HasIndex("BusinessTargetPolicyId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_business_target_policy_versions_business_target_policy_id_v~"); + + b.ToTable("business_target_policy_versions", null, t => + { + t.HasCheckConstraint("ck_business_target_policy_active", "max_active_targets between 1 and 16"); + + t.HasCheckConstraint("ck_business_target_policy_alternate", "max_alternate_targets between 0 and max_active_targets"); + + t.HasCheckConstraint("ck_business_target_policy_cooldown", "target_change_cooldown_days between 0 and 365"); + + t.HasCheckConstraint("ck_business_target_policy_primary", "max_primary_targets between 0 and max_active_targets"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("SourceCollectionId") + .HasColumnType("uuid") + .HasColumnName("source_collection_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.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_collection_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_collection_releases_tenant_id_id"); + + b.HasIndex("TenantId", "ContentReleaseId") + .HasDatabaseName("ix_collection_releases_tenant_id_content_release_id"); + + b.HasIndex("TenantId", "SourceCollectionId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_collection_releases_tenant_id_source_collection_id_version_~"); + + b.ToTable("collection_releases", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionReleaseQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CollectionReleaseId") + .HasColumnType("uuid") + .HasColumnName("collection_release_id"); + + b.Property("ContentReleaseQuestionId") + .HasColumnType("uuid") + .HasColumnName("content_release_question_id"); + + b.Property("Required") + .HasColumnType("boolean") + .HasColumnName("required"); + + b.Property("Score") + .HasColumnType("numeric") + .HasColumnName("score"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_collection_release_questions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_collection_release_questions_tenant_id_id"); + + b.HasIndex("TenantId", "ContentReleaseQuestionId") + .HasDatabaseName("ix_collection_release_questions_tenant_id_content_release_ques~"); + + b.HasIndex("TenantId", "CollectionReleaseId", "ContentReleaseQuestionId") + .IsUnique() + .HasDatabaseName("ux_collection_release_question_item"); + + b.HasIndex("TenantId", "CollectionReleaseId", "SortOrder") + .IsUnique() + .HasDatabaseName("ux_collection_release_question_order"); + + b.ToTable("collection_release_questions", (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.ContentRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("CompilerVersion") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("compiler_version"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("ReleaseNo") + .HasColumnType("integer") + .HasColumnName("release_no"); + + b.Property("SourceFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("source_fingerprint"); + + 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_content_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_releases_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_content_releases_business_line_id"); + + b.HasIndex("TenantId", "CurriculumVersionId", "ReleaseNo") + .IsUnique() + .HasDatabaseName("ix_content_releases_tenant_id_curriculum_version_id_release_no"); + + b.HasIndex("TenantId", "BusinessLineId", "Status", "PublishedAt") + .HasDatabaseName("ix_content_releases_tenant_id_business_line_id_status_publishe~"); + + b.ToTable("content_releases", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentReleaseQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("AudienceSegmentId") + .HasColumnType("uuid") + .HasColumnName("audience_segment_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CurriculumNodeId") + .HasColumnType("uuid") + .HasColumnName("curriculum_node_id"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("Ordinal") + .HasColumnType("integer") + .HasColumnName("ordinal"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionAssetOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_asset_owner_tenant_id"); + + b.Property("QuestionPlacementId") + .HasColumnType("uuid") + .HasColumnName("question_placement_id"); + + b.Property("QuestionRevisionId") + .HasColumnType("uuid") + .HasColumnName("question_revision_id"); + + b.Property("QuestionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("question_type"); + + 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_content_release_questions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_release_questions_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId") + .HasDatabaseName("ix_content_release_questions_tenant_id_assessment_policy_versi~"); + + b.HasIndex("TenantId", "AudienceSegmentId") + .HasDatabaseName("ix_content_release_questions_tenant_id_audience_segment_id"); + + b.HasIndex("TenantId", "QuestionPlacementId") + .HasDatabaseName("ix_content_release_questions_tenant_id_question_placement_id"); + + b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId", "QuestionRevisionId") + .HasDatabaseName("ix_content_release_questions_question_asset_owner_tenant_id_qu~"); + + b.HasIndex("TenantId", "ContentReleaseId", "AudienceSegmentId", "CurriculumNodeId", "Ordinal") + .IsUnique() + .HasDatabaseName("ix_content_release_questions_tenant_id_content_release_id_audi~"); + + b.HasIndex("TenantId", "ContentReleaseId", "AudienceSegmentId", "CurriculumNodeId", "Status", "Difficulty", "QuestionType", "Ordinal") + .HasDatabaseName("ix_content_release_question_candidates"); + + b.ToTable("content_release_questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.Curriculum", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_curricula"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curricula_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_curricula_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_curricula_tenant_id_code"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_curricula_tenant_id_id_current_version_id"); + + b.ToTable("curricula", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNode", 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("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("kind"); + + b.Property("KnowledgeConceptId") + .HasColumnType("uuid") + .HasColumnName("knowledge_concept_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_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_curriculum_nodes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curriculum_nodes_tenant_id_id"); + + b.HasIndex("TenantId", "KnowledgeConceptId") + .HasDatabaseName("ix_curriculum_nodes_tenant_id_knowledge_concept_id"); + + b.HasIndex("TenantId", "ParentId") + .HasDatabaseName("ix_curriculum_nodes_tenant_id_parent_id"); + + b.HasIndex("TenantId", "CurriculumVersionId", "Code") + .IsUnique() + .HasDatabaseName("ix_curriculum_nodes_tenant_id_curriculum_version_id_code"); + + b.HasIndex("TenantId", "CurriculumVersionId", "ParentId", "SortOrder") + .HasDatabaseName("ix_curriculum_nodes_tenant_id_curriculum_version_id_parent_id_~"); + + b.ToTable("curriculum_nodes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNodeClosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AncestorNodeId") + .HasColumnType("uuid") + .HasColumnName("ancestor_node_id"); + + b.Property("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("Depth") + .HasColumnType("integer") + .HasColumnName("depth"); + + b.Property("DescendantNodeId") + .HasColumnType("uuid") + .HasColumnName("descendant_node_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_curriculum_node_closure"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curriculum_node_closure_tenant_id_id"); + + b.HasIndex("TenantId", "CurriculumVersionId", "AncestorNodeId", "DescendantNodeId") + .IsUnique() + .HasDatabaseName("ix_curriculum_node_closure_tenant_id_curriculum_version_id_anc~"); + + b.HasIndex("TenantId", "CurriculumVersionId", "DescendantNodeId", "Depth") + .HasDatabaseName("ix_curriculum_node_closure_tenant_id_curriculum_version_id_des~"); + + b.ToTable("curriculum_node_closure", null, t => + { + t.HasCheckConstraint("ck_curriculum_node_closure_depth", "depth >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumVersion", 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("CurriculumId") + .HasColumnType("uuid") + .HasColumnName("curriculum_id"); + + b.Property("ExamYear") + .HasColumnType("integer") + .HasColumnName("exam_year"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_curriculum_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curriculum_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "CurriculumId", "Id") + .HasName("ak_curriculum_versions_tenant_id_curriculum_id_id"); + + b.HasIndex("TenantId", "CurriculumId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_curriculum_versions_tenant_id_curriculum_id_version_no"); + + b.ToTable("curriculum_versions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_paper_specifications"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_exam_paper_specifications_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_exam_paper_specifications_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_exam_paper_specifications_tenant_id_code"); + + b.ToTable("exam_paper_specifications", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecificationVersion", 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("DurationMinutes") + .HasColumnType("integer") + .HasColumnName("duration_minutes"); + + b.Property("ExamYear") + .HasColumnType("integer") + .HasColumnName("exam_year"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Sections") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sections") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("SpecificationId") + .HasColumnType("uuid") + .HasColumnName("specification_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalScore") + .HasColumnType("numeric") + .HasColumnName("total_score"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_exam_paper_specification_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_exam_paper_specification_versions_tenant_id_id"); + + b.HasIndex("TenantId", "SpecificationId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_exam_paper_specification_versions_tenant_id_specification_i~"); + + b.ToTable("exam_paper_specification_versions", null, t => + { + t.HasCheckConstraint("ck_exam_paper_spec_duration", "duration_minutes > 0"); + + t.HasCheckConstraint("ck_exam_paper_spec_total_score", "total_score > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("code"); + + 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_exam_target_profiles"); + + b.HasIndex("BusinessLineId", "Code") + .IsUnique() + .HasDatabaseName("ix_exam_target_profiles_business_line_id_code"); + + b.HasIndex("Id", "CurrentVersionId") + .HasDatabaseName("ix_exam_target_profiles_id_current_version_id"); + + b.ToTable("exam_target_profiles", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.Property("TargetNodeId") + .HasColumnType("uuid") + .HasColumnName("target_node_id"); + + b.HasKey("Id") + .HasName("pk_exam_target_profile_values"); + + b.HasIndex("TargetDimensionDefinitionId") + .HasDatabaseName("ix_exam_target_profile_values_target_dimension_definition_id"); + + b.HasIndex("TargetNodeId") + .HasDatabaseName("ix_exam_target_profile_values_target_node_id"); + + b.HasIndex("ExamTargetProfileVersionId", "TargetDimensionDefinitionId", "TargetNodeId") + .IsUnique() + .HasDatabaseName("ix_exam_target_profile_values_exam_target_profile_version_id_t~"); + + b.ToTable("exam_target_profile_values", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileVersion", 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(500) + .HasColumnType("character varying(500)") + .HasColumnName("display_name"); + + b.Property("ExamTargetProfileId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_id"); + + b.Property("ExamYear") + .HasColumnType("integer") + .HasColumnName("exam_year"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_exam_target_profile_versions"); + + b.HasAlternateKey("ExamTargetProfileId", "Id") + .HasName("ak_exam_target_profile_versions_exam_target_profile_id_id"); + + b.HasIndex("ExamTargetProfileId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_exam_target_profile_versions_exam_target_profile_id_version~"); + + b.ToTable("exam_target_profile_versions", (string)null); + }); + + 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.KnowledgeConcept", 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(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_knowledge_concepts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_knowledge_concepts_tenant_id_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_knowledge_concepts_tenant_id_code"); + + b.ToTable("knowledge_concepts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ObjectiveGradingRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Configuration") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("configuration") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RuleType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("rule_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_objective_grading_rules"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_objective_grading_rules_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "RuleType") + .IsUnique() + .HasDatabaseName("ix_objective_grading_rules_tenant_id_assessment_policy_version~"); + + b.ToTable("objective_grading_rules", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageCellProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CellId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("cell_id"); + + b.Property("ContentHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("content_hash"); + + b.Property("EventSequence") + .HasColumnType("bigint") + .HasColumnName("event_sequence"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("PackageCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("package_code"); + + b.Property("PackageOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("package_owner_tenant_id"); + + b.Property("PlatformContentPackageVersionId") + .HasColumnType("uuid") + .HasColumnName("platform_content_package_version_id"); + + b.Property("ProjectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("projected_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_platform_content_package_cell_projections"); + + b.HasIndex("PackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasDatabaseName("ix_platform_package_cell_owner_version"); + + b.HasIndex("PlatformContentPackageVersionId", "CellId") + .HasDatabaseName("ix_platform_package_cell_version"); + + b.HasIndex("PackageOwnerTenantId", "PackageCode", "CellId") + .IsUnique() + .HasDatabaseName("ux_platform_package_cell_cursor"); + + b.ToTable("platform_content_package_cell_projections", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ContentHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("content_hash"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("PackageCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("package_code"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_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.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_platform_content_package_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_content_package_versions_tenant_id_id"); + + b.HasIndex("TenantId", "ContentHash") + .IsUnique() + .HasDatabaseName("ix_platform_content_package_versions_tenant_id_content_hash"); + + b.HasIndex("TenantId", "ContentReleaseId") + .HasDatabaseName("ix_platform_content_package_versions_tenant_id_content_release~"); + + b.HasIndex("TenantId", "PackageCode", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_platform_content_package_versions_tenant_id_package_code_ve~"); + + b.ToTable("platform_content_package_versions", (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.QuestionAsset", 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("CurrentRevisionId") + .HasColumnType("uuid") + .HasColumnName("current_revision_id"); + + b.Property("DeliveryFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("delivery_fingerprint"); + + b.Property("FamilyId") + .HasColumnType("uuid") + .HasColumnName("family_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.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.HasKey("Id") + .HasName("pk_question_assets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_assets_tenant_id_id"); + + b.HasIndex("TenantId", "DeliveryFingerprint") + .IsUnique() + .HasDatabaseName("ix_question_assets_tenant_id_delivery_fingerprint"); + + b.HasIndex("TenantId", "FamilyId") + .HasDatabaseName("ix_question_assets_tenant_id_family_id"); + + b.HasIndex("TenantId", "Id", "CurrentRevisionId") + .HasDatabaseName("ix_question_assets_tenant_id_id_current_revision_id"); + + b.ToTable("question_assets", (string)null); + }); + + 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.QuestionFamily", 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("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_families"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_families_tenant_id_id"); + + b.ToTable("question_families", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_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("CurriculumNodeId") + .HasColumnType("uuid") + .HasColumnName("curriculum_node_id"); + + b.Property("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("DifficultyOverride") + .HasColumnType("integer") + .HasColumnName("difficulty_override"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionAssetOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_asset_owner_tenant_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.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + 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_question_placements"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placements_tenant_id_id"); + + b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasDatabaseName("ix_question_placements_question_asset_owner_tenant_id_question~"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId") + .HasDatabaseName("ix_question_placements_tenant_id_assessment_policy_version_id"); + + b.HasIndex("TenantId", "CurriculumNodeId") + .HasDatabaseName("ix_question_placements_tenant_id_curriculum_node_id"); + + b.HasIndex("TenantId", "CurriculumVersionId") + .HasDatabaseName("ix_question_placements_tenant_id_curriculum_version_id"); + + b.HasIndex("TenantId", "QuestionAssetOwnerTenantId", "QuestionAssetId", "CurriculumVersionId", "CurriculumNodeId", "AssessmentPolicyVersionId") + .IsUnique() + .HasDatabaseName("ix_question_placements_tenant_id_question_asset_owner_tenant_i~"); + + b.ToTable("question_placements", null, t => + { + t.HasCheckConstraint("ck_question_placements_valid_period", "valid_to is null or valid_from is null or valid_to > valid_from"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementConcept", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("KnowledgeConceptId") + .HasColumnType("uuid") + .HasColumnName("knowledge_concept_id"); + + b.Property("PlacementId") + .HasColumnType("uuid") + .HasColumnName("placement_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_placement_concepts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placement_concepts_tenant_id_id"); + + b.HasIndex("TenantId", "KnowledgeConceptId") + .HasDatabaseName("ix_question_placement_concepts_tenant_id_knowledge_concept_id"); + + b.HasIndex("TenantId", "PlacementId", "KnowledgeConceptId") + .IsUnique() + .HasDatabaseName("ix_question_placement_concepts_tenant_id_placement_id_knowledg~"); + + b.ToTable("question_placement_concepts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleCondition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("operator"); + + b.Property("RuleGroupId") + .HasColumnType("uuid") + .HasColumnName("rule_group_id"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.Property("TargetNodeId") + .HasColumnType("uuid") + .HasColumnName("target_node_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_placement_rule_conditions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placement_rule_conditions_tenant_id_id"); + + b.HasIndex("TargetDimensionDefinitionId") + .HasDatabaseName("ix_question_placement_rule_conditions_target_dimension_definit~"); + + b.HasIndex("TargetNodeId") + .HasDatabaseName("ix_question_placement_rule_conditions_target_node_id"); + + b.HasIndex("TenantId", "RuleGroupId", "TargetDimensionDefinitionId", "TargetNodeId", "Operator") + .IsUnique() + .HasDatabaseName("ix_question_placement_rule_conditions_tenant_id_rule_group_id_~"); + + b.ToTable("question_placement_rule_conditions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("GroupOrder") + .HasColumnType("integer") + .HasColumnName("group_order"); + + b.Property("PlacementId") + .HasColumnType("uuid") + .HasColumnName("placement_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_placement_rule_groups"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placement_rule_groups_tenant_id_id"); + + b.HasIndex("TenantId", "PlacementId", "GroupOrder") + .IsUnique() + .HasDatabaseName("ix_question_placement_rule_groups_tenant_id_placement_id_group~"); + + b.ToTable("question_placement_rule_groups", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionRevision", 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("DeliveryFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("delivery_fingerprint"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("Explanation") + .HasColumnType("text") + .HasColumnName("explanation"); + + b.Property("NormalizedContent") + .IsRequired() + .HasColumnType("text") + .HasColumnName("normalized_content"); + + b.Property("Options") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("options") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("question_type"); + + b.Property("RevisionNo") + .HasColumnType("integer") + .HasColumnName("revision_no"); + + b.Property("SubQuestions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sub_questions") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TypeLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label"); + + b.HasKey("Id") + .HasName("pk_question_revisions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_revisions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "QuestionAssetId", "Id") + .HasName("ak_question_revisions_tenant_id_question_asset_id_id"); + + b.HasIndex("NormalizedContent") + .HasDatabaseName("ix_question_revisions_content_trgm"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("NormalizedContent"), "gin"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("NormalizedContent"), new[] { "gin_trgm_ops" }); + + b.HasIndex("TenantId", "DeliveryFingerprint") + .HasDatabaseName("ix_question_revisions_tenant_id_delivery_fingerprint"); + + b.HasIndex("TenantId", "QuestionAssetId", "RevisionNo") + .IsUnique() + .HasDatabaseName("ix_question_revisions_tenant_id_question_asset_id_revision_no"); + + b.ToTable("question_revisions", (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.ScoreAdjustmentRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Configuration") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("configuration") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RuleType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("rule_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_score_adjustment_rules"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_score_adjustment_rules_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "Priority") + .IsUnique() + .HasDatabaseName("ix_score_adjustment_rules_tenant_id_assessment_policy_version_~"); + + b.ToTable("score_adjustment_rules", (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.SubjectiveRubric", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("Criteria") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("criteria") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("MaxScore") + .HasColumnType("numeric") + .HasColumnName("max_score"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_subjective_rubrics"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_subjective_rubrics_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "Code") + .IsUnique() + .HasDatabaseName("ix_subjective_rubrics_tenant_id_assessment_policy_version_id_c~"); + + b.ToTable("subjective_rubrics", null, t => + { + t.HasCheckConstraint("ck_subjective_rubric_max_score", "max_score > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TargetDimensionDefinition", 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsHierarchical") + .HasColumnType("boolean") + .HasColumnName("is_hierarchical"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_target_dimension_definitions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_target_dimension_definitions_code"); + + b.ToTable("target_dimension_definitions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TargetNode", 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .IsRequired() + .HasColumnType("ltree") + .HasColumnName("path"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_target_nodes"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_target_nodes_parent_id"); + + b.HasIndex("Path") + .HasDatabaseName("ix_target_nodes_path"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Path"), "gist"); + + b.HasIndex("TargetDimensionDefinitionId", "Code") + .IsUnique() + .HasDatabaseName("ix_target_nodes_target_dimension_definition_id_code"); + + b.ToTable("target_nodes", (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("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", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ux_answer_records_idempotency"); + + 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.BusinessLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RegionAccessStrategy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("region_access_strategy"); + + b.Property("RequiresBaseRegion") + .HasColumnType("boolean") + .HasColumnName("requires_base_region"); + + b.Property("TargetRegionCooldownDays") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30) + .HasColumnName("target_region_cooldown_days"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_business_lines"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_business_lines_code"); + + b.ToTable("business_lines", null, t => + { + t.HasCheckConstraint("ck_business_lines_target_region_cooldown_days", "target_region_cooldown_days between 0 and 365"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ClassAssignmentGrant", 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("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_type"); + + b.Property("RevokedBy") + .HasColumnType("uuid") + .HasColumnName("revoked_by"); + + b.Property("RevokedReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("revoked_reason"); + + 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_class_assignment_grants"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_class_assignment_grants_tenant_id_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId") + .HasDatabaseName("ix_class_assignment_grants_tenant_id_product_access_manifest_v~"); + + b.HasIndex("TenantId", "ClassId", "Status", "StartsAt") + .HasDatabaseName("ix_class_assignment_grants_tenant_id_class_id_status_starts_at"); + + b.HasIndex("TenantId", "ResourceType", "ResourceId", "Status") + .HasDatabaseName("ix_class_assignment_grants_tenant_id_resource_type_resource_id~"); + + b.ToTable("class_assignment_grants", null, t => + { + t.HasCheckConstraint("ck_class_assignment_grant_period", "ends_at is null or ends_at > starts_at"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ClassContentAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ClassId") + .HasColumnType("uuid") + .HasColumnName("class_id"); + + b.Property("ContentSliceId") + .HasColumnType("uuid") + .HasColumnName("content_slice_id"); + + b.Property("ContentSliceOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("content_slice_owner_tenant_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("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_type"); + + b.Property("RevokedBy") + .HasColumnType("uuid") + .HasColumnName("revoked_by"); + + b.Property("RevokedReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("revoked_reason"); + + 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_class_content_assignments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_class_content_assignments_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_class_content_assignments_created_by"); + + b.HasIndex("RevokedBy") + .HasDatabaseName("ix_class_content_assignments_revoked_by"); + + b.HasIndex("ContentSliceOwnerTenantId", "ContentSliceId") + .HasDatabaseName("ix_class_content_assignments_content_slice_owner_tenant_id_con~"); + + b.HasIndex("TenantId", "ClassId", "Status", "StartsAt") + .HasDatabaseName("ix_class_content_assignments_tenant_id_class_id_status_starts_~"); + + b.HasIndex("TenantId", "ResourceType", "ResourceId", "Status") + .HasDatabaseName("ix_class_content_assignments_tenant_id_resource_type_resource_~"); + + b.ToTable("class_content_assignments", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ContentSlice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ContentVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("content_version"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("MarketRegionId") + .HasColumnType("uuid") + .HasColumnName("market_region_id"); + + b.Property("RegionScope") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("region_scope"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_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_content_slices"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_slices_tenant_id_id"); + + b.HasIndex("MarketRegionId") + .HasDatabaseName("ix_content_slices_market_region_id"); + + b.HasIndex("BusinessLineId", "RegionScope", "MarketRegionId") + .HasDatabaseName("ix_content_slices_business_line_id_region_scope_market_region_~"); + + b.HasIndex("TenantId", "ResourceType", "ResourceId", "Status") + .HasDatabaseName("ix_content_slices_tenant_id_resource_type_resource_id_status"); + + b.ToTable("content_slices", null, t => + { + t.HasCheckConstraint("ck_content_slices_region_scope", "(region_scope = 'national' and market_region_id is null) or (region_scope = 'region' and market_region_id is not null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.CurrentAnswer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerRecordId") + .HasColumnType("uuid") + .HasColumnName("answer_record_id"); + + b.Property("ClientSequence") + .HasColumnType("bigint") + .HasColumnName("client_sequence"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("Revision") + .HasColumnType("integer") + .HasColumnName("revision"); + + b.Property("SessionQuestionId") + .HasColumnType("uuid") + .HasColumnName("session_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("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_current_answers"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_current_answers_tenant_id_id"); + + b.HasIndex("TenantId", "AnswerRecordId") + .HasDatabaseName("ix_current_answers_tenant_id_answer_record_id"); + + b.HasIndex("TenantId", "PracticeSessionId", "SessionQuestionId") + .HasDatabaseName("ix_current_answers_tenant_id_practice_session_id_session_quest~"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "SessionQuestionId") + .IsUnique() + .HasDatabaseName("ux_current_answers_session_question"); + + b.ToTable("current_answers", (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.EffectiveAccessProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.PrimitiveCollection("AlternateProfileVersionIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("alternate_profile_version_ids"); + + b.PrimitiveCollection("AudienceSegmentIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("audience_segment_ids"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("CompiledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("compiled_at"); + + b.PrimitiveCollection("ContentReleaseIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("content_release_ids"); + + b.Property("ContentVersion") + .HasColumnType("bigint") + .HasColumnName("content_version"); + + b.Property("GrantVersion") + .HasColumnType("bigint") + .HasColumnName("grant_version"); + + b.PrimitiveCollection("ManifestVersionIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("manifest_version_ids"); + + b.Property("PrimaryProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("primary_profile_version_id"); + + b.Property("StrongRevocationVersion") + .HasColumnType("bigint") + .HasColumnName("strong_revocation_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("valid_until"); + + b.HasKey("Id") + .HasName("pk_effective_access_projections"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_effective_access_projections_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_effective_access_projections_business_line_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_effective_access_projections_user_id"); + + b.HasIndex("TenantId", "ValidUntil") + .HasDatabaseName("ix_effective_access_projections_tenant_id_valid_until"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ix_effective_access_projections_tenant_id_user_id_business_lin~"); + + b.ToTable("effective_access_projections", (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.LearningAccessVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentVersion") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("content_version"); + + b.Property("GrantVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("grant_version"); + + b.Property("StrongRevocationVersion") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("strong_revocation_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_learning_access_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_access_versions_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_learning_access_versions_user_id"); + + b.HasIndex("TenantId", "UserId") + .IsUnique() + .HasDatabaseName("ix_learning_access_versions_tenant_id_user_id"); + + b.ToTable("learning_access_versions", (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.LearningOutboxMessage", b => + { + b.Property("OccurredAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_at") + .HasDefaultValueSql("now()"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AggregateId") + .HasColumnType("uuid") + .HasColumnName("aggregate_id"); + + b.Property("AttemptCount") + .HasColumnType("integer") + .HasColumnName("attempt_count"); + + b.Property("AvailableAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("available_at") + .HasDefaultValueSql("now()"); + + b.Property("DeadLetteredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("dead_lettered_at"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("event_type"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .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("Payload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at"); + + b.Property("SchemaVersion") + .HasColumnType("integer") + .HasColumnName("schema_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("OccurredAt", "Id") + .HasName("pk_learning_outbox_messages"); + + b.HasAlternateKey("TenantId", "OccurredAt", "Id") + .HasName("ak_learning_outbox_messages_tenant_id_occurred_at_id"); + + b.HasIndex("AvailableAt", "LockExpiresAt", "OccurredAt", "Id") + .HasDatabaseName("ix_learning_outbox_pending") + .HasFilter("processed_at IS NULL AND dead_lettered_at IS NULL"); + + b.HasIndex("TenantId", "EventType", "AggregateId", "OccurredAt") + .IsUnique() + .HasDatabaseName("ix_learning_outbox_messages_tenant_id_event_type_aggregate_id_~"); + + b.ToTable("learning_outbox_messages", null, t => + { + t.HasCheckConstraint("ck_learning_outbox_messages_attempt_count", "attempt_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProduct", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + 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_learning_products"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_products_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_learning_products_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_learning_products_tenant_id_code"); + + b.ToTable("learning_products", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProductScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentSliceId") + .HasColumnType("uuid") + .HasColumnName("content_slice_id"); + + b.Property("ContentSliceOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("content_slice_owner_tenant_id"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasColumnName("product_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_learning_product_scopes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_product_scopes_tenant_id_id"); + + b.HasIndex("ContentSliceOwnerTenantId", "ContentSliceId") + .HasDatabaseName("ix_learning_product_scopes_content_slice_owner_tenant_id_conte~"); + + b.HasIndex("TenantId", "ProductId", "ContentSliceId") + .IsUnique() + .HasDatabaseName("ix_learning_product_scopes_tenant_id_product_id_content_slice_~"); + + b.ToTable("learning_product_scopes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProjectionReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("OutboxMessageId") + .HasColumnType("uuid") + .HasColumnName("outbox_message_id"); + + b.Property("ProcessedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at") + .HasDefaultValueSql("now()"); + + b.Property("ProjectionName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("projection_name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_learning_projection_receipts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_projection_receipts_tenant_id_id"); + + b.HasIndex("TenantId", "OutboxMessageId", "ProjectionName") + .IsUnique() + .HasDatabaseName("ix_learning_projection_receipts_tenant_id_outbox_message_id_pr~"); + + b.ToTable("learning_projection_receipts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.MarketRegion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("ParentCode") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("parent_code"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_market_regions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_market_regions_code"); + + b.ToTable("market_regions", (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("AccessClassAssignmentId") + .HasColumnType("uuid") + .HasColumnName("access_class_assignment_id"); + + b.Property("AccessEntitlementId") + .HasColumnType("uuid") + .HasColumnName("access_entitlement_id"); + + b.Property("AccessGrantVersion") + .HasColumnType("bigint") + .HasColumnName("access_grant_version"); + + 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("AuthorizationExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("authorization_expires_at"); + + 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("StrongRevocationVersion") + .HasColumnType("bigint") + .HasColumnName("strong_revocation_version"); + + 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", "StrongRevocationVersion") + .HasDatabaseName("ix_practice_sessions_tenant_id_user_id_strong_revocation_versi~"); + + 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("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("ContentReleaseQuestionId") + .HasColumnType("uuid") + .HasColumnName("content_release_question_id"); + + b.Property("DifficultySnapshot") + .HasColumnType("integer") + .HasColumnName("difficulty_snapshot"); + + b.Property("GradingRulesSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("grading_rules_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Position") + .HasColumnType("integer") + .HasColumnName("position"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("QuestionOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_owner_tenant_id"); + + b.Property("QuestionPlacementId") + .HasColumnType("uuid") + .HasColumnName("question_placement_id"); + + b.Property("QuestionReferenceId") + .HasColumnType("uuid") + .HasColumnName("question_reference_id"); + + b.Property("QuestionRevisionId") + .HasColumnType("uuid") + .HasColumnName("question_revision_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", "ContentReleaseQuestionId") + .IsUnique() + .HasDatabaseName("ix_practice_session_questions_tenant_id_practice_session_id_co~") + .HasFilter("content_release_question_id is not null"); + + 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.ProductAccessManifest", 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LearningProductId") + .HasColumnType("uuid") + .HasColumnName("learning_product_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_product_access_manifests"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_access_manifests_tenant_id_id"); + + b.HasIndex("TenantId", "LearningProductId") + .IsUnique() + .HasDatabaseName("ix_product_access_manifests_tenant_id_learning_product_id"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_product_access_manifests_tenant_id_id_current_version_id"); + + b.ToTable("product_access_manifests", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductAccessManifestVersion", 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("DailyQuestionLimit") + .HasColumnType("integer") + .HasColumnName("daily_question_limit"); + + b.Property("MaxActiveTargets") + .HasColumnType("integer") + .HasColumnName("max_active_targets"); + + b.Property("MaxAlternateTargets") + .HasColumnType("integer") + .HasColumnName("max_alternate_targets"); + + b.Property("ProductAccessManifestId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_id"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalQuestionLimit") + .HasColumnType("integer") + .HasColumnName("total_question_limit"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_product_access_manifest_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_access_manifest_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "ProductAccessManifestId", "Id") + .HasName("ak_product_access_manifest_versions_tenant_id_product_access_m~"); + + b.HasIndex("TenantId", "ProductAccessManifestId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_product_access_manifest_versions_tenant_id_product_access_m~"); + + b.ToTable("product_access_manifest_versions", null, t => + { + t.HasCheckConstraint("ck_product_manifest_active_targets", "max_active_targets between 1 and 16"); + + t.HasCheckConstraint("ck_product_manifest_alternate_targets", "max_alternate_targets between 0 and max_active_targets"); + + t.HasCheckConstraint("ck_product_manifest_daily_question_limit", "daily_question_limit is null or daily_question_limit >= 0"); + + t.HasCheckConstraint("ck_product_manifest_total_question_limit", "total_question_limit is null or total_question_limit >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("content_owner_tenant_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("PlatformContentPackageOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("platform_content_package_owner_tenant_id"); + + b.Property("PlatformContentPackageVersionId") + .HasColumnType("uuid") + .HasColumnName("platform_content_package_version_id"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_product_manifest_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_manifest_releases_tenant_id_id"); + + b.HasIndex("ContentOwnerTenantId", "ContentReleaseId") + .HasDatabaseName("ix_product_manifest_releases_content_owner_tenant_id_content_r~"); + + b.HasIndex("PlatformContentPackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasDatabaseName("ix_product_manifest_releases_platform_content_package_owner_te~"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "ContentOwnerTenantId", "ContentReleaseId") + .IsUnique() + .HasDatabaseName("ix_product_manifest_releases_tenant_id_product_access_manifest~"); + + b.ToTable("product_manifest_releases", null, t => + { + t.HasCheckConstraint("ck_product_manifest_release_platform_pair", "(platform_content_package_owner_tenant_id is null) = (platform_content_package_version_id is null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("display_name"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_type"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_product_manifest_resources"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_manifest_resources_tenant_id_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "ResourceType", "ResourceId") + .IsUnique() + .HasDatabaseName("ux_product_manifest_resource"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "SortOrder", "Id") + .HasDatabaseName("ix_product_manifest_resource_order"); + + b.ToTable("product_manifest_resources", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("MayBeAlternate") + .HasColumnType("boolean") + .HasColumnName("may_be_alternate"); + + b.Property("MayBePrimary") + .HasColumnType("boolean") + .HasColumnName("may_be_primary"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_product_manifest_targets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_manifest_targets_tenant_id_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_product_manifest_targets_exam_target_profile_version_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_product_manifest_targets_tenant_id_product_access_manifest_~"); + + b.ToTable("product_manifest_targets", null, t => + { + t.HasCheckConstraint("ck_product_manifest_target_role", "may_be_primary or may_be_alternate"); + }); + }); + + 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.StudentEntitlement", 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("LearningProductId") + .HasColumnType("uuid") + .HasColumnName("learning_product_id"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + 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("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_type"); + + 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.Property("UsedQuestionCount") + .HasColumnType("integer") + .HasColumnName("used_question_count"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_student_entitlements_v2"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_entitlements_v2_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_entitlements_v2_user_id"); + + b.HasIndex("TenantId", "LearningProductId") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_learning_product_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_product_access_manifest_v~"); + + b.HasIndex("TenantId", "SourceType", "SourceId") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_source_type_source_id") + .HasFilter("source_id is not null"); + + b.HasIndex("TenantId", "UserId", "Status", "StartsAt", "EndsAt") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_user_id_status_starts_at_~"); + + b.ToTable("student_entitlements_v2", null, t => + { + t.HasCheckConstraint("ck_student_entitlement_v2_period", "ends_at is null or ends_at > starts_at"); + + t.HasCheckConstraint("ck_student_entitlements_v2_used_question_count", "used_question_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetRegionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ChangedBy") + .HasColumnType("uuid") + .HasColumnName("changed_by"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("effective_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("IsCurrent") + .HasColumnType("boolean") + .HasColumnName("is_current"); + + b.Property("MarketRegionId") + .HasColumnType("uuid") + .HasColumnName("market_region_id"); + + b.Property("Reason") + .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.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_student_target_region_history"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_target_region_history_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_student_target_region_history_business_line_id"); + + b.HasIndex("ChangedBy") + .HasDatabaseName("ix_student_target_region_history_changed_by"); + + b.HasIndex("MarketRegionId") + .HasDatabaseName("ix_student_target_region_history_market_region_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_target_region_history_user_id"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ux_student_target_region_current") + .HasFilter("is_current"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId", "EffectiveAt") + .HasDatabaseName("ix_student_target_region_history"); + + b.ToTable("student_target_region_history", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetSelectionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ChangedBy") + .HasColumnType("uuid") + .HasColumnName("changed_by"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("effective_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("EntitlementId") + .HasColumnType("uuid") + .HasColumnName("entitlement_id"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("IsCurrent") + .HasColumnType("boolean") + .HasColumnName("is_current"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("role"); + + 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_target_selection_history"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_target_selection_history_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_student_target_selection_history_business_line_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_student_target_selection_history_exam_target_profile_versio~"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_target_selection_history_user_id"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ux_student_target_primary") + .HasFilter("is_current and role = 'primary'"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId", "EffectiveAt") + .HasDatabaseName("ix_student_target_selection_history"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_student_target_selection_history_tenant_id_user_id_business~") + .HasFilter("is_current"); + + b.ToTable("student_target_selection_history", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantBusinessLicense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("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.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_tenant_business_licenses"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_business_licenses_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_tenant_business_licenses_business_line_id"); + + b.HasIndex("TenantId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ix_tenant_business_licenses_tenant_id_business_line_id"); + + b.ToTable("tenant_business_licenses", null, t => + { + t.HasCheckConstraint("ck_tenant_business_license_period", "ends_at is null or ends_at > starts_at"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowsAnyTargetRegion") + .HasColumnType("boolean") + .HasColumnName("allows_any_target_region"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("IncludesNational") + .HasColumnType("boolean") + .HasColumnName("includes_national"); + + b.Property("IsPrimary") + .HasColumnType("boolean") + .HasColumnName("is_primary"); + + 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.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_tenant_learning_licenses"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_learning_licenses_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_tenant_learning_licenses_business_line_id"); + + b.HasIndex("TenantId") + .IsUnique() + .HasDatabaseName("ix_tenant_learning_licenses_tenant_id") + .HasFilter("is_primary and status = 'active'"); + + b.HasIndex("TenantId", "BusinessLineId", "Status") + .HasDatabaseName("ix_tenant_learning_licenses_tenant_id_business_line_id_status"); + + b.ToTable("tenant_learning_licenses", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicenseRegion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("IsBaseRegion") + .HasColumnType("boolean") + .HasColumnName("is_base_region"); + + b.Property("LicenseId") + .HasColumnType("uuid") + .HasColumnName("license_id"); + + b.Property("MarketRegionId") + .HasColumnType("uuid") + .HasColumnName("market_region_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_tenant_learning_license_regions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_learning_license_regions_tenant_id_id"); + + b.HasIndex("MarketRegionId") + .HasDatabaseName("ix_tenant_learning_license_regions_market_region_id"); + + b.HasIndex("TenantId", "LicenseId", "MarketRegionId") + .IsUnique() + .HasDatabaseName("ix_tenant_learning_license_regions_tenant_id_license_id_market~"); + + b.ToTable("tenant_learning_license_regions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLicensedTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("IsBaseTarget") + .HasColumnType("boolean") + .HasColumnName("is_base_target"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_at"); + + b.Property("TenantBusinessLicenseId") + .HasColumnType("uuid") + .HasColumnName("tenant_business_license_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_tenant_licensed_targets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_licensed_targets_tenant_id_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_tenant_licensed_targets_exam_target_profile_version_id"); + + b.HasIndex("TenantId", "TenantBusinessLicenseId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_tenant_licensed_targets_tenant_id_tenant_business_license_i~"); + + b.ToTable("tenant_licensed_targets", null, t => + { + t.HasCheckConstraint("ck_tenant_licensed_target_period", "ends_at is null or ends_at > starts_at"); + }); + }); + + 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.Learning.WrongQuestionV2", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("QuestionAssetOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_asset_owner_tenant_id"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("LastQuestionPlacementId") + .HasColumnType("uuid") + .HasColumnName("last_question_placement_id"); + + b.Property("LastQuestionRevisionId") + .HasColumnType("uuid") + .HasColumnName("last_question_revision_id"); + + b.Property("LastWrongAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_wrong_at") + .HasDefaultValueSql("now()"); + + 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", "QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasName("pk_wrong_questions_v2"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_wrong_questions_v2_user_id"); + + b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId", "LastQuestionRevisionId") + .HasDatabaseName("ix_wrong_questions_v2_question_asset_owner_tenant_id_question_~"); + + b.HasIndex("TenantId", "UserId", "LastWrongAt") + .HasDatabaseName("ix_wrong_questions_v2_tenant_id_user_id_last_wrong_at"); + + b.ToTable("wrong_questions_v2", (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.PlatformApprovalPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AlwaysRequireApproval") + .HasColumnType("boolean") + .HasColumnName("always_require_approval"); + + b.Property("AmountThresholdCents") + .HasColumnType("integer") + .HasColumnName("amount_threshold_cents"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .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("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("ExpiresAfterHours") + .HasColumnType("integer") + .HasColumnName("expires_after_hours"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RequiredPermission") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("required_permission"); + + 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_platform_approval_policies"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_approval_policies_code"); + + b.ToTable("platform_approval_policies", null, t => + { + t.HasCheckConstraint("ck_platform_approval_policies_expiry", "expires_after_hours between 1 and 720"); + + t.HasCheckConstraint("ck_platform_approval_policies_threshold", "amount_threshold_cents is null or amount_threshold_cents > 0"); + + t.HasCheckConstraint("ck_platform_approval_policies_version", "version > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformApprovalRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("CommandType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("command_type"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("uuid") + .HasColumnName("concurrency_stamp"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DecidedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("decided_at"); + + b.Property("DecidedBy") + .HasColumnType("uuid") + .HasColumnName("decided_by"); + + b.Property("DecisionReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("decision_reason"); + + b.Property("Error") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("error"); + + b.Property("ExecutedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_at"); + + 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("PolicyCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("policy_code"); + + b.Property("PolicyVersion") + .HasColumnType("integer") + .HasColumnName("policy_version"); + + b.Property("RequestHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("request_hash"); + + b.Property("RequestNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("request_no"); + + b.Property("RequestReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("request_reason"); + + b.Property("RequestSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("request_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RequestedBy") + .HasColumnType("uuid") + .HasColumnName("requested_by"); + + b.Property("RequiredPermission") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("required_permission"); + + b.Property("ResultSnapshot") + .HasColumnType("jsonb") + .HasColumnName("result_snapshot"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TargetId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("target_id"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("target_type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_approval_requests"); + + b.HasIndex("DecidedBy") + .HasDatabaseName("ix_platform_approval_requests_decided_by"); + + b.HasIndex("RequestNo") + .IsUnique() + .HasDatabaseName("ix_platform_approval_requests_request_no"); + + b.HasIndex("RequestedBy", "CommandType", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_approval_requests_requested_by_command_type_idempo~"); + + b.HasIndex("Status", "ExpiresAt", "CreatedAt") + .HasDatabaseName("ix_platform_approval_requests_status_expires_at_created_at"); + + b.ToTable("platform_approval_requests", (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.PlatformConfigurationDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowRuntimeManagement") + .HasColumnType("boolean") + .HasColumnName("allow_runtime_management"); + + 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("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("IsSensitive") + .HasColumnType("boolean") + .HasColumnName("is_sensitive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("ValidationSchema") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("validation_schema") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ValueType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("value_type"); + + b.HasKey("Id") + .HasName("pk_platform_configuration_definitions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_configuration_definitions_code"); + + b.HasIndex("Category", "Code") + .HasDatabaseName("ix_platform_configuration_definitions_category_code"); + + b.ToTable("platform_configuration_definitions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformConfigurationVersion", 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("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id"); + + b.Property("Environment") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)") + .HasColumnName("environment"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("PublishedBy") + .HasColumnType("uuid") + .HasColumnName("published_by"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("RolledBackFromVersionId") + .HasColumnType("uuid") + .HasColumnName("rolled_back_from_version_id"); + + 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.Property("Value") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("value") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_platform_configuration_versions"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_platform_configuration_versions_created_by"); + + b.HasIndex("PublishedBy") + .HasDatabaseName("ix_platform_configuration_versions_published_by"); + + b.HasIndex("RolledBackFromVersionId") + .HasDatabaseName("ix_platform_configuration_versions_rolled_back_from_version_id"); + + b.HasIndex("DefinitionId", "Environment", "Status") + .HasDatabaseName("ix_platform_configuration_versions_definition_id_environment_s~"); + + b.HasIndex("DefinitionId", "Environment", "Version") + .IsUnique() + .HasDatabaseName("ix_platform_configuration_versions_definition_id_environment_v~"); + + b.ToTable("platform_configuration_versions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformNotificationDelivery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("body"); + + 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("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("LastError") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("last_error"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RecipientRoleCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("recipient_role_code"); + + b.Property("RecipientUserId") + .HasColumnType("uuid") + .HasColumnName("recipient_user_id"); + + 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("Subject") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("subject"); + + b.Property("TemplateId") + .HasColumnType("uuid") + .HasColumnName("template_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_notification_deliveries"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_platform_notification_deliveries_created_by"); + + b.HasIndex("RecipientUserId") + .HasDatabaseName("ix_platform_notification_deliveries_recipient_user_id"); + + b.HasIndex("Status", "CreatedAt") + .HasDatabaseName("ix_platform_notification_deliveries_status_created_at"); + + b.HasIndex("TemplateId", "RecipientUserId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_notification_deliveries_template_id_recipient_user~"); + + b.ToTable("platform_notification_deliveries", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformNotificationTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BodyTemplate") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("body_template"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("channel"); + + 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("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("SubjectTemplate") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("subject_template"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("variables") + .HasDefaultValueSql("'[]'::jsonb"); + + b.HasKey("Id") + .HasName("pk_platform_notification_templates"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_notification_templates_code"); + + b.ToTable("platform_notification_templates", (string)null); + }); + + 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("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + 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("QuestionType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("choice") + .HasColumnName("question_type"); + + 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("Tags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("tags") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TypeLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label"); + + 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("CellId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("cell-01") + .HasColumnName("cell_id"); + + 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.HasIndex("CellId", "Status") + .HasDatabaseName("ix_tenants_cell_id_status"); + + 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"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "LearningProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_entitlements_learning_products_tenant_id_learning_product_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.AssessmentPolicy", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_assessment_policies_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_policies_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "AssessmentPolicyId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_assessment_policies_assessment_policy_versions_tenant_id_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentPolicyVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_policy_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicy", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_policy_versions_assessment_policies_tenant_id_as~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentScoreBand", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_score_bands_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_score_bands_assessment_policy_versions_tenant_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegment", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segments_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segments_content_releases_tenant_id_content_releas~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegmentMember", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_audience_segment_members_exam_target_profile_versions_exam_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segment_members_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AudienceSegment", null) + .WithMany() + .HasForeignKey("TenantId", "AudienceSegmentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segment_members_audience_segments_tenant_id_audien~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BlueprintRelease", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_blueprint_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CollectionRelease", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_blueprint_releases_collection_releases_tenant_id_collection~"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_blueprint_releases_content_releases_tenant_id_content_relea~"); + + b.HasOne("Tiku.Domain.Content.ExamPaperSpecificationVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ExamPaperSpecificationVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_blueprint_releases_exam_paper_specification_versions_tenant~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetDimensionRequirement", b => + { + b.HasOne("Tiku.Domain.Content.BusinessTargetPolicyVersion", null) + .WithMany() + .HasForeignKey("BusinessTargetPolicyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_target_dimension_requirements_business_target_poli~"); + + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_business_target_dimension_requirements_target_dimension_def~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicy", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_business_target_policies_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Content.BusinessTargetPolicyVersion", null) + .WithMany() + .HasForeignKey("Id", "CurrentVersionId") + .HasPrincipalKey("BusinessTargetPolicyId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_business_target_policies_business_target_policy_versions_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicyVersion", b => + { + b.HasOne("Tiku.Domain.Content.BusinessTargetPolicy", null) + .WithMany() + .HasForeignKey("BusinessTargetPolicyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_target_policy_versions_business_target_policies_bu~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionRelease", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_collection_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_collection_releases_content_releases_tenant_id_content_rele~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionReleaseQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_collection_release_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CollectionRelease", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_collection_release_questions_collection_releases_tenant_id_~"); + + b.HasOne("Tiku.Domain.Content.ContentReleaseQuestion", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseQuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_collection_release_questions_content_release_questions_tena~"); + }); + + 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.ContentRelease", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_releases_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_releases_curriculum_versions_tenant_id_curriculum_v~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentReleaseQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_release_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_assessment_policy_versions_tenant~"); + + b.HasOne("Tiku.Domain.Content.AudienceSegment", null) + .WithMany() + .HasForeignKey("TenantId", "AudienceSegmentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_audience_segments_tenant_id_audie~"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_release_questions_content_releases_tenant_id_conten~"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacement", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionPlacementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_question_placements_tenant_id_que~"); + + b.HasOne("Tiku.Domain.Content.QuestionRevision", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId", "QuestionRevisionId") + .HasPrincipalKey("TenantId", "QuestionAssetId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_question_revisions_question_asset~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.Curriculum", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_curricula_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curricula_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "CurriculumId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_curricula_curriculum_versions_tenant_id_id_current_version_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNode", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_nodes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_nodes_curriculum_versions_tenant_id_curriculum_v~"); + + b.HasOne("Tiku.Domain.Content.KnowledgeConcept", null) + .WithMany() + .HasForeignKey("TenantId", "KnowledgeConceptId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_curriculum_nodes_knowledge_concepts_tenant_id_knowledge_con~"); + + b.HasOne("Tiku.Domain.Content.CurriculumNode", null) + .WithMany() + .HasForeignKey("TenantId", "ParentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_curriculum_nodes_curriculum_nodes_tenant_id_parent_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNodeClosure", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_node_closure_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.Curriculum", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_versions_curricula_tenant_id_curriculum_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecification", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_paper_specifications_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_paper_specifications_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecificationVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_paper_specification_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ExamPaperSpecification", null) + .WithMany() + .HasForeignKey("TenantId", "SpecificationId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_paper_specification_versions_exam_paper_specifications~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfile", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_target_profiles_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("Id", "CurrentVersionId") + .HasPrincipalKey("ExamTargetProfileId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_exam_target_profiles_exam_target_profile_versions_id_curren~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileValue", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_values_exam_target_profile_versions_exa~"); + + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_values_target_dimension_definitions_tar~"); + + b.HasOne("Tiku.Domain.Content.TargetNode", null) + .WithMany() + .HasForeignKey("TargetNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_values_target_nodes_target_node_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileVersion", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfile", null) + .WithMany() + .HasForeignKey("ExamTargetProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_versions_exam_target_profiles_exam_targ~"); + }); + + 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.KnowledgeConcept", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_knowledge_concepts_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ObjectiveGradingRule", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_objective_grading_rules_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_objective_grading_rules_assessment_policy_versions_tenant_i~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageCellProjection", b => + { + b.HasOne("Tiku.Domain.Content.PlatformContentPackageVersion", null) + .WithMany() + .HasForeignKey("PackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_content_package_cell_projections_platform_content_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_content_package_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_content_package_versions_content_releases_tenant_i~"); + }); + + 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.QuestionAsset", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_assets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionFamily", null) + .WithMany() + .HasForeignKey("TenantId", "FamilyId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_assets_question_families_tenant_id_family_id"); + + b.HasOne("Tiku.Domain.Content.QuestionRevision", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentRevisionId") + .HasPrincipalKey("TenantId", "QuestionAssetId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_assets_question_revisions_tenant_id_id_current_rev~"); + }); + + 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.QuestionFamily", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_families_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacement", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placements_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionAsset", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_question_assets_question_asset_owner_te~"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_assessment_policy_versions_tenant_id_as~"); + + b.HasOne("Tiku.Domain.Content.CurriculumNode", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_curriculum_nodes_tenant_id_curriculum_n~"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_curriculum_versions_tenant_id_curriculu~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementConcept", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_concepts_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.KnowledgeConcept", null) + .WithMany() + .HasForeignKey("TenantId", "KnowledgeConceptId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placement_concepts_knowledge_concepts_tenant_id_kn~"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacement", null) + .WithMany() + .HasForeignKey("TenantId", "PlacementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_concepts_question_placements_tenant_id_p~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleCondition", b => + { + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_target_dimension_definit~"); + + b.HasOne("Tiku.Domain.Content.TargetNode", null) + .WithMany() + .HasForeignKey("TargetNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_target_nodes_target_node~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacementRuleGroup", null) + .WithMany() + .HasForeignKey("TenantId", "RuleGroupId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_question_placement_rule_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleGroup", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_groups_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacement", null) + .WithMany() + .HasForeignKey("TenantId", "PlacementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_groups_question_placements_tenant_i~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionRevision", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_revisions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionAsset", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_revisions_question_assets_tenant_id_question_asset~"); + }); + + 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.ScoreAdjustmentRule", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_score_adjustment_rules_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_score_adjustment_rules_assessment_policy_versions_tenant_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.SubjectiveRubric", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subjective_rubrics_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subjective_rubrics_assessment_policy_versions_tenant_id_ass~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TargetNode", b => + { + b.HasOne("Tiku.Domain.Content.TargetNode", null) + .WithMany() + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_target_nodes_target_nodes_parent_id"); + + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_target_nodes_target_dimension_definitions_target_dimension_~"); + }); + + 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.ClassAssignmentGrant", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_assignment_grants_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Tenancy.TenantClass", null) + .WithMany() + .HasForeignKey("TenantId", "ClassId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_assignment_grants_tenant_classes_tenant_id_class_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_class_assignment_grants_product_access_manifest_versions_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ClassContentAssignment", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_class_content_assignments_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RevokedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_class_content_assignments_users_revoked_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_content_assignments_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ContentSlice", null) + .WithMany() + .HasForeignKey("ContentSliceOwnerTenantId", "ContentSliceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_class_content_assignments_content_slices_content_slice_owne~"); + + b.HasOne("Tiku.Domain.Tenancy.TenantClass", null) + .WithMany() + .HasForeignKey("TenantId", "ClassId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_content_assignments_tenant_classes_tenant_id_class_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ContentSlice", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_slices_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Learning.MarketRegion", null) + .WithMany() + .HasForeignKey("MarketRegionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_slices_market_regions_market_region_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_slices_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.CurrentAnswer", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_current_answers_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.AnswerRecord", null) + .WithMany() + .HasForeignKey("TenantId", "AnswerRecordId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_current_answers_answer_records_tenant_id_answer_record_id"); + + b.HasOne("Tiku.Domain.Learning.PracticeSessionQuestion", null) + .WithMany() + .HasForeignKey("TenantId", "PracticeSessionId", "SessionQuestionId") + .HasPrincipalKey("TenantId", "PracticeSessionId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_current_answers_practice_session_questions_tenant_id_practi~"); + }); + + 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.EffectiveAccessProjection", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_effective_access_projections_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_effective_access_projections_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_effective_access_projections_users_user_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.LearningAccessVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_access_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_access_versions_users_user_id"); + }); + + 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.LearningOutboxMessage", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_outbox_messages_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProduct", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_learning_products_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_products_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProductScope", b => + { + b.HasOne("Tiku.Domain.Learning.ContentSlice", null) + .WithMany() + .HasForeignKey("ContentSliceOwnerTenantId", "ContentSliceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_learning_product_scopes_content_slices_content_slice_owner_~"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "ProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_product_scopes_learning_products_tenant_id_product~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProjectionReceipt", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_projection_receipts_tenants_tenant_id"); + }); + + 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) + .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) + .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.ProductAccessManifest", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifests_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "LearningProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifests_learning_products_tenant_id_learni~"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "ProductAccessManifestId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_product_access_manifests_product_access_manifest_versions_t~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductAccessManifestVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifest_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifest", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifest_versions_product_access_manifests_t~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestRelease", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("ContentOwnerTenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_product_manifest_releases_content_releases_content_owner_te~"); + + b.HasOne("Tiku.Domain.Content.PlatformContentPackageVersion", null) + .WithMany() + .HasForeignKey("PlatformContentPackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_product_manifest_releases_platform_content_package_versions~"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_releases_product_access_manifest_versions_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestResource", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_resources_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_resources_product_access_manifest_versions~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestTarget", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_product_manifest_targets_exam_target_profile_versions_exam_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_targets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_targets_product_access_manifest_versions_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.StudentEntitlement", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_users_user_id"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "LearningProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_learning_products_tenant_id_learnin~"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_product_access_manifest_versions_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetRegionHistory", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_business_lines_business_line_~"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ChangedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_student_target_region_history_users_changed_by"); + + b.HasOne("Tiku.Domain.Learning.MarketRegion", null) + .WithMany() + .HasForeignKey("MarketRegionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_market_regions_market_region_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetSelectionHistory", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_business_lines_business_li~"); + + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_exam_target_profile_versio~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantBusinessLicense", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_business_licenses_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_business_licenses_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicense", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_learning_licenses_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_learning_licenses_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicenseRegion", b => + { + b.HasOne("Tiku.Domain.Learning.MarketRegion", null) + .WithMany() + .HasForeignKey("MarketRegionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_learning_license_regions_market_regions_market_regio~"); + + b.HasOne("Tiku.Domain.Learning.TenantLearningLicense", null) + .WithMany() + .HasForeignKey("TenantId", "LicenseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_learning_license_regions_tenant_learning_licenses_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLicensedTarget", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_licensed_targets_exam_target_profile_versions_exam_t~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_licensed_targets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.TenantBusinessLicense", null) + .WithMany() + .HasForeignKey("TenantId", "TenantBusinessLicenseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_licensed_targets_tenant_business_licenses_tenant_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.Learning.WrongQuestionV2", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_users_user_id"); + + b.HasOne("Tiku.Domain.Content.QuestionAsset", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_question_assets_question_asset_owner_ten~"); + + b.HasOne("Tiku.Domain.Content.QuestionRevision", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId", "LastQuestionRevisionId") + .HasPrincipalKey("TenantId", "QuestionAssetId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_question_revisions_question_asset_owner_~"); + }); + + 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.PlatformApprovalRequest", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("DecidedBy") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_approval_requests_users_decided_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RequestedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_approval_requests_users_requested_by"); + }); + + 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.PlatformConfigurationVersion", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_configuration_versions_users_created_by"); + + b.HasOne("Tiku.Domain.Platform.PlatformConfigurationDefinition", null) + .WithMany() + .HasForeignKey("DefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_configuration_versions_platform_configuration_defi~"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("PublishedBy") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_configuration_versions_users_published_by"); + + b.HasOne("Tiku.Domain.Platform.PlatformConfigurationVersion", null) + .WithMany() + .HasForeignKey("RolledBackFromVersionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_configuration_versions_platform_configuration_vers~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformNotificationDelivery", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_notification_deliveries_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RecipientUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_notification_deliveries_users_recipient_user_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformNotificationTemplate", null) + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_notification_deliveries_platform_notification_temp~"); + }); + + 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/20260805091745_ContentDomainV2Working.cs b/Tiku.Infrastructure/Persistence/Migrations/20260805091745_ContentDomainV2Working.cs new file mode 100644 index 0000000..eeabf4d --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260805091745_ContentDomainV2Working.cs @@ -0,0 +1,2811 @@ +using System; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Tiku.Infrastructure.Persistence.Migrations +{ + /// + public partial class ContentDomainV2Working : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "answer_submission_idempotencies"); + + migrationBuilder.AlterColumn( + name: "question_version_id", + table: "practice_session_questions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "question_reference_id", + table: "practice_session_questions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "question_id", + table: "practice_session_questions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "assessment_policy_version_id", + table: "practice_session_questions", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "content_release_question_id", + table: "practice_session_questions", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "question_asset_id", + table: "practice_session_questions", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "question_placement_id", + table: "practice_session_questions", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "question_revision_id", + table: "practice_session_questions", + type: "uuid", + nullable: true); + + migrationBuilder.CreateTable( + name: "curriculum_node_closure", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + curriculum_version_id = table.Column(type: "uuid", nullable: false), + ancestor_node_id = table.Column(type: "uuid", nullable: false), + descendant_node_id = table.Column(type: "uuid", nullable: false), + depth = table.Column(type: "integer", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_curriculum_node_closure", x => x.id); + table.UniqueConstraint("ak_curriculum_node_closure_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_curriculum_node_closure_depth", "depth >= 0"); + table.ForeignKey( + name: "fk_curriculum_node_closure_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "effective_access_projections", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + user_id = table.Column(type: "uuid", nullable: false), + business_line_id = table.Column(type: "uuid", nullable: false), + primary_profile_version_id = table.Column(type: "uuid", nullable: true), + alternate_profile_version_ids = table.Column(type: "uuid[]", nullable: false), + content_release_ids = table.Column(type: "uuid[]", nullable: false), + audience_segment_ids = table.Column(type: "uuid[]", nullable: false), + manifest_version_ids = table.Column(type: "uuid[]", nullable: false), + grant_version = table.Column(type: "bigint", nullable: false), + content_version = table.Column(type: "bigint", nullable: false), + strong_revocation_version = table.Column(type: "bigint", nullable: false), + valid_until = table.Column(type: "timestamp with time zone", nullable: false), + compiled_at = table.Column(type: "timestamp with time zone", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_effective_access_projections", x => x.id); + table.UniqueConstraint("ak_effective_access_projections_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_effective_access_projections_business_lines_business_line_id", + column: x => x.business_line_id, + principalTable: "business_lines", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_effective_access_projections_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_effective_access_projections_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "exam_paper_specifications", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + business_line_id = table.Column(type: "uuid", nullable: false), + code = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + current_version_id = table.Column(type: "uuid", nullable: true), + is_active = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_exam_paper_specifications", x => x.id); + table.UniqueConstraint("ak_exam_paper_specifications_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_exam_paper_specifications_business_lines_business_line_id", + column: x => x.business_line_id, + principalTable: "business_lines", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_exam_paper_specifications_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "knowledge_concepts", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + code = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + is_active = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_knowledge_concepts", x => x.id); + table.UniqueConstraint("ak_knowledge_concepts_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_knowledge_concepts_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "question_families", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + created_by = table.Column(type: "uuid", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_question_families", x => x.id); + table.UniqueConstraint("ak_question_families_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_question_families_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "target_dimension_definitions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + code = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + is_hierarchical = table.Column(type: "boolean", nullable: false), + is_active = table.Column(type: "boolean", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_target_dimension_definitions", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "tenant_business_licenses", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + business_line_id = table.Column(type: "uuid", nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + starts_at = table.Column(type: "timestamp with time zone", nullable: false), + ends_at = table.Column(type: "timestamp with time zone", nullable: true), + version = table.Column(type: "bigint", nullable: false, defaultValue: 1L), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_tenant_business_licenses", x => x.id); + table.UniqueConstraint("ak_tenant_business_licenses_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_tenant_business_license_period", "ends_at is null or ends_at > starts_at"); + table.ForeignKey( + name: "fk_tenant_business_licenses_business_lines_business_line_id", + column: x => x.business_line_id, + principalTable: "business_lines", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_tenant_business_licenses_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "exam_paper_specification_versions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + specification_id = table.Column(type: "uuid", nullable: false), + version_no = table.Column(type: "integer", nullable: false), + exam_year = table.Column(type: "integer", nullable: true), + duration_minutes = table.Column(type: "integer", nullable: false), + total_score = table.Column(type: "numeric", nullable: false), + sections = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + published_at = table.Column(type: "timestamp with time zone", nullable: true), + created_by = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_exam_paper_specification_versions", x => x.id); + table.UniqueConstraint("ak_exam_paper_specification_versions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_exam_paper_spec_duration", "duration_minutes > 0"); + table.CheckConstraint("ck_exam_paper_spec_total_score", "total_score > 0"); + table.ForeignKey( + name: "fk_exam_paper_specification_versions_exam_paper_specifications~", + columns: x => new { x.tenant_id, x.specification_id }, + principalTable: "exam_paper_specifications", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_exam_paper_specification_versions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "target_nodes", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + target_dimension_definition_id = table.Column(type: "uuid", nullable: false), + parent_id = table.Column(type: "uuid", nullable: true), + code = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + path = table.Column(type: "ltree", nullable: false), + is_active = table.Column(type: "boolean", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_target_nodes", x => x.id); + table.ForeignKey( + name: "fk_target_nodes_target_dimension_definitions_target_dimension_~", + column: x => x.target_dimension_definition_id, + principalTable: "target_dimension_definitions", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_target_nodes_target_nodes_parent_id", + column: x => x.parent_id, + principalTable: "target_nodes", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "assessment_policies", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + business_line_id = table.Column(type: "uuid", nullable: false), + code = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + current_version_id = table.Column(type: "uuid", nullable: true), + is_active = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_assessment_policies", x => x.id); + table.UniqueConstraint("ak_assessment_policies_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_assessment_policies_business_lines_business_line_id", + column: x => x.business_line_id, + principalTable: "business_lines", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_assessment_policies_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "assessment_policy_versions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + assessment_policy_id = table.Column(type: "uuid", nullable: false), + version_no = table.Column(type: "integer", nullable: false), + grading_mode = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + default_score = table.Column(type: "numeric", nullable: false), + rounding_scale = table.Column(type: "integer", nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + published_at = table.Column(type: "timestamp with time zone", nullable: true), + created_by = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_assessment_policy_versions", x => x.id); + table.UniqueConstraint("ak_assessment_policy_versions_tenant_id_assessment_policy_id_id", x => new { x.tenant_id, x.assessment_policy_id, x.id }); + table.UniqueConstraint("ak_assessment_policy_versions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_assessment_policy_default_score", "default_score > 0"); + table.CheckConstraint("ck_assessment_policy_rounding", "rounding_scale between 0 and 6"); + table.ForeignKey( + name: "fk_assessment_policy_versions_assessment_policies_tenant_id_as~", + columns: x => new { x.tenant_id, x.assessment_policy_id }, + principalTable: "assessment_policies", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_assessment_policy_versions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "assessment_score_bands", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + assessment_policy_version_id = table.Column(type: "uuid", nullable: false), + min_score = table.Column(type: "numeric", nullable: false), + max_score = table.Column(type: "numeric", nullable: false), + label = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_assessment_score_bands", x => x.id); + table.UniqueConstraint("ak_assessment_score_bands_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_assessment_score_band", "max_score >= min_score"); + table.ForeignKey( + name: "fk_assessment_score_bands_assessment_policy_versions_tenant_id~", + columns: x => new { x.tenant_id, x.assessment_policy_version_id }, + principalTable: "assessment_policy_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_assessment_score_bands_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "objective_grading_rules", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + assessment_policy_version_id = table.Column(type: "uuid", nullable: false), + rule_type = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + configuration = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_objective_grading_rules", x => x.id); + table.UniqueConstraint("ak_objective_grading_rules_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_objective_grading_rules_assessment_policy_versions_tenant_i~", + columns: x => new { x.tenant_id, x.assessment_policy_version_id }, + principalTable: "assessment_policy_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_objective_grading_rules_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "score_adjustment_rules", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + assessment_policy_version_id = table.Column(type: "uuid", nullable: false), + priority = table.Column(type: "integer", nullable: false), + rule_type = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + configuration = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_score_adjustment_rules", x => x.id); + table.UniqueConstraint("ak_score_adjustment_rules_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_score_adjustment_rules_assessment_policy_versions_tenant_id~", + columns: x => new { x.tenant_id, x.assessment_policy_version_id }, + principalTable: "assessment_policy_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_score_adjustment_rules_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "subjective_rubrics", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + assessment_policy_version_id = table.Column(type: "uuid", nullable: false), + code = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + max_score = table.Column(type: "numeric", nullable: false), + criteria = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_subjective_rubrics", x => x.id); + table.UniqueConstraint("ak_subjective_rubrics_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_subjective_rubric_max_score", "max_score > 0"); + table.ForeignKey( + name: "fk_subjective_rubrics_assessment_policy_versions_tenant_id_ass~", + columns: x => new { x.tenant_id, x.assessment_policy_version_id }, + principalTable: "assessment_policy_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_subjective_rubrics_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "audience_segment_members", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + content_release_id = table.Column(type: "uuid", nullable: false), + audience_segment_id = table.Column(type: "uuid", nullable: false), + exam_target_profile_version_id = table.Column(type: "uuid", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_audience_segment_members", x => x.id); + table.UniqueConstraint("ak_audience_segment_members_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_audience_segment_members_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "audience_segments", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + content_release_id = table.Column(type: "uuid", nullable: false), + rule_hash = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + segment_ordinal = table.Column(type: "integer", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_audience_segments", x => x.id); + table.UniqueConstraint("ak_audience_segments_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_audience_segments_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "blueprint_releases", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + content_release_id = table.Column(type: "uuid", nullable: false), + collection_release_id = table.Column(type: "uuid", nullable: true), + source_blueprint_id = table.Column(type: "uuid", nullable: false), + exam_paper_specification_version_id = table.Column(type: "uuid", nullable: false), + version_no = table.Column(type: "integer", nullable: false), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + assembly_rules = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + published_at = table.Column(type: "timestamp with time zone", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_blueprint_releases", x => x.id); + table.UniqueConstraint("ak_blueprint_releases_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_blueprint_releases_exam_paper_specification_versions_tenant~", + columns: x => new { x.tenant_id, x.exam_paper_specification_version_id }, + principalTable: "exam_paper_specification_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_blueprint_releases_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "business_target_dimension_requirements", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + business_target_policy_version_id = table.Column(type: "uuid", nullable: false), + target_dimension_definition_id = table.Column(type: "uuid", nullable: false), + min_values = table.Column(type: "integer", nullable: false), + max_values = table.Column(type: "integer", nullable: false), + required_for_profile = table.Column(type: "boolean", nullable: false), + sort_order = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_business_target_dimension_requirements", x => x.id); + table.CheckConstraint("ck_business_target_dimension_cardinality", "min_values >= 0 and max_values >= min_values"); + table.ForeignKey( + name: "fk_business_target_dimension_requirements_target_dimension_def~", + column: x => x.target_dimension_definition_id, + principalTable: "target_dimension_definitions", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "business_target_policies", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + business_line_id = table.Column(type: "uuid", nullable: false), + current_version_id = table.Column(type: "uuid", nullable: true), + is_active = table.Column(type: "boolean", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_business_target_policies", x => x.id); + table.ForeignKey( + name: "fk_business_target_policies_business_lines_business_line_id", + column: x => x.business_line_id, + principalTable: "business_lines", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "business_target_policy_versions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + business_target_policy_id = table.Column(type: "uuid", nullable: false), + version_no = table.Column(type: "integer", nullable: false), + max_active_targets = table.Column(type: "integer", nullable: false), + max_primary_targets = table.Column(type: "integer", nullable: false), + max_alternate_targets = table.Column(type: "integer", nullable: false), + target_change_cooldown_days = table.Column(type: "integer", nullable: false), + includes_base_content = table.Column(type: "boolean", nullable: false), + selection_mode = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + published_at = table.Column(type: "timestamp with time zone", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_business_target_policy_versions", x => x.id); + table.UniqueConstraint("ak_business_target_policy_versions_business_target_policy_id_id", x => new { x.business_target_policy_id, x.id }); + table.CheckConstraint("ck_business_target_policy_active", "max_active_targets between 1 and 16"); + table.CheckConstraint("ck_business_target_policy_alternate", "max_alternate_targets between 0 and max_active_targets"); + table.CheckConstraint("ck_business_target_policy_cooldown", "target_change_cooldown_days between 0 and 365"); + table.CheckConstraint("ck_business_target_policy_primary", "max_primary_targets between 0 and max_active_targets"); + table.ForeignKey( + name: "fk_business_target_policy_versions_business_target_policies_bu~", + column: x => x.business_target_policy_id, + principalTable: "business_target_policies", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "class_assignment_grants", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + class_id = table.Column(type: "uuid", nullable: false), + product_access_manifest_version_id = table.Column(type: "uuid", nullable: false), + resource_type = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + resource_id = table.Column(type: "uuid", nullable: false), + starts_at = table.Column(type: "timestamp with time zone", nullable: false), + ends_at = table.Column(type: "timestamp with time zone", nullable: true), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + created_by = table.Column(type: "uuid", nullable: true), + revoked_by = table.Column(type: "uuid", nullable: true), + revoked_reason = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_class_assignment_grants", x => x.id); + table.UniqueConstraint("ak_class_assignment_grants_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_class_assignment_grant_period", "ends_at is null or ends_at > starts_at"); + table.ForeignKey( + name: "fk_class_assignment_grants_tenant_classes_tenant_id_class_id", + columns: x => new { x.tenant_id, x.class_id }, + principalTable: "tenant_classes", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_class_assignment_grants_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "collection_release_questions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + collection_release_id = table.Column(type: "uuid", nullable: false), + content_release_question_id = table.Column(type: "uuid", nullable: false), + sort_order = table.Column(type: "integer", nullable: false), + score = table.Column(type: "numeric", nullable: true), + required = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_collection_release_questions", x => x.id); + table.UniqueConstraint("ak_collection_release_questions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_collection_release_questions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "collection_releases", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + content_release_id = table.Column(type: "uuid", nullable: false), + source_collection_id = table.Column(type: "uuid", nullable: false), + version_no = table.Column(type: "integer", nullable: false), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + published_at = table.Column(type: "timestamp with time zone", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_collection_releases", x => x.id); + table.UniqueConstraint("ak_collection_releases_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_collection_releases_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "content_release_questions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + content_release_id = table.Column(type: "uuid", nullable: false), + audience_segment_id = table.Column(type: "uuid", nullable: false), + curriculum_node_id = table.Column(type: "uuid", nullable: false), + question_placement_id = table.Column(type: "uuid", nullable: false), + question_asset_owner_tenant_id = table.Column(type: "uuid", nullable: false), + question_asset_id = table.Column(type: "uuid", nullable: false), + question_revision_id = table.Column(type: "uuid", nullable: false), + assessment_policy_version_id = table.Column(type: "uuid", nullable: false), + ordinal = table.Column(type: "integer", nullable: false), + difficulty = table.Column(type: "integer", nullable: true), + question_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_content_release_questions", x => x.id); + table.UniqueConstraint("ak_content_release_questions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_content_release_questions_assessment_policy_versions_tenant~", + columns: x => new { x.tenant_id, x.assessment_policy_version_id }, + principalTable: "assessment_policy_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_content_release_questions_audience_segments_tenant_id_audie~", + columns: x => new { x.tenant_id, x.audience_segment_id }, + principalTable: "audience_segments", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_content_release_questions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "content_releases", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + business_line_id = table.Column(type: "uuid", nullable: false), + curriculum_version_id = table.Column(type: "uuid", nullable: false), + release_no = table.Column(type: "integer", nullable: false), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + compiler_version = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + source_fingerprint = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + published_at = table.Column(type: "timestamp with time zone", nullable: true), + created_by = table.Column(type: "uuid", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_content_releases", x => x.id); + table.UniqueConstraint("ak_content_releases_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_content_releases_business_lines_business_line_id", + column: x => x.business_line_id, + principalTable: "business_lines", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_content_releases_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "platform_content_package_versions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + package_code = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + version_no = table.Column(type: "integer", nullable: false), + business_line_id = table.Column(type: "uuid", nullable: false), + content_release_id = table.Column(type: "uuid", nullable: false), + content_hash = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + published_at = table.Column(type: "timestamp with time zone", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_platform_content_package_versions", x => x.id); + table.UniqueConstraint("ak_platform_content_package_versions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_platform_content_package_versions_content_releases_tenant_i~", + columns: x => new { x.tenant_id, x.content_release_id }, + principalTable: "content_releases", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_platform_content_package_versions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "platform_content_package_cell_projections", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + package_owner_tenant_id = table.Column(type: "uuid", nullable: false), + package_code = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + platform_content_package_version_id = table.Column(type: "uuid", nullable: false), + cell_id = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + event_sequence = table.Column(type: "bigint", nullable: false), + content_hash = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + projected_at = table.Column(type: "timestamp with time zone", nullable: true), + last_error = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_platform_content_package_cell_projections", x => x.id); + table.ForeignKey( + name: "fk_platform_content_package_cell_projections_platform_content_~", + columns: x => new { x.package_owner_tenant_id, x.platform_content_package_version_id }, + principalTable: "platform_content_package_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "curricula", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + business_line_id = table.Column(type: "uuid", nullable: false), + code = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + current_version_id = table.Column(type: "uuid", nullable: true), + is_active = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_curricula", x => x.id); + table.UniqueConstraint("ak_curricula_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_curricula_business_lines_business_line_id", + column: x => x.business_line_id, + principalTable: "business_lines", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_curricula_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "curriculum_versions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + curriculum_id = table.Column(type: "uuid", nullable: false), + version_no = table.Column(type: "integer", nullable: false), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + exam_year = table.Column(type: "integer", nullable: true), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + published_at = table.Column(type: "timestamp with time zone", nullable: true), + created_by = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_curriculum_versions", x => x.id); + table.UniqueConstraint("ak_curriculum_versions_tenant_id_curriculum_id_id", x => new { x.tenant_id, x.curriculum_id, x.id }); + table.UniqueConstraint("ak_curriculum_versions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_curriculum_versions_curricula_tenant_id_curriculum_id", + columns: x => new { x.tenant_id, x.curriculum_id }, + principalTable: "curricula", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_curriculum_versions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "curriculum_nodes", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + curriculum_version_id = table.Column(type: "uuid", nullable: false), + parent_id = table.Column(type: "uuid", nullable: true), + knowledge_concept_id = table.Column(type: "uuid", nullable: true), + code = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + kind = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + sort_order = table.Column(type: "integer", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_curriculum_nodes", x => x.id); + table.UniqueConstraint("ak_curriculum_nodes_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_curriculum_nodes_curriculum_nodes_tenant_id_parent_id", + columns: x => new { x.tenant_id, x.parent_id }, + principalTable: "curriculum_nodes", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_curriculum_nodes_curriculum_versions_tenant_id_curriculum_v~", + columns: x => new { x.tenant_id, x.curriculum_version_id }, + principalTable: "curriculum_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_curriculum_nodes_knowledge_concepts_tenant_id_knowledge_con~", + columns: x => new { x.tenant_id, x.knowledge_concept_id }, + principalTable: "knowledge_concepts", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_curriculum_nodes_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "exam_target_profile_values", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + exam_target_profile_version_id = table.Column(type: "uuid", nullable: false), + target_dimension_definition_id = table.Column(type: "uuid", nullable: false), + target_node_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_exam_target_profile_values", x => x.id); + table.ForeignKey( + name: "fk_exam_target_profile_values_target_dimension_definitions_tar~", + column: x => x.target_dimension_definition_id, + principalTable: "target_dimension_definitions", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_exam_target_profile_values_target_nodes_target_node_id", + column: x => x.target_node_id, + principalTable: "target_nodes", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "exam_target_profile_versions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + exam_target_profile_id = table.Column(type: "uuid", nullable: false), + version_no = table.Column(type: "integer", nullable: false), + display_name = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + exam_year = table.Column(type: "integer", nullable: true), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + published_at = table.Column(type: "timestamp with time zone", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_exam_target_profile_versions", x => x.id); + table.UniqueConstraint("ak_exam_target_profile_versions_exam_target_profile_id_id", x => new { x.exam_target_profile_id, x.id }); + }); + + migrationBuilder.CreateTable( + name: "exam_target_profiles", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + business_line_id = table.Column(type: "uuid", nullable: false), + code = table.Column(type: "character varying(150)", maxLength: 150, nullable: false), + name = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + current_version_id = table.Column(type: "uuid", nullable: true), + is_active = table.Column(type: "boolean", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_exam_target_profiles", x => x.id); + table.ForeignKey( + name: "fk_exam_target_profiles_business_lines_business_line_id", + column: x => x.business_line_id, + principalTable: "business_lines", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_exam_target_profiles_exam_target_profile_versions_id_curren~", + columns: x => new { x.id, x.current_version_id }, + principalTable: "exam_target_profile_versions", + principalColumns: new[] { "exam_target_profile_id", "id" }, + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "student_target_selection_history", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + user_id = table.Column(type: "uuid", nullable: false), + business_line_id = table.Column(type: "uuid", nullable: false), + exam_target_profile_version_id = table.Column(type: "uuid", nullable: false), + role = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + entitlement_id = table.Column(type: "uuid", nullable: true), + effective_at = table.Column(type: "timestamp with time zone", nullable: false), + ended_at = table.Column(type: "timestamp with time zone", nullable: true), + is_current = table.Column(type: "boolean", nullable: false), + changed_by = table.Column(type: "uuid", nullable: true), + reason = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_student_target_selection_history", x => x.id); + table.UniqueConstraint("ak_student_target_selection_history_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_student_target_selection_history_business_lines_business_li~", + column: x => x.business_line_id, + principalTable: "business_lines", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_student_target_selection_history_exam_target_profile_versio~", + column: x => x.exam_target_profile_version_id, + principalTable: "exam_target_profile_versions", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_student_target_selection_history_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_student_target_selection_history_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "tenant_licensed_targets", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + tenant_business_license_id = table.Column(type: "uuid", nullable: false), + exam_target_profile_version_id = table.Column(type: "uuid", nullable: false), + is_base_target = table.Column(type: "boolean", nullable: false), + starts_at = table.Column(type: "timestamp with time zone", nullable: false), + ends_at = table.Column(type: "timestamp with time zone", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_tenant_licensed_targets", x => x.id); + table.UniqueConstraint("ak_tenant_licensed_targets_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_tenant_licensed_target_period", "ends_at is null or ends_at > starts_at"); + table.ForeignKey( + name: "fk_tenant_licensed_targets_exam_target_profile_versions_exam_t~", + column: x => x.exam_target_profile_version_id, + principalTable: "exam_target_profile_versions", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_tenant_licensed_targets_tenant_business_licenses_tenant_id_~", + columns: x => new { x.tenant_id, x.tenant_business_license_id }, + principalTable: "tenant_business_licenses", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_tenant_licensed_targets_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "product_access_manifest_versions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + product_access_manifest_id = table.Column(type: "uuid", nullable: false), + version_no = table.Column(type: "integer", nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + max_active_targets = table.Column(type: "integer", nullable: false), + max_alternate_targets = table.Column(type: "integer", nullable: false), + daily_question_limit = table.Column(type: "integer", nullable: true), + total_question_limit = table.Column(type: "integer", nullable: true), + published_at = table.Column(type: "timestamp with time zone", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_product_access_manifest_versions", x => x.id); + table.UniqueConstraint("ak_product_access_manifest_versions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.UniqueConstraint("ak_product_access_manifest_versions_tenant_id_product_access_m~", x => new { x.tenant_id, x.product_access_manifest_id, x.id }); + table.CheckConstraint("ck_product_manifest_active_targets", "max_active_targets between 1 and 16"); + table.CheckConstraint("ck_product_manifest_alternate_targets", "max_alternate_targets between 0 and max_active_targets"); + table.CheckConstraint("ck_product_manifest_daily_question_limit", "daily_question_limit is null or daily_question_limit >= 0"); + table.CheckConstraint("ck_product_manifest_total_question_limit", "total_question_limit is null or total_question_limit >= 0"); + table.ForeignKey( + name: "fk_product_access_manifest_versions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "product_access_manifests", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + learning_product_id = table.Column(type: "uuid", nullable: false), + current_version_id = table.Column(type: "uuid", nullable: true), + is_active = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_product_access_manifests", x => x.id); + table.UniqueConstraint("ak_product_access_manifests_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_product_access_manifests_learning_products_tenant_id_learni~", + columns: x => new { x.tenant_id, x.learning_product_id }, + principalTable: "learning_products", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_product_access_manifests_product_access_manifest_versions_t~", + columns: x => new { x.tenant_id, x.id, x.current_version_id }, + principalTable: "product_access_manifest_versions", + principalColumns: new[] { "tenant_id", "product_access_manifest_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_product_access_manifests_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "product_manifest_releases", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + product_access_manifest_version_id = table.Column(type: "uuid", nullable: false), + content_owner_tenant_id = table.Column(type: "uuid", nullable: false), + content_release_id = table.Column(type: "uuid", nullable: false), + platform_content_package_owner_tenant_id = table.Column(type: "uuid", nullable: true), + platform_content_package_version_id = table.Column(type: "uuid", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_product_manifest_releases", x => x.id); + table.UniqueConstraint("ak_product_manifest_releases_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_product_manifest_release_platform_pair", "(platform_content_package_owner_tenant_id is null) = (platform_content_package_version_id is null)"); + table.ForeignKey( + name: "fk_product_manifest_releases_content_releases_content_owner_te~", + columns: x => new { x.content_owner_tenant_id, x.content_release_id }, + principalTable: "content_releases", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_product_manifest_releases_platform_content_package_versions~", + columns: x => new { x.platform_content_package_owner_tenant_id, x.platform_content_package_version_id }, + principalTable: "platform_content_package_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_product_manifest_releases_product_access_manifest_versions_~", + columns: x => new { x.tenant_id, x.product_access_manifest_version_id }, + principalTable: "product_access_manifest_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_product_manifest_releases_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "product_manifest_resources", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + product_access_manifest_version_id = table.Column(type: "uuid", nullable: false), + resource_type = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + resource_id = table.Column(type: "uuid", nullable: false), + display_name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + sort_order = table.Column(type: "integer", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_product_manifest_resources", x => x.id); + table.UniqueConstraint("ak_product_manifest_resources_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_product_manifest_resources_product_access_manifest_versions~", + columns: x => new { x.tenant_id, x.product_access_manifest_version_id }, + principalTable: "product_access_manifest_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_product_manifest_resources_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "product_manifest_targets", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + product_access_manifest_version_id = table.Column(type: "uuid", nullable: false), + exam_target_profile_version_id = table.Column(type: "uuid", nullable: false), + may_be_primary = table.Column(type: "boolean", nullable: false), + may_be_alternate = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_product_manifest_targets", x => x.id); + table.UniqueConstraint("ak_product_manifest_targets_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_product_manifest_target_role", "may_be_primary or may_be_alternate"); + table.ForeignKey( + name: "fk_product_manifest_targets_exam_target_profile_versions_exam_~", + column: x => x.exam_target_profile_version_id, + principalTable: "exam_target_profile_versions", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_product_manifest_targets_product_access_manifest_versions_t~", + columns: x => new { x.tenant_id, x.product_access_manifest_version_id }, + principalTable: "product_access_manifest_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_product_manifest_targets_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "student_entitlements_v2", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + user_id = table.Column(type: "uuid", nullable: false), + learning_product_id = table.Column(type: "uuid", nullable: false), + product_access_manifest_version_id = table.Column(type: "uuid", nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + starts_at = table.Column(type: "timestamp with time zone", nullable: false), + ends_at = table.Column(type: "timestamp with time zone", nullable: true), + source_type = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + source_id = table.Column(type: "uuid", nullable: true), + used_question_count = table.Column(type: "integer", nullable: false), + revoked_at = table.Column(type: "timestamp with time zone", nullable: true), + revoked_by = table.Column(type: "uuid", nullable: true), + revoked_reason = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_student_entitlements_v2", x => x.id); + table.UniqueConstraint("ak_student_entitlements_v2_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_student_entitlement_v2_period", "ends_at is null or ends_at > starts_at"); + table.CheckConstraint("ck_student_entitlements_v2_used_question_count", "used_question_count >= 0"); + table.ForeignKey( + name: "fk_student_entitlements_v2_learning_products_tenant_id_learnin~", + columns: x => new { x.tenant_id, x.learning_product_id }, + principalTable: "learning_products", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_student_entitlements_v2_product_access_manifest_versions_te~", + columns: x => new { x.tenant_id, x.product_access_manifest_version_id }, + principalTable: "product_access_manifest_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_student_entitlements_v2_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_student_entitlements_v2_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "question_assets", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + family_id = table.Column(type: "uuid", nullable: true), + current_revision_id = table.Column(type: "uuid", nullable: true), + delivery_fingerprint = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + created_by = table.Column(type: "uuid", nullable: true), + updated_by = table.Column(type: "uuid", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_question_assets", x => x.id); + table.UniqueConstraint("ak_question_assets_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_question_assets_question_families_tenant_id_family_id", + columns: x => new { x.tenant_id, x.family_id }, + principalTable: "question_families", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_question_assets_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "question_placements", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + question_asset_owner_tenant_id = table.Column(type: "uuid", nullable: false), + question_asset_id = table.Column(type: "uuid", nullable: false), + curriculum_version_id = table.Column(type: "uuid", nullable: false), + curriculum_node_id = table.Column(type: "uuid", nullable: false), + assessment_policy_version_id = table.Column(type: "uuid", nullable: false), + difficulty_override = table.Column(type: "integer", nullable: true), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + valid_from = table.Column(type: "timestamp with time zone", nullable: true), + valid_to = table.Column(type: "timestamp with time zone", nullable: true), + created_by = table.Column(type: "uuid", nullable: true), + updated_by = table.Column(type: "uuid", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_question_placements", x => x.id); + table.UniqueConstraint("ak_question_placements_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_question_placements_valid_period", "valid_to is null or valid_from is null or valid_to > valid_from"); + table.ForeignKey( + name: "fk_question_placements_assessment_policy_versions_tenant_id_as~", + columns: x => new { x.tenant_id, x.assessment_policy_version_id }, + principalTable: "assessment_policy_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_question_placements_curriculum_nodes_tenant_id_curriculum_n~", + columns: x => new { x.tenant_id, x.curriculum_node_id }, + principalTable: "curriculum_nodes", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_question_placements_curriculum_versions_tenant_id_curriculu~", + columns: x => new { x.tenant_id, x.curriculum_version_id }, + principalTable: "curriculum_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_question_placements_question_assets_question_asset_owner_te~", + columns: x => new { x.question_asset_owner_tenant_id, x.question_asset_id }, + principalTable: "question_assets", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_question_placements_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "question_revisions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + question_asset_id = table.Column(type: "uuid", nullable: false), + revision_no = table.Column(type: "integer", nullable: false), + question_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + type_label = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + difficulty = table.Column(type: "integer", nullable: true), + normalized_content = table.Column(type: "text", nullable: false), + content = table.Column(type: "text", nullable: true), + options = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"), + correct_option_index = table.Column(type: "integer", nullable: true), + correct_option_indices = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"), + answer_text = table.Column(type: "text", nullable: true), + explanation = table.Column(type: "text", nullable: true), + sub_questions = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"), + code_lang = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + code_template = table.Column(type: "text", nullable: true), + delivery_fingerprint = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + created_by = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_question_revisions", x => x.id); + table.UniqueConstraint("ak_question_revisions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.UniqueConstraint("ak_question_revisions_tenant_id_question_asset_id_id", x => new { x.tenant_id, x.question_asset_id, x.id }); + table.ForeignKey( + name: "fk_question_revisions_question_assets_tenant_id_question_asset~", + columns: x => new { x.tenant_id, x.question_asset_id }, + principalTable: "question_assets", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_question_revisions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "question_placement_concepts", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + placement_id = table.Column(type: "uuid", nullable: false), + knowledge_concept_id = table.Column(type: "uuid", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_question_placement_concepts", x => x.id); + table.UniqueConstraint("ak_question_placement_concepts_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_question_placement_concepts_knowledge_concepts_tenant_id_kn~", + columns: x => new { x.tenant_id, x.knowledge_concept_id }, + principalTable: "knowledge_concepts", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_question_placement_concepts_question_placements_tenant_id_p~", + columns: x => new { x.tenant_id, x.placement_id }, + principalTable: "question_placements", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_question_placement_concepts_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "question_placement_rule_groups", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + placement_id = table.Column(type: "uuid", nullable: false), + group_order = table.Column(type: "integer", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_question_placement_rule_groups", x => x.id); + table.UniqueConstraint("ak_question_placement_rule_groups_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_question_placement_rule_groups_question_placements_tenant_i~", + columns: x => new { x.tenant_id, x.placement_id }, + principalTable: "question_placements", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_question_placement_rule_groups_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "wrong_questions_v2", + columns: table => new + { + user_id = table.Column(type: "uuid", nullable: false), + question_asset_owner_tenant_id = table.Column(type: "uuid", nullable: false), + question_asset_id = table.Column(type: "uuid", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + last_question_revision_id = table.Column(type: "uuid", nullable: false), + last_question_placement_id = table.Column(type: "uuid", nullable: false), + wrong_count = table.Column(type: "integer", nullable: false, defaultValue: 1), + last_wrong_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + resolved_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_wrong_questions_v2", x => new { x.tenant_id, x.user_id, x.question_asset_owner_tenant_id, x.question_asset_id }); + table.ForeignKey( + name: "fk_wrong_questions_v2_question_assets_question_asset_owner_ten~", + columns: x => new { x.question_asset_owner_tenant_id, x.question_asset_id }, + principalTable: "question_assets", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_wrong_questions_v2_question_revisions_question_asset_owner_~", + columns: x => new { x.question_asset_owner_tenant_id, x.question_asset_id, x.last_question_revision_id }, + principalTable: "question_revisions", + principalColumns: new[] { "tenant_id", "question_asset_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_wrong_questions_v2_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_wrong_questions_v2_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "question_placement_rule_conditions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + rule_group_id = table.Column(type: "uuid", nullable: false), + target_dimension_definition_id = table.Column(type: "uuid", nullable: false), + target_node_id = table.Column(type: "uuid", nullable: false), + @operator = table.Column(name: "operator", type: "character varying(32)", maxLength: 32, nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_question_placement_rule_conditions", x => x.id); + table.UniqueConstraint("ak_question_placement_rule_conditions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_question_placement_rule_conditions_question_placement_rule_~", + columns: x => new { x.tenant_id, x.rule_group_id }, + principalTable: "question_placement_rule_groups", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_question_placement_rule_conditions_target_dimension_definit~", + column: x => x.target_dimension_definition_id, + principalTable: "target_dimension_definitions", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_question_placement_rule_conditions_target_nodes_target_node~", + column: x => x.target_node_id, + principalTable: "target_nodes", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_question_placement_rule_conditions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_practice_session_questions_tenant_id_practice_session_id_co~", + table: "practice_session_questions", + columns: new[] { "tenant_id", "practice_session_id", "content_release_question_id" }, + unique: true, + filter: "content_release_question_id is not null"); + + migrationBuilder.CreateIndex( + name: "ux_answer_records_idempotency", + table: "answer_records", + columns: new[] { "tenant_id", "user_id", "practice_session_id", "idempotency_key" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_assessment_policies_business_line_id", + table: "assessment_policies", + column: "business_line_id"); + + migrationBuilder.CreateIndex( + name: "ix_assessment_policies_tenant_id_code", + table: "assessment_policies", + columns: new[] { "tenant_id", "code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_assessment_policies_tenant_id_id_current_version_id", + table: "assessment_policies", + columns: new[] { "tenant_id", "id", "current_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_assessment_policy_versions_tenant_id_assessment_policy_id_v~", + table: "assessment_policy_versions", + columns: new[] { "tenant_id", "assessment_policy_id", "version_no" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_assessment_score_bands_tenant_id_assessment_policy_version_~", + table: "assessment_score_bands", + columns: new[] { "tenant_id", "assessment_policy_version_id", "min_score", "max_score" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_audience_segment_members_exam_target_profile_version_id", + table: "audience_segment_members", + column: "exam_target_profile_version_id"); + + migrationBuilder.CreateIndex( + name: "ix_audience_segment_members_tenant_id_audience_segment_id", + table: "audience_segment_members", + columns: new[] { "tenant_id", "audience_segment_id" }); + + migrationBuilder.CreateIndex( + name: "ix_audience_segment_members_tenant_id_content_release_id_audie~", + table: "audience_segment_members", + columns: new[] { "tenant_id", "content_release_id", "audience_segment_id", "exam_target_profile_version_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_audience_segment_members_tenant_id_content_release_id_exam_~", + table: "audience_segment_members", + columns: new[] { "tenant_id", "content_release_id", "exam_target_profile_version_id", "audience_segment_id" }); + + migrationBuilder.CreateIndex( + name: "ix_audience_segments_tenant_id_content_release_id_rule_hash", + table: "audience_segments", + columns: new[] { "tenant_id", "content_release_id", "rule_hash" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_audience_segments_tenant_id_content_release_id_segment_ordi~", + table: "audience_segments", + columns: new[] { "tenant_id", "content_release_id", "segment_ordinal" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_blueprint_releases_tenant_id_collection_release_id", + table: "blueprint_releases", + columns: new[] { "tenant_id", "collection_release_id" }); + + migrationBuilder.CreateIndex( + name: "ix_blueprint_releases_tenant_id_content_release_id", + table: "blueprint_releases", + columns: new[] { "tenant_id", "content_release_id" }); + + migrationBuilder.CreateIndex( + name: "ix_blueprint_releases_tenant_id_exam_paper_specification_versi~", + table: "blueprint_releases", + columns: new[] { "tenant_id", "exam_paper_specification_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_blueprint_releases_tenant_id_source_blueprint_id_version_no", + table: "blueprint_releases", + columns: new[] { "tenant_id", "source_blueprint_id", "version_no" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_business_target_dimension_requirements_business_target_poli~", + table: "business_target_dimension_requirements", + columns: new[] { "business_target_policy_version_id", "target_dimension_definition_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_business_target_dimension_requirements_target_dimension_def~", + table: "business_target_dimension_requirements", + column: "target_dimension_definition_id"); + + migrationBuilder.CreateIndex( + name: "ix_business_target_policies_business_line_id", + table: "business_target_policies", + column: "business_line_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_business_target_policies_id_current_version_id", + table: "business_target_policies", + columns: new[] { "id", "current_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_business_target_policy_versions_business_target_policy_id_v~", + table: "business_target_policy_versions", + columns: new[] { "business_target_policy_id", "version_no" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_class_assignment_grants_tenant_id_class_id_status_starts_at", + table: "class_assignment_grants", + columns: new[] { "tenant_id", "class_id", "status", "starts_at" }); + + migrationBuilder.CreateIndex( + name: "ix_class_assignment_grants_tenant_id_product_access_manifest_v~", + table: "class_assignment_grants", + columns: new[] { "tenant_id", "product_access_manifest_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_class_assignment_grants_tenant_id_resource_type_resource_id~", + table: "class_assignment_grants", + columns: new[] { "tenant_id", "resource_type", "resource_id", "status" }); + + migrationBuilder.CreateIndex( + name: "ix_collection_release_questions_tenant_id_content_release_ques~", + table: "collection_release_questions", + columns: new[] { "tenant_id", "content_release_question_id" }); + + migrationBuilder.CreateIndex( + name: "ux_collection_release_question_item", + table: "collection_release_questions", + columns: new[] { "tenant_id", "collection_release_id", "content_release_question_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ux_collection_release_question_order", + table: "collection_release_questions", + columns: new[] { "tenant_id", "collection_release_id", "sort_order" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_collection_releases_tenant_id_content_release_id", + table: "collection_releases", + columns: new[] { "tenant_id", "content_release_id" }); + + migrationBuilder.CreateIndex( + name: "ix_collection_releases_tenant_id_source_collection_id_version_~", + table: "collection_releases", + columns: new[] { "tenant_id", "source_collection_id", "version_no" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_content_release_question_candidates", + table: "content_release_questions", + columns: new[] { "tenant_id", "content_release_id", "audience_segment_id", "curriculum_node_id", "status", "difficulty", "question_type", "ordinal" }); + + migrationBuilder.CreateIndex( + name: "ix_content_release_questions_question_asset_owner_tenant_id_qu~", + table: "content_release_questions", + columns: new[] { "question_asset_owner_tenant_id", "question_asset_id", "question_revision_id" }); + + migrationBuilder.CreateIndex( + name: "ix_content_release_questions_tenant_id_assessment_policy_versi~", + table: "content_release_questions", + columns: new[] { "tenant_id", "assessment_policy_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_content_release_questions_tenant_id_audience_segment_id", + table: "content_release_questions", + columns: new[] { "tenant_id", "audience_segment_id" }); + + migrationBuilder.CreateIndex( + name: "ix_content_release_questions_tenant_id_content_release_id_audi~", + table: "content_release_questions", + columns: new[] { "tenant_id", "content_release_id", "audience_segment_id", "curriculum_node_id", "ordinal" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_content_release_questions_tenant_id_question_placement_id", + table: "content_release_questions", + columns: new[] { "tenant_id", "question_placement_id" }); + + migrationBuilder.CreateIndex( + name: "ix_content_releases_business_line_id", + table: "content_releases", + column: "business_line_id"); + + migrationBuilder.CreateIndex( + name: "ix_content_releases_tenant_id_business_line_id_status_publishe~", + table: "content_releases", + columns: new[] { "tenant_id", "business_line_id", "status", "published_at" }); + + migrationBuilder.CreateIndex( + name: "ix_content_releases_tenant_id_curriculum_version_id_release_no", + table: "content_releases", + columns: new[] { "tenant_id", "curriculum_version_id", "release_no" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_curricula_business_line_id", + table: "curricula", + column: "business_line_id"); + + migrationBuilder.CreateIndex( + name: "ix_curricula_tenant_id_code", + table: "curricula", + columns: new[] { "tenant_id", "code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_curricula_tenant_id_id_current_version_id", + table: "curricula", + columns: new[] { "tenant_id", "id", "current_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_curriculum_node_closure_tenant_id_curriculum_version_id_anc~", + table: "curriculum_node_closure", + columns: new[] { "tenant_id", "curriculum_version_id", "ancestor_node_id", "descendant_node_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_curriculum_node_closure_tenant_id_curriculum_version_id_des~", + table: "curriculum_node_closure", + columns: new[] { "tenant_id", "curriculum_version_id", "descendant_node_id", "depth" }); + + migrationBuilder.CreateIndex( + name: "ix_curriculum_nodes_tenant_id_curriculum_version_id_code", + table: "curriculum_nodes", + columns: new[] { "tenant_id", "curriculum_version_id", "code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_curriculum_nodes_tenant_id_curriculum_version_id_parent_id_~", + table: "curriculum_nodes", + columns: new[] { "tenant_id", "curriculum_version_id", "parent_id", "sort_order" }); + + migrationBuilder.CreateIndex( + name: "ix_curriculum_nodes_tenant_id_knowledge_concept_id", + table: "curriculum_nodes", + columns: new[] { "tenant_id", "knowledge_concept_id" }); + + migrationBuilder.CreateIndex( + name: "ix_curriculum_nodes_tenant_id_parent_id", + table: "curriculum_nodes", + columns: new[] { "tenant_id", "parent_id" }); + + migrationBuilder.CreateIndex( + name: "ix_curriculum_versions_tenant_id_curriculum_id_version_no", + table: "curriculum_versions", + columns: new[] { "tenant_id", "curriculum_id", "version_no" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_effective_access_projections_business_line_id", + table: "effective_access_projections", + column: "business_line_id"); + + migrationBuilder.CreateIndex( + name: "ix_effective_access_projections_tenant_id_user_id_business_lin~", + table: "effective_access_projections", + columns: new[] { "tenant_id", "user_id", "business_line_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_effective_access_projections_tenant_id_valid_until", + table: "effective_access_projections", + columns: new[] { "tenant_id", "valid_until" }); + + migrationBuilder.CreateIndex( + name: "ix_effective_access_projections_user_id", + table: "effective_access_projections", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_exam_paper_specification_versions_tenant_id_specification_i~", + table: "exam_paper_specification_versions", + columns: new[] { "tenant_id", "specification_id", "version_no" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_exam_paper_specifications_business_line_id", + table: "exam_paper_specifications", + column: "business_line_id"); + + migrationBuilder.CreateIndex( + name: "ix_exam_paper_specifications_tenant_id_code", + table: "exam_paper_specifications", + columns: new[] { "tenant_id", "code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_exam_target_profile_values_exam_target_profile_version_id_t~", + table: "exam_target_profile_values", + columns: new[] { "exam_target_profile_version_id", "target_dimension_definition_id", "target_node_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_exam_target_profile_values_target_dimension_definition_id", + table: "exam_target_profile_values", + column: "target_dimension_definition_id"); + + migrationBuilder.CreateIndex( + name: "ix_exam_target_profile_values_target_node_id", + table: "exam_target_profile_values", + column: "target_node_id"); + + migrationBuilder.CreateIndex( + name: "ix_exam_target_profile_versions_exam_target_profile_id_version~", + table: "exam_target_profile_versions", + columns: new[] { "exam_target_profile_id", "version_no" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_exam_target_profiles_business_line_id_code", + table: "exam_target_profiles", + columns: new[] { "business_line_id", "code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_exam_target_profiles_id_current_version_id", + table: "exam_target_profiles", + columns: new[] { "id", "current_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_knowledge_concepts_tenant_id_code", + table: "knowledge_concepts", + columns: new[] { "tenant_id", "code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_objective_grading_rules_tenant_id_assessment_policy_version~", + table: "objective_grading_rules", + columns: new[] { "tenant_id", "assessment_policy_version_id", "rule_type" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_platform_package_cell_owner_version", + table: "platform_content_package_cell_projections", + columns: new[] { "package_owner_tenant_id", "platform_content_package_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_platform_package_cell_version", + table: "platform_content_package_cell_projections", + columns: new[] { "platform_content_package_version_id", "cell_id" }); + + migrationBuilder.CreateIndex( + name: "ux_platform_package_cell_cursor", + table: "platform_content_package_cell_projections", + columns: new[] { "package_owner_tenant_id", "package_code", "cell_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_platform_content_package_versions_tenant_id_content_hash", + table: "platform_content_package_versions", + columns: new[] { "tenant_id", "content_hash" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_platform_content_package_versions_tenant_id_content_release~", + table: "platform_content_package_versions", + columns: new[] { "tenant_id", "content_release_id" }); + + migrationBuilder.CreateIndex( + name: "ix_platform_content_package_versions_tenant_id_package_code_ve~", + table: "platform_content_package_versions", + columns: new[] { "tenant_id", "package_code", "version_no" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_product_access_manifest_versions_tenant_id_product_access_m~", + table: "product_access_manifest_versions", + columns: new[] { "tenant_id", "product_access_manifest_id", "version_no" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_product_access_manifests_tenant_id_id_current_version_id", + table: "product_access_manifests", + columns: new[] { "tenant_id", "id", "current_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_product_access_manifests_tenant_id_learning_product_id", + table: "product_access_manifests", + columns: new[] { "tenant_id", "learning_product_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_product_manifest_releases_content_owner_tenant_id_content_r~", + table: "product_manifest_releases", + columns: new[] { "content_owner_tenant_id", "content_release_id" }); + + migrationBuilder.CreateIndex( + name: "ix_product_manifest_releases_platform_content_package_owner_te~", + table: "product_manifest_releases", + columns: new[] { "platform_content_package_owner_tenant_id", "platform_content_package_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_product_manifest_releases_tenant_id_product_access_manifest~", + table: "product_manifest_releases", + columns: new[] { "tenant_id", "product_access_manifest_version_id", "content_owner_tenant_id", "content_release_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_product_manifest_resource_order", + table: "product_manifest_resources", + columns: new[] { "tenant_id", "product_access_manifest_version_id", "sort_order", "id" }); + + migrationBuilder.CreateIndex( + name: "ux_product_manifest_resource", + table: "product_manifest_resources", + columns: new[] { "tenant_id", "product_access_manifest_version_id", "resource_type", "resource_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_product_manifest_targets_exam_target_profile_version_id", + table: "product_manifest_targets", + column: "exam_target_profile_version_id"); + + migrationBuilder.CreateIndex( + name: "ix_product_manifest_targets_tenant_id_product_access_manifest_~", + table: "product_manifest_targets", + columns: new[] { "tenant_id", "product_access_manifest_version_id", "exam_target_profile_version_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_question_assets_tenant_id_delivery_fingerprint", + table: "question_assets", + columns: new[] { "tenant_id", "delivery_fingerprint" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_question_assets_tenant_id_family_id", + table: "question_assets", + columns: new[] { "tenant_id", "family_id" }); + + migrationBuilder.CreateIndex( + name: "ix_question_assets_tenant_id_id_current_revision_id", + table: "question_assets", + columns: new[] { "tenant_id", "id", "current_revision_id" }); + + migrationBuilder.CreateIndex( + name: "ix_question_placement_concepts_tenant_id_knowledge_concept_id", + table: "question_placement_concepts", + columns: new[] { "tenant_id", "knowledge_concept_id" }); + + migrationBuilder.CreateIndex( + name: "ix_question_placement_concepts_tenant_id_placement_id_knowledg~", + table: "question_placement_concepts", + columns: new[] { "tenant_id", "placement_id", "knowledge_concept_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_question_placement_rule_conditions_target_dimension_definit~", + table: "question_placement_rule_conditions", + column: "target_dimension_definition_id"); + + migrationBuilder.CreateIndex( + name: "ix_question_placement_rule_conditions_target_node_id", + table: "question_placement_rule_conditions", + column: "target_node_id"); + + migrationBuilder.CreateIndex( + name: "ix_question_placement_rule_conditions_tenant_id_rule_group_id_~", + table: "question_placement_rule_conditions", + columns: new[] { "tenant_id", "rule_group_id", "target_dimension_definition_id", "target_node_id", "operator" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_question_placement_rule_groups_tenant_id_placement_id_group~", + table: "question_placement_rule_groups", + columns: new[] { "tenant_id", "placement_id", "group_order" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_question_placements_question_asset_owner_tenant_id_question~", + table: "question_placements", + columns: new[] { "question_asset_owner_tenant_id", "question_asset_id" }); + + migrationBuilder.CreateIndex( + name: "ix_question_placements_tenant_id_assessment_policy_version_id", + table: "question_placements", + columns: new[] { "tenant_id", "assessment_policy_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_question_placements_tenant_id_curriculum_node_id", + table: "question_placements", + columns: new[] { "tenant_id", "curriculum_node_id" }); + + migrationBuilder.CreateIndex( + name: "ix_question_placements_tenant_id_curriculum_version_id", + table: "question_placements", + columns: new[] { "tenant_id", "curriculum_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_question_placements_tenant_id_question_asset_owner_tenant_i~", + table: "question_placements", + columns: new[] { "tenant_id", "question_asset_owner_tenant_id", "question_asset_id", "curriculum_version_id", "curriculum_node_id", "assessment_policy_version_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_question_revisions_content_trgm", + table: "question_revisions", + column: "normalized_content") + .Annotation("Npgsql:IndexMethod", "gin") + .Annotation("Npgsql:IndexOperators", new[] { "gin_trgm_ops" }); + + migrationBuilder.CreateIndex( + name: "ix_question_revisions_tenant_id_delivery_fingerprint", + table: "question_revisions", + columns: new[] { "tenant_id", "delivery_fingerprint" }); + + migrationBuilder.CreateIndex( + name: "ix_question_revisions_tenant_id_question_asset_id_revision_no", + table: "question_revisions", + columns: new[] { "tenant_id", "question_asset_id", "revision_no" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_score_adjustment_rules_tenant_id_assessment_policy_version_~", + table: "score_adjustment_rules", + columns: new[] { "tenant_id", "assessment_policy_version_id", "priority" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_student_entitlements_v2_tenant_id_learning_product_id", + table: "student_entitlements_v2", + columns: new[] { "tenant_id", "learning_product_id" }); + + migrationBuilder.CreateIndex( + name: "ix_student_entitlements_v2_tenant_id_product_access_manifest_v~", + table: "student_entitlements_v2", + columns: new[] { "tenant_id", "product_access_manifest_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_student_entitlements_v2_tenant_id_source_type_source_id", + table: "student_entitlements_v2", + columns: new[] { "tenant_id", "source_type", "source_id" }, + filter: "source_id is not null"); + + migrationBuilder.CreateIndex( + name: "ix_student_entitlements_v2_tenant_id_user_id_status_starts_at_~", + table: "student_entitlements_v2", + columns: new[] { "tenant_id", "user_id", "status", "starts_at", "ends_at" }); + + migrationBuilder.CreateIndex( + name: "ix_student_entitlements_v2_user_id", + table: "student_entitlements_v2", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_student_target_selection_history", + table: "student_target_selection_history", + columns: new[] { "tenant_id", "user_id", "business_line_id", "effective_at" }); + + migrationBuilder.CreateIndex( + name: "ix_student_target_selection_history_business_line_id", + table: "student_target_selection_history", + column: "business_line_id"); + + migrationBuilder.CreateIndex( + name: "ix_student_target_selection_history_exam_target_profile_versio~", + table: "student_target_selection_history", + column: "exam_target_profile_version_id"); + + migrationBuilder.CreateIndex( + name: "ix_student_target_selection_history_tenant_id_user_id_business~", + table: "student_target_selection_history", + columns: new[] { "tenant_id", "user_id", "business_line_id", "exam_target_profile_version_id" }, + unique: true, + filter: "is_current"); + + migrationBuilder.CreateIndex( + name: "ix_student_target_selection_history_user_id", + table: "student_target_selection_history", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ux_student_target_primary", + table: "student_target_selection_history", + columns: new[] { "tenant_id", "user_id", "business_line_id" }, + unique: true, + filter: "is_current and role = 'primary'"); + + migrationBuilder.CreateIndex( + name: "ix_subjective_rubrics_tenant_id_assessment_policy_version_id_c~", + table: "subjective_rubrics", + columns: new[] { "tenant_id", "assessment_policy_version_id", "code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_target_dimension_definitions_code", + table: "target_dimension_definitions", + column: "code", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_target_nodes_parent_id", + table: "target_nodes", + column: "parent_id"); + + migrationBuilder.CreateIndex( + name: "ix_target_nodes_path", + table: "target_nodes", + column: "path") + .Annotation("Npgsql:IndexMethod", "gist"); + + migrationBuilder.CreateIndex( + name: "ix_target_nodes_target_dimension_definition_id_code", + table: "target_nodes", + columns: new[] { "target_dimension_definition_id", "code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_tenant_business_licenses_business_line_id", + table: "tenant_business_licenses", + column: "business_line_id"); + + migrationBuilder.CreateIndex( + name: "ix_tenant_business_licenses_tenant_id_business_line_id", + table: "tenant_business_licenses", + columns: new[] { "tenant_id", "business_line_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_tenant_licensed_targets_exam_target_profile_version_id", + table: "tenant_licensed_targets", + column: "exam_target_profile_version_id"); + + migrationBuilder.CreateIndex( + name: "ix_tenant_licensed_targets_tenant_id_tenant_business_license_i~", + table: "tenant_licensed_targets", + columns: new[] { "tenant_id", "tenant_business_license_id", "exam_target_profile_version_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_wrong_questions_v2_question_asset_owner_tenant_id_question_~", + table: "wrong_questions_v2", + columns: new[] { "question_asset_owner_tenant_id", "question_asset_id", "last_question_revision_id" }); + + migrationBuilder.CreateIndex( + name: "ix_wrong_questions_v2_tenant_id_user_id_last_wrong_at", + table: "wrong_questions_v2", + columns: new[] { "tenant_id", "user_id", "last_wrong_at" }); + + migrationBuilder.CreateIndex( + name: "ix_wrong_questions_v2_user_id", + table: "wrong_questions_v2", + column: "user_id"); + + migrationBuilder.AddForeignKey( + name: "fk_assessment_policies_assessment_policy_versions_tenant_id_id~", + table: "assessment_policies", + columns: new[] { "tenant_id", "id", "current_version_id" }, + principalTable: "assessment_policy_versions", + principalColumns: new[] { "tenant_id", "assessment_policy_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_audience_segment_members_audience_segments_tenant_id_audien~", + table: "audience_segment_members", + columns: new[] { "tenant_id", "audience_segment_id" }, + principalTable: "audience_segments", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_audience_segment_members_exam_target_profile_versions_exam_~", + table: "audience_segment_members", + column: "exam_target_profile_version_id", + principalTable: "exam_target_profile_versions", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_audience_segments_content_releases_tenant_id_content_releas~", + table: "audience_segments", + columns: new[] { "tenant_id", "content_release_id" }, + principalTable: "content_releases", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_blueprint_releases_collection_releases_tenant_id_collection~", + table: "blueprint_releases", + columns: new[] { "tenant_id", "collection_release_id" }, + principalTable: "collection_releases", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_blueprint_releases_content_releases_tenant_id_content_relea~", + table: "blueprint_releases", + columns: new[] { "tenant_id", "content_release_id" }, + principalTable: "content_releases", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_business_target_dimension_requirements_business_target_poli~", + table: "business_target_dimension_requirements", + column: "business_target_policy_version_id", + principalTable: "business_target_policy_versions", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_business_target_policies_business_target_policy_versions_id~", + table: "business_target_policies", + columns: new[] { "id", "current_version_id" }, + principalTable: "business_target_policy_versions", + principalColumns: new[] { "business_target_policy_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_class_assignment_grants_product_access_manifest_versions_te~", + table: "class_assignment_grants", + columns: new[] { "tenant_id", "product_access_manifest_version_id" }, + principalTable: "product_access_manifest_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_collection_release_questions_collection_releases_tenant_id_~", + table: "collection_release_questions", + columns: new[] { "tenant_id", "collection_release_id" }, + principalTable: "collection_releases", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_collection_release_questions_content_release_questions_tena~", + table: "collection_release_questions", + columns: new[] { "tenant_id", "content_release_question_id" }, + principalTable: "content_release_questions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_collection_releases_content_releases_tenant_id_content_rele~", + table: "collection_releases", + columns: new[] { "tenant_id", "content_release_id" }, + principalTable: "content_releases", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_content_release_questions_content_releases_tenant_id_conten~", + table: "content_release_questions", + columns: new[] { "tenant_id", "content_release_id" }, + principalTable: "content_releases", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_content_release_questions_question_placements_tenant_id_que~", + table: "content_release_questions", + columns: new[] { "tenant_id", "question_placement_id" }, + principalTable: "question_placements", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_content_release_questions_question_revisions_question_asset~", + table: "content_release_questions", + columns: new[] { "question_asset_owner_tenant_id", "question_asset_id", "question_revision_id" }, + principalTable: "question_revisions", + principalColumns: new[] { "tenant_id", "question_asset_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_content_releases_curriculum_versions_tenant_id_curriculum_v~", + table: "content_releases", + columns: new[] { "tenant_id", "curriculum_version_id" }, + principalTable: "curriculum_versions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_curricula_curriculum_versions_tenant_id_id_current_version_~", + table: "curricula", + columns: new[] { "tenant_id", "id", "current_version_id" }, + principalTable: "curriculum_versions", + principalColumns: new[] { "tenant_id", "curriculum_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_exam_target_profile_values_exam_target_profile_versions_exa~", + table: "exam_target_profile_values", + column: "exam_target_profile_version_id", + principalTable: "exam_target_profile_versions", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_exam_target_profile_versions_exam_target_profiles_exam_targ~", + table: "exam_target_profile_versions", + column: "exam_target_profile_id", + principalTable: "exam_target_profiles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_product_access_manifest_versions_product_access_manifests_t~", + table: "product_access_manifest_versions", + columns: new[] { "tenant_id", "product_access_manifest_id" }, + principalTable: "product_access_manifests", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_question_assets_question_revisions_tenant_id_id_current_rev~", + table: "question_assets", + columns: new[] { "tenant_id", "id", "current_revision_id" }, + principalTable: "question_revisions", + principalColumns: new[] { "tenant_id", "question_asset_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.EnsureContentV2ImmutabilityGuards(); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropContentV2ImmutabilityGuards(); + + migrationBuilder.DropForeignKey( + name: "fk_assessment_policies_assessment_policy_versions_tenant_id_id~", + table: "assessment_policies"); + + migrationBuilder.DropForeignKey( + name: "fk_exam_target_profiles_exam_target_profile_versions_id_curren~", + table: "exam_target_profiles"); + + migrationBuilder.DropForeignKey( + name: "fk_business_target_policies_business_target_policy_versions_id~", + table: "business_target_policies"); + + migrationBuilder.DropForeignKey( + name: "fk_product_access_manifests_product_access_manifest_versions_t~", + table: "product_access_manifests"); + + migrationBuilder.DropForeignKey( + name: "fk_question_assets_question_revisions_tenant_id_id_current_rev~", + table: "question_assets"); + + migrationBuilder.DropForeignKey( + name: "fk_curricula_curriculum_versions_tenant_id_id_current_version_~", + table: "curricula"); + + migrationBuilder.DropTable( + name: "assessment_score_bands"); + + migrationBuilder.DropTable( + name: "audience_segment_members"); + + migrationBuilder.DropTable( + name: "blueprint_releases"); + + migrationBuilder.DropTable( + name: "business_target_dimension_requirements"); + + migrationBuilder.DropTable( + name: "class_assignment_grants"); + + migrationBuilder.DropTable( + name: "collection_release_questions"); + + migrationBuilder.DropTable( + name: "curriculum_node_closure"); + + migrationBuilder.DropTable( + name: "effective_access_projections"); + + migrationBuilder.DropTable( + name: "exam_target_profile_values"); + + migrationBuilder.DropTable( + name: "objective_grading_rules"); + + migrationBuilder.DropTable( + name: "platform_content_package_cell_projections"); + + migrationBuilder.DropTable( + name: "product_manifest_releases"); + + migrationBuilder.DropTable( + name: "product_manifest_resources"); + + migrationBuilder.DropTable( + name: "product_manifest_targets"); + + migrationBuilder.DropTable( + name: "question_placement_concepts"); + + migrationBuilder.DropTable( + name: "question_placement_rule_conditions"); + + migrationBuilder.DropTable( + name: "score_adjustment_rules"); + + migrationBuilder.DropTable( + name: "student_entitlements_v2"); + + migrationBuilder.DropTable( + name: "student_target_selection_history"); + + migrationBuilder.DropTable( + name: "subjective_rubrics"); + + migrationBuilder.DropTable( + name: "tenant_licensed_targets"); + + migrationBuilder.DropTable( + name: "wrong_questions_v2"); + + migrationBuilder.DropTable( + name: "exam_paper_specification_versions"); + + migrationBuilder.DropTable( + name: "collection_releases"); + + migrationBuilder.DropTable( + name: "content_release_questions"); + + migrationBuilder.DropTable( + name: "platform_content_package_versions"); + + migrationBuilder.DropTable( + name: "question_placement_rule_groups"); + + migrationBuilder.DropTable( + name: "target_nodes"); + + migrationBuilder.DropTable( + name: "tenant_business_licenses"); + + migrationBuilder.DropTable( + name: "exam_paper_specifications"); + + migrationBuilder.DropTable( + name: "audience_segments"); + + migrationBuilder.DropTable( + name: "question_placements"); + + migrationBuilder.DropTable( + name: "target_dimension_definitions"); + + migrationBuilder.DropTable( + name: "content_releases"); + + migrationBuilder.DropTable( + name: "curriculum_nodes"); + + migrationBuilder.DropTable( + name: "knowledge_concepts"); + + migrationBuilder.DropTable( + name: "assessment_policy_versions"); + + migrationBuilder.DropTable( + name: "assessment_policies"); + + migrationBuilder.DropTable( + name: "exam_target_profile_versions"); + + migrationBuilder.DropTable( + name: "exam_target_profiles"); + + migrationBuilder.DropTable( + name: "business_target_policy_versions"); + + migrationBuilder.DropTable( + name: "business_target_policies"); + + migrationBuilder.DropTable( + name: "product_access_manifest_versions"); + + migrationBuilder.DropTable( + name: "product_access_manifests"); + + migrationBuilder.DropTable( + name: "question_revisions"); + + migrationBuilder.DropTable( + name: "question_assets"); + + migrationBuilder.DropTable( + name: "question_families"); + + migrationBuilder.DropTable( + name: "curriculum_versions"); + + migrationBuilder.DropTable( + name: "curricula"); + + migrationBuilder.DropIndex( + name: "ix_practice_session_questions_tenant_id_practice_session_id_co~", + table: "practice_session_questions"); + + migrationBuilder.DropIndex( + name: "ux_answer_records_idempotency", + table: "answer_records"); + + migrationBuilder.DropColumn( + name: "assessment_policy_version_id", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "content_release_question_id", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "question_asset_id", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "question_placement_id", + table: "practice_session_questions"); + + migrationBuilder.DropColumn( + name: "question_revision_id", + table: "practice_session_questions"); + + migrationBuilder.AlterColumn( + name: "question_version_id", + table: "practice_session_questions", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "question_reference_id", + table: "practice_session_questions", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "question_id", + table: "practice_session_questions", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "answer_submission_idempotencies", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + answer_record_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + idempotency_key = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + practice_session_id = table.Column(type: "uuid", nullable: false), + request_hash = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + response_snapshot = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + tenant_id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_answer_submission_idempotencies", x => x.id); + table.UniqueConstraint("ak_answer_submission_idempotencies_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_answer_submission_idempotencies_practice_sessions_tenant_id~", + 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_answer_submission_idempotencies_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_answer_submission_idempotencies_tenant_id_answer_record_id", + table: "answer_submission_idempotencies", + columns: new[] { "tenant_id", "answer_record_id" }); + + migrationBuilder.CreateIndex( + name: "ix_answer_submission_idempotencies_user_id", + table: "answer_submission_idempotencies", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ux_answer_submission_idempotency", + table: "answer_submission_idempotencies", + columns: new[] { "tenant_id", "user_id", "practice_session_id", "idempotency_key" }, + unique: true); + } + } +} diff --git a/Tiku.Infrastructure/Persistence/Migrations/20260805095745_AuditTargetSelectionSource.Designer.cs b/Tiku.Infrastructure/Persistence/Migrations/20260805095745_AuditTargetSelectionSource.Designer.cs new file mode 100644 index 0000000..50bba7e --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260805095745_AuditTargetSelectionSource.Designer.cs @@ -0,0 +1,26758 @@ +// +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("20260805095745_AuditTargetSelectionSource")] + partial class AuditTargetSelectionSource + { + /// + 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("LearningProductId") + .HasColumnType("uuid") + .HasColumnName("learning_product_id"); + + 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", "LearningProductId") + .HasDatabaseName("ix_entitlements_tenant_id_learning_product_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.AssessmentPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_assessment_policies"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_assessment_policies_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_assessment_policies_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_assessment_policies_tenant_id_code"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_assessment_policies_tenant_id_id_current_version_id"); + + b.ToTable("assessment_policies", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentPolicyVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_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("DefaultScore") + .HasColumnType("numeric") + .HasColumnName("default_score"); + + b.Property("GradingMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("grading_mode"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("RoundingScale") + .HasColumnType("integer") + .HasColumnName("rounding_scale"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_assessment_policy_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_assessment_policy_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "AssessmentPolicyId", "Id") + .HasName("ak_assessment_policy_versions_tenant_id_assessment_policy_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_assessment_policy_versions_tenant_id_assessment_policy_id_v~"); + + b.ToTable("assessment_policy_versions", null, t => + { + t.HasCheckConstraint("ck_assessment_policy_default_score", "default_score > 0"); + + t.HasCheckConstraint("ck_assessment_policy_rounding", "rounding_scale between 0 and 6"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentScoreBand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("label"); + + b.Property("MaxScore") + .HasColumnType("numeric") + .HasColumnName("max_score"); + + b.Property("MinScore") + .HasColumnType("numeric") + .HasColumnName("min_score"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_assessment_score_bands"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_assessment_score_bands_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "MinScore", "MaxScore") + .IsUnique() + .HasDatabaseName("ix_assessment_score_bands_tenant_id_assessment_policy_version_~"); + + b.ToTable("assessment_score_bands", null, t => + { + t.HasCheckConstraint("ck_assessment_score_band", "max_score >= min_score"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("RuleHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("rule_hash"); + + b.Property("SegmentOrdinal") + .HasColumnType("integer") + .HasColumnName("segment_ordinal"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_audience_segments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_audience_segments_tenant_id_id"); + + b.HasIndex("TenantId", "ContentReleaseId", "RuleHash") + .IsUnique() + .HasDatabaseName("ix_audience_segments_tenant_id_content_release_id_rule_hash"); + + b.HasIndex("TenantId", "ContentReleaseId", "SegmentOrdinal") + .IsUnique() + .HasDatabaseName("ix_audience_segments_tenant_id_content_release_id_segment_ordi~"); + + b.ToTable("audience_segments", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegmentMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AudienceSegmentId") + .HasColumnType("uuid") + .HasColumnName("audience_segment_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_audience_segment_members"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_audience_segment_members_tenant_id_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_audience_segment_members_exam_target_profile_version_id"); + + b.HasIndex("TenantId", "AudienceSegmentId") + .HasDatabaseName("ix_audience_segment_members_tenant_id_audience_segment_id"); + + b.HasIndex("TenantId", "ContentReleaseId", "AudienceSegmentId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_audience_segment_members_tenant_id_content_release_id_audie~"); + + b.HasIndex("TenantId", "ContentReleaseId", "ExamTargetProfileVersionId", "AudienceSegmentId") + .HasDatabaseName("ix_audience_segment_members_tenant_id_content_release_id_exam_~"); + + b.ToTable("audience_segment_members", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BlueprintRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssemblyRules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("assembly_rules") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CollectionReleaseId") + .HasColumnType("uuid") + .HasColumnName("collection_release_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ExamPaperSpecificationVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_paper_specification_version_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("SourceBlueprintId") + .HasColumnType("uuid") + .HasColumnName("source_blueprint_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.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_blueprint_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_blueprint_releases_tenant_id_id"); + + b.HasIndex("TenantId", "CollectionReleaseId") + .HasDatabaseName("ix_blueprint_releases_tenant_id_collection_release_id"); + + b.HasIndex("TenantId", "ContentReleaseId") + .HasDatabaseName("ix_blueprint_releases_tenant_id_content_release_id"); + + b.HasIndex("TenantId", "ExamPaperSpecificationVersionId") + .HasDatabaseName("ix_blueprint_releases_tenant_id_exam_paper_specification_versi~"); + + b.HasIndex("TenantId", "SourceBlueprintId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_blueprint_releases_tenant_id_source_blueprint_id_version_no"); + + b.ToTable("blueprint_releases", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetDimensionRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessTargetPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("business_target_policy_version_id"); + + b.Property("MaxValues") + .HasColumnType("integer") + .HasColumnName("max_values"); + + b.Property("MinValues") + .HasColumnType("integer") + .HasColumnName("min_values"); + + b.Property("RequiredForProfile") + .HasColumnType("boolean") + .HasColumnName("required_for_profile"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.HasKey("Id") + .HasName("pk_business_target_dimension_requirements"); + + b.HasIndex("TargetDimensionDefinitionId") + .HasDatabaseName("ix_business_target_dimension_requirements_target_dimension_def~"); + + b.HasIndex("BusinessTargetPolicyVersionId", "TargetDimensionDefinitionId") + .IsUnique() + .HasDatabaseName("ix_business_target_dimension_requirements_business_target_poli~"); + + b.ToTable("business_target_dimension_requirements", null, t => + { + t.HasCheckConstraint("ck_business_target_dimension_cardinality", "min_values >= 0 and max_values >= min_values"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_business_target_policies"); + + b.HasIndex("BusinessLineId") + .IsUnique() + .HasDatabaseName("ix_business_target_policies_business_line_id"); + + b.HasIndex("Id", "CurrentVersionId") + .HasDatabaseName("ix_business_target_policies_id_current_version_id"); + + b.ToTable("business_target_policies", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicyVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessTargetPolicyId") + .HasColumnType("uuid") + .HasColumnName("business_target_policy_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IncludesBaseContent") + .HasColumnType("boolean") + .HasColumnName("includes_base_content"); + + b.Property("MaxActiveTargets") + .HasColumnType("integer") + .HasColumnName("max_active_targets"); + + b.Property("MaxAlternateTargets") + .HasColumnType("integer") + .HasColumnName("max_alternate_targets"); + + b.Property("MaxPrimaryTargets") + .HasColumnType("integer") + .HasColumnName("max_primary_targets"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("SelectionMode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("selection_mode"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TargetChangeCooldownDays") + .HasColumnType("integer") + .HasColumnName("target_change_cooldown_days"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_business_target_policy_versions"); + + b.HasAlternateKey("BusinessTargetPolicyId", "Id") + .HasName("ak_business_target_policy_versions_business_target_policy_id_id"); + + b.HasIndex("BusinessTargetPolicyId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_business_target_policy_versions_business_target_policy_id_v~"); + + b.ToTable("business_target_policy_versions", null, t => + { + t.HasCheckConstraint("ck_business_target_policy_active", "max_active_targets between 1 and 16"); + + t.HasCheckConstraint("ck_business_target_policy_alternate", "max_alternate_targets between 0 and max_active_targets"); + + t.HasCheckConstraint("ck_business_target_policy_cooldown", "target_change_cooldown_days between 0 and 365"); + + t.HasCheckConstraint("ck_business_target_policy_primary", "max_primary_targets between 0 and max_active_targets"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("SourceCollectionId") + .HasColumnType("uuid") + .HasColumnName("source_collection_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.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_collection_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_collection_releases_tenant_id_id"); + + b.HasIndex("TenantId", "ContentReleaseId") + .HasDatabaseName("ix_collection_releases_tenant_id_content_release_id"); + + b.HasIndex("TenantId", "SourceCollectionId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_collection_releases_tenant_id_source_collection_id_version_~"); + + b.ToTable("collection_releases", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionReleaseQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CollectionReleaseId") + .HasColumnType("uuid") + .HasColumnName("collection_release_id"); + + b.Property("ContentReleaseQuestionId") + .HasColumnType("uuid") + .HasColumnName("content_release_question_id"); + + b.Property("Required") + .HasColumnType("boolean") + .HasColumnName("required"); + + b.Property("Score") + .HasColumnType("numeric") + .HasColumnName("score"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_collection_release_questions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_collection_release_questions_tenant_id_id"); + + b.HasIndex("TenantId", "ContentReleaseQuestionId") + .HasDatabaseName("ix_collection_release_questions_tenant_id_content_release_ques~"); + + b.HasIndex("TenantId", "CollectionReleaseId", "ContentReleaseQuestionId") + .IsUnique() + .HasDatabaseName("ux_collection_release_question_item"); + + b.HasIndex("TenantId", "CollectionReleaseId", "SortOrder") + .IsUnique() + .HasDatabaseName("ux_collection_release_question_order"); + + b.ToTable("collection_release_questions", (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.ContentRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("CompilerVersion") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("compiler_version"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("ReleaseNo") + .HasColumnType("integer") + .HasColumnName("release_no"); + + b.Property("SourceFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("source_fingerprint"); + + 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_content_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_releases_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_content_releases_business_line_id"); + + b.HasIndex("TenantId", "CurriculumVersionId", "ReleaseNo") + .IsUnique() + .HasDatabaseName("ix_content_releases_tenant_id_curriculum_version_id_release_no"); + + b.HasIndex("TenantId", "BusinessLineId", "Status", "PublishedAt") + .HasDatabaseName("ix_content_releases_tenant_id_business_line_id_status_publishe~"); + + b.ToTable("content_releases", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentReleaseQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("AudienceSegmentId") + .HasColumnType("uuid") + .HasColumnName("audience_segment_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CurriculumNodeId") + .HasColumnType("uuid") + .HasColumnName("curriculum_node_id"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("Ordinal") + .HasColumnType("integer") + .HasColumnName("ordinal"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionAssetOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_asset_owner_tenant_id"); + + b.Property("QuestionPlacementId") + .HasColumnType("uuid") + .HasColumnName("question_placement_id"); + + b.Property("QuestionRevisionId") + .HasColumnType("uuid") + .HasColumnName("question_revision_id"); + + b.Property("QuestionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("question_type"); + + 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_content_release_questions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_release_questions_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId") + .HasDatabaseName("ix_content_release_questions_tenant_id_assessment_policy_versi~"); + + b.HasIndex("TenantId", "AudienceSegmentId") + .HasDatabaseName("ix_content_release_questions_tenant_id_audience_segment_id"); + + b.HasIndex("TenantId", "QuestionPlacementId") + .HasDatabaseName("ix_content_release_questions_tenant_id_question_placement_id"); + + b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId", "QuestionRevisionId") + .HasDatabaseName("ix_content_release_questions_question_asset_owner_tenant_id_qu~"); + + b.HasIndex("TenantId", "ContentReleaseId", "AudienceSegmentId", "CurriculumNodeId", "Ordinal") + .IsUnique() + .HasDatabaseName("ix_content_release_questions_tenant_id_content_release_id_audi~"); + + b.HasIndex("TenantId", "ContentReleaseId", "AudienceSegmentId", "CurriculumNodeId", "Status", "Difficulty", "QuestionType", "Ordinal") + .HasDatabaseName("ix_content_release_question_candidates"); + + b.ToTable("content_release_questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.Curriculum", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_curricula"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curricula_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_curricula_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_curricula_tenant_id_code"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_curricula_tenant_id_id_current_version_id"); + + b.ToTable("curricula", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNode", 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("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("kind"); + + b.Property("KnowledgeConceptId") + .HasColumnType("uuid") + .HasColumnName("knowledge_concept_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_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_curriculum_nodes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curriculum_nodes_tenant_id_id"); + + b.HasIndex("TenantId", "KnowledgeConceptId") + .HasDatabaseName("ix_curriculum_nodes_tenant_id_knowledge_concept_id"); + + b.HasIndex("TenantId", "ParentId") + .HasDatabaseName("ix_curriculum_nodes_tenant_id_parent_id"); + + b.HasIndex("TenantId", "CurriculumVersionId", "Code") + .IsUnique() + .HasDatabaseName("ix_curriculum_nodes_tenant_id_curriculum_version_id_code"); + + b.HasIndex("TenantId", "CurriculumVersionId", "ParentId", "SortOrder") + .HasDatabaseName("ix_curriculum_nodes_tenant_id_curriculum_version_id_parent_id_~"); + + b.ToTable("curriculum_nodes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNodeClosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AncestorNodeId") + .HasColumnType("uuid") + .HasColumnName("ancestor_node_id"); + + b.Property("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("Depth") + .HasColumnType("integer") + .HasColumnName("depth"); + + b.Property("DescendantNodeId") + .HasColumnType("uuid") + .HasColumnName("descendant_node_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_curriculum_node_closure"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curriculum_node_closure_tenant_id_id"); + + b.HasIndex("TenantId", "CurriculumVersionId", "AncestorNodeId", "DescendantNodeId") + .IsUnique() + .HasDatabaseName("ix_curriculum_node_closure_tenant_id_curriculum_version_id_anc~"); + + b.HasIndex("TenantId", "CurriculumVersionId", "DescendantNodeId", "Depth") + .HasDatabaseName("ix_curriculum_node_closure_tenant_id_curriculum_version_id_des~"); + + b.ToTable("curriculum_node_closure", null, t => + { + t.HasCheckConstraint("ck_curriculum_node_closure_depth", "depth >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumVersion", 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("CurriculumId") + .HasColumnType("uuid") + .HasColumnName("curriculum_id"); + + b.Property("ExamYear") + .HasColumnType("integer") + .HasColumnName("exam_year"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_curriculum_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curriculum_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "CurriculumId", "Id") + .HasName("ak_curriculum_versions_tenant_id_curriculum_id_id"); + + b.HasIndex("TenantId", "CurriculumId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_curriculum_versions_tenant_id_curriculum_id_version_no"); + + b.ToTable("curriculum_versions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_paper_specifications"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_exam_paper_specifications_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_exam_paper_specifications_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_exam_paper_specifications_tenant_id_code"); + + b.ToTable("exam_paper_specifications", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecificationVersion", 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("DurationMinutes") + .HasColumnType("integer") + .HasColumnName("duration_minutes"); + + b.Property("ExamYear") + .HasColumnType("integer") + .HasColumnName("exam_year"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Sections") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sections") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("SpecificationId") + .HasColumnType("uuid") + .HasColumnName("specification_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalScore") + .HasColumnType("numeric") + .HasColumnName("total_score"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_exam_paper_specification_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_exam_paper_specification_versions_tenant_id_id"); + + b.HasIndex("TenantId", "SpecificationId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_exam_paper_specification_versions_tenant_id_specification_i~"); + + b.ToTable("exam_paper_specification_versions", null, t => + { + t.HasCheckConstraint("ck_exam_paper_spec_duration", "duration_minutes > 0"); + + t.HasCheckConstraint("ck_exam_paper_spec_total_score", "total_score > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("code"); + + 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_exam_target_profiles"); + + b.HasIndex("BusinessLineId", "Code") + .IsUnique() + .HasDatabaseName("ix_exam_target_profiles_business_line_id_code"); + + b.HasIndex("Id", "CurrentVersionId") + .HasDatabaseName("ix_exam_target_profiles_id_current_version_id"); + + b.ToTable("exam_target_profiles", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.Property("TargetNodeId") + .HasColumnType("uuid") + .HasColumnName("target_node_id"); + + b.HasKey("Id") + .HasName("pk_exam_target_profile_values"); + + b.HasIndex("TargetDimensionDefinitionId") + .HasDatabaseName("ix_exam_target_profile_values_target_dimension_definition_id"); + + b.HasIndex("TargetNodeId") + .HasDatabaseName("ix_exam_target_profile_values_target_node_id"); + + b.HasIndex("ExamTargetProfileVersionId", "TargetDimensionDefinitionId", "TargetNodeId") + .IsUnique() + .HasDatabaseName("ix_exam_target_profile_values_exam_target_profile_version_id_t~"); + + b.ToTable("exam_target_profile_values", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileVersion", 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(500) + .HasColumnType("character varying(500)") + .HasColumnName("display_name"); + + b.Property("ExamTargetProfileId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_id"); + + b.Property("ExamYear") + .HasColumnType("integer") + .HasColumnName("exam_year"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_exam_target_profile_versions"); + + b.HasAlternateKey("ExamTargetProfileId", "Id") + .HasName("ak_exam_target_profile_versions_exam_target_profile_id_id"); + + b.HasIndex("ExamTargetProfileId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_exam_target_profile_versions_exam_target_profile_id_version~"); + + b.ToTable("exam_target_profile_versions", (string)null); + }); + + 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.KnowledgeConcept", 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(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_knowledge_concepts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_knowledge_concepts_tenant_id_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_knowledge_concepts_tenant_id_code"); + + b.ToTable("knowledge_concepts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ObjectiveGradingRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Configuration") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("configuration") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RuleType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("rule_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_objective_grading_rules"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_objective_grading_rules_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "RuleType") + .IsUnique() + .HasDatabaseName("ix_objective_grading_rules_tenant_id_assessment_policy_version~"); + + b.ToTable("objective_grading_rules", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageCellProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CellId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("cell_id"); + + b.Property("ContentHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("content_hash"); + + b.Property("EventSequence") + .HasColumnType("bigint") + .HasColumnName("event_sequence"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("PackageCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("package_code"); + + b.Property("PackageOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("package_owner_tenant_id"); + + b.Property("PlatformContentPackageVersionId") + .HasColumnType("uuid") + .HasColumnName("platform_content_package_version_id"); + + b.Property("ProjectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("projected_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_platform_content_package_cell_projections"); + + b.HasIndex("PackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasDatabaseName("ix_platform_package_cell_owner_version"); + + b.HasIndex("PlatformContentPackageVersionId", "CellId") + .HasDatabaseName("ix_platform_package_cell_version"); + + b.HasIndex("PackageOwnerTenantId", "PackageCode", "CellId") + .IsUnique() + .HasDatabaseName("ux_platform_package_cell_cursor"); + + b.ToTable("platform_content_package_cell_projections", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ContentHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("content_hash"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("PackageCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("package_code"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_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.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_platform_content_package_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_content_package_versions_tenant_id_id"); + + b.HasIndex("TenantId", "ContentHash") + .IsUnique() + .HasDatabaseName("ix_platform_content_package_versions_tenant_id_content_hash"); + + b.HasIndex("TenantId", "ContentReleaseId") + .HasDatabaseName("ix_platform_content_package_versions_tenant_id_content_release~"); + + b.HasIndex("TenantId", "PackageCode", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_platform_content_package_versions_tenant_id_package_code_ve~"); + + b.ToTable("platform_content_package_versions", (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.QuestionAsset", 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("CurrentRevisionId") + .HasColumnType("uuid") + .HasColumnName("current_revision_id"); + + b.Property("DeliveryFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("delivery_fingerprint"); + + b.Property("FamilyId") + .HasColumnType("uuid") + .HasColumnName("family_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.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.HasKey("Id") + .HasName("pk_question_assets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_assets_tenant_id_id"); + + b.HasIndex("TenantId", "DeliveryFingerprint") + .IsUnique() + .HasDatabaseName("ix_question_assets_tenant_id_delivery_fingerprint"); + + b.HasIndex("TenantId", "FamilyId") + .HasDatabaseName("ix_question_assets_tenant_id_family_id"); + + b.HasIndex("TenantId", "Id", "CurrentRevisionId") + .HasDatabaseName("ix_question_assets_tenant_id_id_current_revision_id"); + + b.ToTable("question_assets", (string)null); + }); + + 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.QuestionFamily", 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("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_families"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_families_tenant_id_id"); + + b.ToTable("question_families", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_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("CurriculumNodeId") + .HasColumnType("uuid") + .HasColumnName("curriculum_node_id"); + + b.Property("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("DifficultyOverride") + .HasColumnType("integer") + .HasColumnName("difficulty_override"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionAssetOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_asset_owner_tenant_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.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + 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_question_placements"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placements_tenant_id_id"); + + b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasDatabaseName("ix_question_placements_question_asset_owner_tenant_id_question~"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId") + .HasDatabaseName("ix_question_placements_tenant_id_assessment_policy_version_id"); + + b.HasIndex("TenantId", "CurriculumNodeId") + .HasDatabaseName("ix_question_placements_tenant_id_curriculum_node_id"); + + b.HasIndex("TenantId", "CurriculumVersionId") + .HasDatabaseName("ix_question_placements_tenant_id_curriculum_version_id"); + + b.HasIndex("TenantId", "QuestionAssetOwnerTenantId", "QuestionAssetId", "CurriculumVersionId", "CurriculumNodeId", "AssessmentPolicyVersionId") + .IsUnique() + .HasDatabaseName("ix_question_placements_tenant_id_question_asset_owner_tenant_i~"); + + b.ToTable("question_placements", null, t => + { + t.HasCheckConstraint("ck_question_placements_valid_period", "valid_to is null or valid_from is null or valid_to > valid_from"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementConcept", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("KnowledgeConceptId") + .HasColumnType("uuid") + .HasColumnName("knowledge_concept_id"); + + b.Property("PlacementId") + .HasColumnType("uuid") + .HasColumnName("placement_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_placement_concepts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placement_concepts_tenant_id_id"); + + b.HasIndex("TenantId", "KnowledgeConceptId") + .HasDatabaseName("ix_question_placement_concepts_tenant_id_knowledge_concept_id"); + + b.HasIndex("TenantId", "PlacementId", "KnowledgeConceptId") + .IsUnique() + .HasDatabaseName("ix_question_placement_concepts_tenant_id_placement_id_knowledg~"); + + b.ToTable("question_placement_concepts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleCondition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("operator"); + + b.Property("RuleGroupId") + .HasColumnType("uuid") + .HasColumnName("rule_group_id"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.Property("TargetNodeId") + .HasColumnType("uuid") + .HasColumnName("target_node_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_placement_rule_conditions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placement_rule_conditions_tenant_id_id"); + + b.HasIndex("TargetDimensionDefinitionId") + .HasDatabaseName("ix_question_placement_rule_conditions_target_dimension_definit~"); + + b.HasIndex("TargetNodeId") + .HasDatabaseName("ix_question_placement_rule_conditions_target_node_id"); + + b.HasIndex("TenantId", "RuleGroupId", "TargetDimensionDefinitionId", "TargetNodeId", "Operator") + .IsUnique() + .HasDatabaseName("ix_question_placement_rule_conditions_tenant_id_rule_group_id_~"); + + b.ToTable("question_placement_rule_conditions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("GroupOrder") + .HasColumnType("integer") + .HasColumnName("group_order"); + + b.Property("PlacementId") + .HasColumnType("uuid") + .HasColumnName("placement_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_placement_rule_groups"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placement_rule_groups_tenant_id_id"); + + b.HasIndex("TenantId", "PlacementId", "GroupOrder") + .IsUnique() + .HasDatabaseName("ix_question_placement_rule_groups_tenant_id_placement_id_group~"); + + b.ToTable("question_placement_rule_groups", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionRevision", 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("DeliveryFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("delivery_fingerprint"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("Explanation") + .HasColumnType("text") + .HasColumnName("explanation"); + + b.Property("NormalizedContent") + .IsRequired() + .HasColumnType("text") + .HasColumnName("normalized_content"); + + b.Property("Options") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("options") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("question_type"); + + b.Property("RevisionNo") + .HasColumnType("integer") + .HasColumnName("revision_no"); + + b.Property("SubQuestions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sub_questions") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TypeLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label"); + + b.HasKey("Id") + .HasName("pk_question_revisions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_revisions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "QuestionAssetId", "Id") + .HasName("ak_question_revisions_tenant_id_question_asset_id_id"); + + b.HasIndex("NormalizedContent") + .HasDatabaseName("ix_question_revisions_content_trgm"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("NormalizedContent"), "gin"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("NormalizedContent"), new[] { "gin_trgm_ops" }); + + b.HasIndex("TenantId", "DeliveryFingerprint") + .HasDatabaseName("ix_question_revisions_tenant_id_delivery_fingerprint"); + + b.HasIndex("TenantId", "QuestionAssetId", "RevisionNo") + .IsUnique() + .HasDatabaseName("ix_question_revisions_tenant_id_question_asset_id_revision_no"); + + b.ToTable("question_revisions", (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.ScoreAdjustmentRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Configuration") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("configuration") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RuleType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("rule_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_score_adjustment_rules"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_score_adjustment_rules_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "Priority") + .IsUnique() + .HasDatabaseName("ix_score_adjustment_rules_tenant_id_assessment_policy_version_~"); + + b.ToTable("score_adjustment_rules", (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.SubjectiveRubric", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("Criteria") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("criteria") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("MaxScore") + .HasColumnType("numeric") + .HasColumnName("max_score"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_subjective_rubrics"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_subjective_rubrics_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "Code") + .IsUnique() + .HasDatabaseName("ix_subjective_rubrics_tenant_id_assessment_policy_version_id_c~"); + + b.ToTable("subjective_rubrics", null, t => + { + t.HasCheckConstraint("ck_subjective_rubric_max_score", "max_score > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TargetDimensionDefinition", 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsHierarchical") + .HasColumnType("boolean") + .HasColumnName("is_hierarchical"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_target_dimension_definitions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_target_dimension_definitions_code"); + + b.ToTable("target_dimension_definitions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TargetNode", 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .IsRequired() + .HasColumnType("ltree") + .HasColumnName("path"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_target_nodes"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_target_nodes_parent_id"); + + b.HasIndex("Path") + .HasDatabaseName("ix_target_nodes_path"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Path"), "gist"); + + b.HasIndex("TargetDimensionDefinitionId", "Code") + .IsUnique() + .HasDatabaseName("ix_target_nodes_target_dimension_definition_id_code"); + + b.ToTable("target_nodes", (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("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", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ux_answer_records_idempotency"); + + 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.BusinessLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RegionAccessStrategy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("region_access_strategy"); + + b.Property("RequiresBaseRegion") + .HasColumnType("boolean") + .HasColumnName("requires_base_region"); + + b.Property("TargetRegionCooldownDays") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30) + .HasColumnName("target_region_cooldown_days"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_business_lines"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_business_lines_code"); + + b.ToTable("business_lines", null, t => + { + t.HasCheckConstraint("ck_business_lines_target_region_cooldown_days", "target_region_cooldown_days between 0 and 365"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ClassAssignmentGrant", 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("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_type"); + + b.Property("RevokedBy") + .HasColumnType("uuid") + .HasColumnName("revoked_by"); + + b.Property("RevokedReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("revoked_reason"); + + 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_class_assignment_grants"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_class_assignment_grants_tenant_id_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId") + .HasDatabaseName("ix_class_assignment_grants_tenant_id_product_access_manifest_v~"); + + b.HasIndex("TenantId", "ClassId", "Status", "StartsAt") + .HasDatabaseName("ix_class_assignment_grants_tenant_id_class_id_status_starts_at"); + + b.HasIndex("TenantId", "ResourceType", "ResourceId", "Status") + .HasDatabaseName("ix_class_assignment_grants_tenant_id_resource_type_resource_id~"); + + b.ToTable("class_assignment_grants", null, t => + { + t.HasCheckConstraint("ck_class_assignment_grant_period", "ends_at is null or ends_at > starts_at"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ClassContentAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ClassId") + .HasColumnType("uuid") + .HasColumnName("class_id"); + + b.Property("ContentSliceId") + .HasColumnType("uuid") + .HasColumnName("content_slice_id"); + + b.Property("ContentSliceOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("content_slice_owner_tenant_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("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_type"); + + b.Property("RevokedBy") + .HasColumnType("uuid") + .HasColumnName("revoked_by"); + + b.Property("RevokedReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("revoked_reason"); + + 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_class_content_assignments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_class_content_assignments_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_class_content_assignments_created_by"); + + b.HasIndex("RevokedBy") + .HasDatabaseName("ix_class_content_assignments_revoked_by"); + + b.HasIndex("ContentSliceOwnerTenantId", "ContentSliceId") + .HasDatabaseName("ix_class_content_assignments_content_slice_owner_tenant_id_con~"); + + b.HasIndex("TenantId", "ClassId", "Status", "StartsAt") + .HasDatabaseName("ix_class_content_assignments_tenant_id_class_id_status_starts_~"); + + b.HasIndex("TenantId", "ResourceType", "ResourceId", "Status") + .HasDatabaseName("ix_class_content_assignments_tenant_id_resource_type_resource_~"); + + b.ToTable("class_content_assignments", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ContentSlice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ContentVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("content_version"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("MarketRegionId") + .HasColumnType("uuid") + .HasColumnName("market_region_id"); + + b.Property("RegionScope") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("region_scope"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_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_content_slices"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_slices_tenant_id_id"); + + b.HasIndex("MarketRegionId") + .HasDatabaseName("ix_content_slices_market_region_id"); + + b.HasIndex("BusinessLineId", "RegionScope", "MarketRegionId") + .HasDatabaseName("ix_content_slices_business_line_id_region_scope_market_region_~"); + + b.HasIndex("TenantId", "ResourceType", "ResourceId", "Status") + .HasDatabaseName("ix_content_slices_tenant_id_resource_type_resource_id_status"); + + b.ToTable("content_slices", null, t => + { + t.HasCheckConstraint("ck_content_slices_region_scope", "(region_scope = 'national' and market_region_id is null) or (region_scope = 'region' and market_region_id is not null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.CurrentAnswer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerRecordId") + .HasColumnType("uuid") + .HasColumnName("answer_record_id"); + + b.Property("ClientSequence") + .HasColumnType("bigint") + .HasColumnName("client_sequence"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("Revision") + .HasColumnType("integer") + .HasColumnName("revision"); + + b.Property("SessionQuestionId") + .HasColumnType("uuid") + .HasColumnName("session_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("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_current_answers"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_current_answers_tenant_id_id"); + + b.HasIndex("TenantId", "AnswerRecordId") + .HasDatabaseName("ix_current_answers_tenant_id_answer_record_id"); + + b.HasIndex("TenantId", "PracticeSessionId", "SessionQuestionId") + .HasDatabaseName("ix_current_answers_tenant_id_practice_session_id_session_quest~"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "SessionQuestionId") + .IsUnique() + .HasDatabaseName("ux_current_answers_session_question"); + + b.ToTable("current_answers", (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.EffectiveAccessProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.PrimitiveCollection("AlternateProfileVersionIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("alternate_profile_version_ids"); + + b.PrimitiveCollection("AudienceSegmentIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("audience_segment_ids"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("CompiledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("compiled_at"); + + b.PrimitiveCollection("ContentReleaseIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("content_release_ids"); + + b.Property("ContentVersion") + .HasColumnType("bigint") + .HasColumnName("content_version"); + + b.Property("GrantVersion") + .HasColumnType("bigint") + .HasColumnName("grant_version"); + + b.PrimitiveCollection("ManifestVersionIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("manifest_version_ids"); + + b.Property("PrimaryProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("primary_profile_version_id"); + + b.Property("StrongRevocationVersion") + .HasColumnType("bigint") + .HasColumnName("strong_revocation_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("valid_until"); + + b.HasKey("Id") + .HasName("pk_effective_access_projections"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_effective_access_projections_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_effective_access_projections_business_line_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_effective_access_projections_user_id"); + + b.HasIndex("TenantId", "ValidUntil") + .HasDatabaseName("ix_effective_access_projections_tenant_id_valid_until"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ix_effective_access_projections_tenant_id_user_id_business_lin~"); + + b.ToTable("effective_access_projections", (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.LearningAccessVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentVersion") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("content_version"); + + b.Property("GrantVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("grant_version"); + + b.Property("StrongRevocationVersion") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("strong_revocation_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_learning_access_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_access_versions_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_learning_access_versions_user_id"); + + b.HasIndex("TenantId", "UserId") + .IsUnique() + .HasDatabaseName("ix_learning_access_versions_tenant_id_user_id"); + + b.ToTable("learning_access_versions", (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.LearningOutboxMessage", b => + { + b.Property("OccurredAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_at") + .HasDefaultValueSql("now()"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AggregateId") + .HasColumnType("uuid") + .HasColumnName("aggregate_id"); + + b.Property("AttemptCount") + .HasColumnType("integer") + .HasColumnName("attempt_count"); + + b.Property("AvailableAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("available_at") + .HasDefaultValueSql("now()"); + + b.Property("DeadLetteredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("dead_lettered_at"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("event_type"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .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("Payload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at"); + + b.Property("SchemaVersion") + .HasColumnType("integer") + .HasColumnName("schema_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("OccurredAt", "Id") + .HasName("pk_learning_outbox_messages"); + + b.HasAlternateKey("TenantId", "OccurredAt", "Id") + .HasName("ak_learning_outbox_messages_tenant_id_occurred_at_id"); + + b.HasIndex("AvailableAt", "LockExpiresAt", "OccurredAt", "Id") + .HasDatabaseName("ix_learning_outbox_pending") + .HasFilter("processed_at IS NULL AND dead_lettered_at IS NULL"); + + b.HasIndex("TenantId", "EventType", "AggregateId", "OccurredAt") + .IsUnique() + .HasDatabaseName("ix_learning_outbox_messages_tenant_id_event_type_aggregate_id_~"); + + b.ToTable("learning_outbox_messages", null, t => + { + t.HasCheckConstraint("ck_learning_outbox_messages_attempt_count", "attempt_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProduct", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + 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_learning_products"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_products_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_learning_products_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_learning_products_tenant_id_code"); + + b.ToTable("learning_products", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProductScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentSliceId") + .HasColumnType("uuid") + .HasColumnName("content_slice_id"); + + b.Property("ContentSliceOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("content_slice_owner_tenant_id"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasColumnName("product_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_learning_product_scopes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_product_scopes_tenant_id_id"); + + b.HasIndex("ContentSliceOwnerTenantId", "ContentSliceId") + .HasDatabaseName("ix_learning_product_scopes_content_slice_owner_tenant_id_conte~"); + + b.HasIndex("TenantId", "ProductId", "ContentSliceId") + .IsUnique() + .HasDatabaseName("ix_learning_product_scopes_tenant_id_product_id_content_slice_~"); + + b.ToTable("learning_product_scopes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProjectionReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("OutboxMessageId") + .HasColumnType("uuid") + .HasColumnName("outbox_message_id"); + + b.Property("ProcessedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at") + .HasDefaultValueSql("now()"); + + b.Property("ProjectionName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("projection_name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_learning_projection_receipts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_projection_receipts_tenant_id_id"); + + b.HasIndex("TenantId", "OutboxMessageId", "ProjectionName") + .IsUnique() + .HasDatabaseName("ix_learning_projection_receipts_tenant_id_outbox_message_id_pr~"); + + b.ToTable("learning_projection_receipts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.MarketRegion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("ParentCode") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("parent_code"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_market_regions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_market_regions_code"); + + b.ToTable("market_regions", (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("AccessClassAssignmentId") + .HasColumnType("uuid") + .HasColumnName("access_class_assignment_id"); + + b.Property("AccessEntitlementId") + .HasColumnType("uuid") + .HasColumnName("access_entitlement_id"); + + b.Property("AccessGrantVersion") + .HasColumnType("bigint") + .HasColumnName("access_grant_version"); + + 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("AuthorizationExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("authorization_expires_at"); + + 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("StrongRevocationVersion") + .HasColumnType("bigint") + .HasColumnName("strong_revocation_version"); + + 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", "StrongRevocationVersion") + .HasDatabaseName("ix_practice_sessions_tenant_id_user_id_strong_revocation_versi~"); + + 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("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("ContentReleaseQuestionId") + .HasColumnType("uuid") + .HasColumnName("content_release_question_id"); + + b.Property("DifficultySnapshot") + .HasColumnType("integer") + .HasColumnName("difficulty_snapshot"); + + b.Property("GradingRulesSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("grading_rules_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Position") + .HasColumnType("integer") + .HasColumnName("position"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("QuestionOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_owner_tenant_id"); + + b.Property("QuestionPlacementId") + .HasColumnType("uuid") + .HasColumnName("question_placement_id"); + + b.Property("QuestionReferenceId") + .HasColumnType("uuid") + .HasColumnName("question_reference_id"); + + b.Property("QuestionRevisionId") + .HasColumnType("uuid") + .HasColumnName("question_revision_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", "ContentReleaseQuestionId") + .IsUnique() + .HasDatabaseName("ix_practice_session_questions_tenant_id_practice_session_id_co~") + .HasFilter("content_release_question_id is not null"); + + 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.ProductAccessManifest", 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LearningProductId") + .HasColumnType("uuid") + .HasColumnName("learning_product_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_product_access_manifests"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_access_manifests_tenant_id_id"); + + b.HasIndex("TenantId", "LearningProductId") + .IsUnique() + .HasDatabaseName("ix_product_access_manifests_tenant_id_learning_product_id"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_product_access_manifests_tenant_id_id_current_version_id"); + + b.ToTable("product_access_manifests", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductAccessManifestVersion", 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("DailyQuestionLimit") + .HasColumnType("integer") + .HasColumnName("daily_question_limit"); + + b.Property("MaxActiveTargets") + .HasColumnType("integer") + .HasColumnName("max_active_targets"); + + b.Property("MaxAlternateTargets") + .HasColumnType("integer") + .HasColumnName("max_alternate_targets"); + + b.Property("ProductAccessManifestId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_id"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalQuestionLimit") + .HasColumnType("integer") + .HasColumnName("total_question_limit"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_product_access_manifest_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_access_manifest_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "ProductAccessManifestId", "Id") + .HasName("ak_product_access_manifest_versions_tenant_id_product_access_m~"); + + b.HasIndex("TenantId", "ProductAccessManifestId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_product_access_manifest_versions_tenant_id_product_access_m~"); + + b.ToTable("product_access_manifest_versions", null, t => + { + t.HasCheckConstraint("ck_product_manifest_active_targets", "max_active_targets between 1 and 16"); + + t.HasCheckConstraint("ck_product_manifest_alternate_targets", "max_alternate_targets between 0 and max_active_targets"); + + t.HasCheckConstraint("ck_product_manifest_daily_question_limit", "daily_question_limit is null or daily_question_limit >= 0"); + + t.HasCheckConstraint("ck_product_manifest_total_question_limit", "total_question_limit is null or total_question_limit >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("content_owner_tenant_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("PlatformContentPackageOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("platform_content_package_owner_tenant_id"); + + b.Property("PlatformContentPackageVersionId") + .HasColumnType("uuid") + .HasColumnName("platform_content_package_version_id"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_product_manifest_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_manifest_releases_tenant_id_id"); + + b.HasIndex("ContentOwnerTenantId", "ContentReleaseId") + .HasDatabaseName("ix_product_manifest_releases_content_owner_tenant_id_content_r~"); + + b.HasIndex("PlatformContentPackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasDatabaseName("ix_product_manifest_releases_platform_content_package_owner_te~"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "ContentOwnerTenantId", "ContentReleaseId") + .IsUnique() + .HasDatabaseName("ix_product_manifest_releases_tenant_id_product_access_manifest~"); + + b.ToTable("product_manifest_releases", null, t => + { + t.HasCheckConstraint("ck_product_manifest_release_platform_pair", "(platform_content_package_owner_tenant_id is null) = (platform_content_package_version_id is null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("display_name"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_type"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_product_manifest_resources"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_manifest_resources_tenant_id_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "ResourceType", "ResourceId") + .IsUnique() + .HasDatabaseName("ux_product_manifest_resource"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "SortOrder", "Id") + .HasDatabaseName("ix_product_manifest_resource_order"); + + b.ToTable("product_manifest_resources", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("MayBeAlternate") + .HasColumnType("boolean") + .HasColumnName("may_be_alternate"); + + b.Property("MayBePrimary") + .HasColumnType("boolean") + .HasColumnName("may_be_primary"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_product_manifest_targets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_manifest_targets_tenant_id_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_product_manifest_targets_exam_target_profile_version_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_product_manifest_targets_tenant_id_product_access_manifest_~"); + + b.ToTable("product_manifest_targets", null, t => + { + t.HasCheckConstraint("ck_product_manifest_target_role", "may_be_primary or may_be_alternate"); + }); + }); + + 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.StudentEntitlement", 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("LearningProductId") + .HasColumnType("uuid") + .HasColumnName("learning_product_id"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + 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("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_type"); + + 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.Property("UsedQuestionCount") + .HasColumnType("integer") + .HasColumnName("used_question_count"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_student_entitlements_v2"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_entitlements_v2_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_entitlements_v2_user_id"); + + b.HasIndex("TenantId", "LearningProductId") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_learning_product_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_product_access_manifest_v~"); + + b.HasIndex("TenantId", "SourceType", "SourceId") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_source_type_source_id") + .HasFilter("source_id is not null"); + + b.HasIndex("TenantId", "UserId", "Status", "StartsAt", "EndsAt") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_user_id_status_starts_at_~"); + + b.ToTable("student_entitlements_v2", null, t => + { + t.HasCheckConstraint("ck_student_entitlement_v2_period", "ends_at is null or ends_at > starts_at"); + + t.HasCheckConstraint("ck_student_entitlements_v2_used_question_count", "used_question_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetRegionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ChangedBy") + .HasColumnType("uuid") + .HasColumnName("changed_by"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("effective_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("IsCurrent") + .HasColumnType("boolean") + .HasColumnName("is_current"); + + b.Property("MarketRegionId") + .HasColumnType("uuid") + .HasColumnName("market_region_id"); + + b.Property("Reason") + .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.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_student_target_region_history"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_target_region_history_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_student_target_region_history_business_line_id"); + + b.HasIndex("ChangedBy") + .HasDatabaseName("ix_student_target_region_history_changed_by"); + + b.HasIndex("MarketRegionId") + .HasDatabaseName("ix_student_target_region_history_market_region_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_target_region_history_user_id"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ux_student_target_region_current") + .HasFilter("is_current"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId", "EffectiveAt") + .HasDatabaseName("ix_student_target_region_history"); + + b.ToTable("student_target_region_history", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetSelectionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ChangedBy") + .HasColumnType("uuid") + .HasColumnName("changed_by"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("effective_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("EntitlementId") + .HasColumnType("uuid") + .HasColumnName("entitlement_id"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("IsCurrent") + .HasColumnType("boolean") + .HasColumnName("is_current"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("role"); + + b.Property("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_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.HasKey("Id") + .HasName("pk_student_target_selection_history"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_target_selection_history_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_student_target_selection_history_business_line_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_student_target_selection_history_exam_target_profile_versio~"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_target_selection_history_user_id"); + + b.HasIndex("TenantId", "EntitlementId") + .HasDatabaseName("ix_student_target_selection_history_tenant_id_entitlement_id"); + + b.HasIndex("TenantId", "SourceType", "SourceId") + .HasDatabaseName("ix_student_target_selection_history_tenant_id_source_type_sour~"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ux_student_target_primary") + .HasFilter("is_current and role = 'primary'"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId", "EffectiveAt") + .HasDatabaseName("ix_student_target_selection_history"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_student_target_selection_history_tenant_id_user_id_business~") + .HasFilter("is_current"); + + b.ToTable("student_target_selection_history", null, t => + { + t.HasCheckConstraint("ck_student_target_selection_source", "(source_type = 'student_entitlement' and entitlement_id = source_id) or (source_type = 'class_assignment_grant' and entitlement_id is null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantBusinessLicense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("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.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_tenant_business_licenses"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_business_licenses_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_tenant_business_licenses_business_line_id"); + + b.HasIndex("TenantId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ix_tenant_business_licenses_tenant_id_business_line_id"); + + b.ToTable("tenant_business_licenses", null, t => + { + t.HasCheckConstraint("ck_tenant_business_license_period", "ends_at is null or ends_at > starts_at"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowsAnyTargetRegion") + .HasColumnType("boolean") + .HasColumnName("allows_any_target_region"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("IncludesNational") + .HasColumnType("boolean") + .HasColumnName("includes_national"); + + b.Property("IsPrimary") + .HasColumnType("boolean") + .HasColumnName("is_primary"); + + 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.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_tenant_learning_licenses"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_learning_licenses_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_tenant_learning_licenses_business_line_id"); + + b.HasIndex("TenantId") + .IsUnique() + .HasDatabaseName("ix_tenant_learning_licenses_tenant_id") + .HasFilter("is_primary and status = 'active'"); + + b.HasIndex("TenantId", "BusinessLineId", "Status") + .HasDatabaseName("ix_tenant_learning_licenses_tenant_id_business_line_id_status"); + + b.ToTable("tenant_learning_licenses", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicenseRegion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("IsBaseRegion") + .HasColumnType("boolean") + .HasColumnName("is_base_region"); + + b.Property("LicenseId") + .HasColumnType("uuid") + .HasColumnName("license_id"); + + b.Property("MarketRegionId") + .HasColumnType("uuid") + .HasColumnName("market_region_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_tenant_learning_license_regions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_learning_license_regions_tenant_id_id"); + + b.HasIndex("MarketRegionId") + .HasDatabaseName("ix_tenant_learning_license_regions_market_region_id"); + + b.HasIndex("TenantId", "LicenseId", "MarketRegionId") + .IsUnique() + .HasDatabaseName("ix_tenant_learning_license_regions_tenant_id_license_id_market~"); + + b.ToTable("tenant_learning_license_regions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLicensedTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("IsBaseTarget") + .HasColumnType("boolean") + .HasColumnName("is_base_target"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_at"); + + b.Property("TenantBusinessLicenseId") + .HasColumnType("uuid") + .HasColumnName("tenant_business_license_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_tenant_licensed_targets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_licensed_targets_tenant_id_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_tenant_licensed_targets_exam_target_profile_version_id"); + + b.HasIndex("TenantId", "TenantBusinessLicenseId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_tenant_licensed_targets_tenant_id_tenant_business_license_i~"); + + b.ToTable("tenant_licensed_targets", null, t => + { + t.HasCheckConstraint("ck_tenant_licensed_target_period", "ends_at is null or ends_at > starts_at"); + }); + }); + + 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.Learning.WrongQuestionV2", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("QuestionAssetOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_asset_owner_tenant_id"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("LastQuestionPlacementId") + .HasColumnType("uuid") + .HasColumnName("last_question_placement_id"); + + b.Property("LastQuestionRevisionId") + .HasColumnType("uuid") + .HasColumnName("last_question_revision_id"); + + b.Property("LastWrongAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_wrong_at") + .HasDefaultValueSql("now()"); + + 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", "QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasName("pk_wrong_questions_v2"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_wrong_questions_v2_user_id"); + + b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId", "LastQuestionRevisionId") + .HasDatabaseName("ix_wrong_questions_v2_question_asset_owner_tenant_id_question_~"); + + b.HasIndex("TenantId", "UserId", "LastWrongAt") + .HasDatabaseName("ix_wrong_questions_v2_tenant_id_user_id_last_wrong_at"); + + b.ToTable("wrong_questions_v2", (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.PlatformApprovalPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AlwaysRequireApproval") + .HasColumnType("boolean") + .HasColumnName("always_require_approval"); + + b.Property("AmountThresholdCents") + .HasColumnType("integer") + .HasColumnName("amount_threshold_cents"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .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("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("ExpiresAfterHours") + .HasColumnType("integer") + .HasColumnName("expires_after_hours"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RequiredPermission") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("required_permission"); + + 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_platform_approval_policies"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_approval_policies_code"); + + b.ToTable("platform_approval_policies", null, t => + { + t.HasCheckConstraint("ck_platform_approval_policies_expiry", "expires_after_hours between 1 and 720"); + + t.HasCheckConstraint("ck_platform_approval_policies_threshold", "amount_threshold_cents is null or amount_threshold_cents > 0"); + + t.HasCheckConstraint("ck_platform_approval_policies_version", "version > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformApprovalRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("CommandType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("command_type"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("uuid") + .HasColumnName("concurrency_stamp"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DecidedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("decided_at"); + + b.Property("DecidedBy") + .HasColumnType("uuid") + .HasColumnName("decided_by"); + + b.Property("DecisionReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("decision_reason"); + + b.Property("Error") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("error"); + + b.Property("ExecutedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_at"); + + 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("PolicyCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("policy_code"); + + b.Property("PolicyVersion") + .HasColumnType("integer") + .HasColumnName("policy_version"); + + b.Property("RequestHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("request_hash"); + + b.Property("RequestNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("request_no"); + + b.Property("RequestReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("request_reason"); + + b.Property("RequestSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("request_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RequestedBy") + .HasColumnType("uuid") + .HasColumnName("requested_by"); + + b.Property("RequiredPermission") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("required_permission"); + + b.Property("ResultSnapshot") + .HasColumnType("jsonb") + .HasColumnName("result_snapshot"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TargetId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("target_id"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("target_type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_approval_requests"); + + b.HasIndex("DecidedBy") + .HasDatabaseName("ix_platform_approval_requests_decided_by"); + + b.HasIndex("RequestNo") + .IsUnique() + .HasDatabaseName("ix_platform_approval_requests_request_no"); + + b.HasIndex("RequestedBy", "CommandType", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_approval_requests_requested_by_command_type_idempo~"); + + b.HasIndex("Status", "ExpiresAt", "CreatedAt") + .HasDatabaseName("ix_platform_approval_requests_status_expires_at_created_at"); + + b.ToTable("platform_approval_requests", (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.PlatformConfigurationDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowRuntimeManagement") + .HasColumnType("boolean") + .HasColumnName("allow_runtime_management"); + + 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("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("IsSensitive") + .HasColumnType("boolean") + .HasColumnName("is_sensitive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("ValidationSchema") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("validation_schema") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ValueType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("value_type"); + + b.HasKey("Id") + .HasName("pk_platform_configuration_definitions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_configuration_definitions_code"); + + b.HasIndex("Category", "Code") + .HasDatabaseName("ix_platform_configuration_definitions_category_code"); + + b.ToTable("platform_configuration_definitions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformConfigurationVersion", 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("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id"); + + b.Property("Environment") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)") + .HasColumnName("environment"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("PublishedBy") + .HasColumnType("uuid") + .HasColumnName("published_by"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("RolledBackFromVersionId") + .HasColumnType("uuid") + .HasColumnName("rolled_back_from_version_id"); + + 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.Property("Value") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("value") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_platform_configuration_versions"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_platform_configuration_versions_created_by"); + + b.HasIndex("PublishedBy") + .HasDatabaseName("ix_platform_configuration_versions_published_by"); + + b.HasIndex("RolledBackFromVersionId") + .HasDatabaseName("ix_platform_configuration_versions_rolled_back_from_version_id"); + + b.HasIndex("DefinitionId", "Environment", "Status") + .HasDatabaseName("ix_platform_configuration_versions_definition_id_environment_s~"); + + b.HasIndex("DefinitionId", "Environment", "Version") + .IsUnique() + .HasDatabaseName("ix_platform_configuration_versions_definition_id_environment_v~"); + + b.ToTable("platform_configuration_versions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformNotificationDelivery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("body"); + + 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("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("LastError") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("last_error"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RecipientRoleCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("recipient_role_code"); + + b.Property("RecipientUserId") + .HasColumnType("uuid") + .HasColumnName("recipient_user_id"); + + 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("Subject") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("subject"); + + b.Property("TemplateId") + .HasColumnType("uuid") + .HasColumnName("template_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_notification_deliveries"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_platform_notification_deliveries_created_by"); + + b.HasIndex("RecipientUserId") + .HasDatabaseName("ix_platform_notification_deliveries_recipient_user_id"); + + b.HasIndex("Status", "CreatedAt") + .HasDatabaseName("ix_platform_notification_deliveries_status_created_at"); + + b.HasIndex("TemplateId", "RecipientUserId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_notification_deliveries_template_id_recipient_user~"); + + b.ToTable("platform_notification_deliveries", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformNotificationTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BodyTemplate") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("body_template"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("channel"); + + 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("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("SubjectTemplate") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("subject_template"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("variables") + .HasDefaultValueSql("'[]'::jsonb"); + + b.HasKey("Id") + .HasName("pk_platform_notification_templates"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_notification_templates_code"); + + b.ToTable("platform_notification_templates", (string)null); + }); + + 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("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + 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("QuestionType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("choice") + .HasColumnName("question_type"); + + 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("Tags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("tags") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TypeLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label"); + + 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("CellId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("cell-01") + .HasColumnName("cell_id"); + + 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.HasIndex("CellId", "Status") + .HasDatabaseName("ix_tenants_cell_id_status"); + + 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"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "LearningProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_entitlements_learning_products_tenant_id_learning_product_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.AssessmentPolicy", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_assessment_policies_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_policies_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "AssessmentPolicyId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_assessment_policies_assessment_policy_versions_tenant_id_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentPolicyVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_policy_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicy", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_policy_versions_assessment_policies_tenant_id_as~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentScoreBand", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_score_bands_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_score_bands_assessment_policy_versions_tenant_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegment", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segments_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segments_content_releases_tenant_id_content_releas~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegmentMember", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_audience_segment_members_exam_target_profile_versions_exam_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segment_members_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AudienceSegment", null) + .WithMany() + .HasForeignKey("TenantId", "AudienceSegmentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segment_members_audience_segments_tenant_id_audien~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BlueprintRelease", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_blueprint_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CollectionRelease", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_blueprint_releases_collection_releases_tenant_id_collection~"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_blueprint_releases_content_releases_tenant_id_content_relea~"); + + b.HasOne("Tiku.Domain.Content.ExamPaperSpecificationVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ExamPaperSpecificationVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_blueprint_releases_exam_paper_specification_versions_tenant~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetDimensionRequirement", b => + { + b.HasOne("Tiku.Domain.Content.BusinessTargetPolicyVersion", null) + .WithMany() + .HasForeignKey("BusinessTargetPolicyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_target_dimension_requirements_business_target_poli~"); + + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_business_target_dimension_requirements_target_dimension_def~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicy", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_business_target_policies_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Content.BusinessTargetPolicyVersion", null) + .WithMany() + .HasForeignKey("Id", "CurrentVersionId") + .HasPrincipalKey("BusinessTargetPolicyId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_business_target_policies_business_target_policy_versions_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicyVersion", b => + { + b.HasOne("Tiku.Domain.Content.BusinessTargetPolicy", null) + .WithMany() + .HasForeignKey("BusinessTargetPolicyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_target_policy_versions_business_target_policies_bu~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionRelease", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_collection_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_collection_releases_content_releases_tenant_id_content_rele~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionReleaseQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_collection_release_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CollectionRelease", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_collection_release_questions_collection_releases_tenant_id_~"); + + b.HasOne("Tiku.Domain.Content.ContentReleaseQuestion", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseQuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_collection_release_questions_content_release_questions_tena~"); + }); + + 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.ContentRelease", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_releases_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_releases_curriculum_versions_tenant_id_curriculum_v~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentReleaseQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_release_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_assessment_policy_versions_tenant~"); + + b.HasOne("Tiku.Domain.Content.AudienceSegment", null) + .WithMany() + .HasForeignKey("TenantId", "AudienceSegmentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_audience_segments_tenant_id_audie~"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_release_questions_content_releases_tenant_id_conten~"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacement", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionPlacementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_question_placements_tenant_id_que~"); + + b.HasOne("Tiku.Domain.Content.QuestionRevision", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId", "QuestionRevisionId") + .HasPrincipalKey("TenantId", "QuestionAssetId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_question_revisions_question_asset~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.Curriculum", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_curricula_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curricula_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "CurriculumId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_curricula_curriculum_versions_tenant_id_id_current_version_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNode", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_nodes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_nodes_curriculum_versions_tenant_id_curriculum_v~"); + + b.HasOne("Tiku.Domain.Content.KnowledgeConcept", null) + .WithMany() + .HasForeignKey("TenantId", "KnowledgeConceptId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_curriculum_nodes_knowledge_concepts_tenant_id_knowledge_con~"); + + b.HasOne("Tiku.Domain.Content.CurriculumNode", null) + .WithMany() + .HasForeignKey("TenantId", "ParentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_curriculum_nodes_curriculum_nodes_tenant_id_parent_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNodeClosure", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_node_closure_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.Curriculum", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_versions_curricula_tenant_id_curriculum_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecification", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_paper_specifications_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_paper_specifications_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecificationVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_paper_specification_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ExamPaperSpecification", null) + .WithMany() + .HasForeignKey("TenantId", "SpecificationId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_paper_specification_versions_exam_paper_specifications~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfile", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_target_profiles_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("Id", "CurrentVersionId") + .HasPrincipalKey("ExamTargetProfileId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_exam_target_profiles_exam_target_profile_versions_id_curren~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileValue", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_values_exam_target_profile_versions_exa~"); + + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_values_target_dimension_definitions_tar~"); + + b.HasOne("Tiku.Domain.Content.TargetNode", null) + .WithMany() + .HasForeignKey("TargetNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_values_target_nodes_target_node_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileVersion", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfile", null) + .WithMany() + .HasForeignKey("ExamTargetProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_versions_exam_target_profiles_exam_targ~"); + }); + + 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.KnowledgeConcept", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_knowledge_concepts_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ObjectiveGradingRule", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_objective_grading_rules_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_objective_grading_rules_assessment_policy_versions_tenant_i~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageCellProjection", b => + { + b.HasOne("Tiku.Domain.Content.PlatformContentPackageVersion", null) + .WithMany() + .HasForeignKey("PackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_content_package_cell_projections_platform_content_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_content_package_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_content_package_versions_content_releases_tenant_i~"); + }); + + 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.QuestionAsset", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_assets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionFamily", null) + .WithMany() + .HasForeignKey("TenantId", "FamilyId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_assets_question_families_tenant_id_family_id"); + + b.HasOne("Tiku.Domain.Content.QuestionRevision", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentRevisionId") + .HasPrincipalKey("TenantId", "QuestionAssetId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_assets_question_revisions_tenant_id_id_current_rev~"); + }); + + 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.QuestionFamily", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_families_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacement", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placements_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionAsset", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_question_assets_question_asset_owner_te~"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_assessment_policy_versions_tenant_id_as~"); + + b.HasOne("Tiku.Domain.Content.CurriculumNode", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_curriculum_nodes_tenant_id_curriculum_n~"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_curriculum_versions_tenant_id_curriculu~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementConcept", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_concepts_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.KnowledgeConcept", null) + .WithMany() + .HasForeignKey("TenantId", "KnowledgeConceptId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placement_concepts_knowledge_concepts_tenant_id_kn~"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacement", null) + .WithMany() + .HasForeignKey("TenantId", "PlacementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_concepts_question_placements_tenant_id_p~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleCondition", b => + { + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_target_dimension_definit~"); + + b.HasOne("Tiku.Domain.Content.TargetNode", null) + .WithMany() + .HasForeignKey("TargetNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_target_nodes_target_node~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacementRuleGroup", null) + .WithMany() + .HasForeignKey("TenantId", "RuleGroupId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_question_placement_rule_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleGroup", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_groups_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacement", null) + .WithMany() + .HasForeignKey("TenantId", "PlacementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_groups_question_placements_tenant_i~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionRevision", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_revisions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionAsset", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_revisions_question_assets_tenant_id_question_asset~"); + }); + + 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.ScoreAdjustmentRule", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_score_adjustment_rules_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_score_adjustment_rules_assessment_policy_versions_tenant_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.SubjectiveRubric", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subjective_rubrics_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subjective_rubrics_assessment_policy_versions_tenant_id_ass~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TargetNode", b => + { + b.HasOne("Tiku.Domain.Content.TargetNode", null) + .WithMany() + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_target_nodes_target_nodes_parent_id"); + + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_target_nodes_target_dimension_definitions_target_dimension_~"); + }); + + 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.ClassAssignmentGrant", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_assignment_grants_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Tenancy.TenantClass", null) + .WithMany() + .HasForeignKey("TenantId", "ClassId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_assignment_grants_tenant_classes_tenant_id_class_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_class_assignment_grants_product_access_manifest_versions_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ClassContentAssignment", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_class_content_assignments_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RevokedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_class_content_assignments_users_revoked_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_content_assignments_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ContentSlice", null) + .WithMany() + .HasForeignKey("ContentSliceOwnerTenantId", "ContentSliceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_class_content_assignments_content_slices_content_slice_owne~"); + + b.HasOne("Tiku.Domain.Tenancy.TenantClass", null) + .WithMany() + .HasForeignKey("TenantId", "ClassId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_content_assignments_tenant_classes_tenant_id_class_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ContentSlice", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_slices_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Learning.MarketRegion", null) + .WithMany() + .HasForeignKey("MarketRegionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_slices_market_regions_market_region_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_slices_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.CurrentAnswer", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_current_answers_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.AnswerRecord", null) + .WithMany() + .HasForeignKey("TenantId", "AnswerRecordId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_current_answers_answer_records_tenant_id_answer_record_id"); + + b.HasOne("Tiku.Domain.Learning.PracticeSessionQuestion", null) + .WithMany() + .HasForeignKey("TenantId", "PracticeSessionId", "SessionQuestionId") + .HasPrincipalKey("TenantId", "PracticeSessionId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_current_answers_practice_session_questions_tenant_id_practi~"); + }); + + 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.EffectiveAccessProjection", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_effective_access_projections_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_effective_access_projections_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_effective_access_projections_users_user_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.LearningAccessVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_access_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_access_versions_users_user_id"); + }); + + 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.LearningOutboxMessage", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_outbox_messages_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProduct", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_learning_products_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_products_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProductScope", b => + { + b.HasOne("Tiku.Domain.Learning.ContentSlice", null) + .WithMany() + .HasForeignKey("ContentSliceOwnerTenantId", "ContentSliceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_learning_product_scopes_content_slices_content_slice_owner_~"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "ProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_product_scopes_learning_products_tenant_id_product~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProjectionReceipt", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_projection_receipts_tenants_tenant_id"); + }); + + 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) + .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) + .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.ProductAccessManifest", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifests_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "LearningProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifests_learning_products_tenant_id_learni~"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "ProductAccessManifestId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_product_access_manifests_product_access_manifest_versions_t~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductAccessManifestVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifest_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifest", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifest_versions_product_access_manifests_t~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestRelease", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("ContentOwnerTenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_product_manifest_releases_content_releases_content_owner_te~"); + + b.HasOne("Tiku.Domain.Content.PlatformContentPackageVersion", null) + .WithMany() + .HasForeignKey("PlatformContentPackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_product_manifest_releases_platform_content_package_versions~"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_releases_product_access_manifest_versions_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestResource", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_resources_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_resources_product_access_manifest_versions~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestTarget", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_product_manifest_targets_exam_target_profile_versions_exam_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_targets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_targets_product_access_manifest_versions_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.StudentEntitlement", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_users_user_id"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "LearningProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_learning_products_tenant_id_learnin~"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_product_access_manifest_versions_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetRegionHistory", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_business_lines_business_line_~"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ChangedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_student_target_region_history_users_changed_by"); + + b.HasOne("Tiku.Domain.Learning.MarketRegion", null) + .WithMany() + .HasForeignKey("MarketRegionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_market_regions_market_region_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetSelectionHistory", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_business_lines_business_li~"); + + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_exam_target_profile_versio~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_users_user_id"); + + b.HasOne("Tiku.Domain.Learning.StudentEntitlement", null) + .WithMany() + .HasForeignKey("TenantId", "EntitlementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_student_target_selection_history_student_entitlements_v2_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantBusinessLicense", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_business_licenses_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_business_licenses_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicense", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_learning_licenses_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_learning_licenses_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicenseRegion", b => + { + b.HasOne("Tiku.Domain.Learning.MarketRegion", null) + .WithMany() + .HasForeignKey("MarketRegionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_learning_license_regions_market_regions_market_regio~"); + + b.HasOne("Tiku.Domain.Learning.TenantLearningLicense", null) + .WithMany() + .HasForeignKey("TenantId", "LicenseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_learning_license_regions_tenant_learning_licenses_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLicensedTarget", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_licensed_targets_exam_target_profile_versions_exam_t~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_licensed_targets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.TenantBusinessLicense", null) + .WithMany() + .HasForeignKey("TenantId", "TenantBusinessLicenseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_licensed_targets_tenant_business_licenses_tenant_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.Learning.WrongQuestionV2", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_users_user_id"); + + b.HasOne("Tiku.Domain.Content.QuestionAsset", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_question_assets_question_asset_owner_ten~"); + + b.HasOne("Tiku.Domain.Content.QuestionRevision", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId", "LastQuestionRevisionId") + .HasPrincipalKey("TenantId", "QuestionAssetId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_question_revisions_question_asset_owner_~"); + }); + + 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.PlatformApprovalRequest", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("DecidedBy") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_approval_requests_users_decided_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RequestedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_approval_requests_users_requested_by"); + }); + + 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.PlatformConfigurationVersion", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_configuration_versions_users_created_by"); + + b.HasOne("Tiku.Domain.Platform.PlatformConfigurationDefinition", null) + .WithMany() + .HasForeignKey("DefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_configuration_versions_platform_configuration_defi~"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("PublishedBy") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_configuration_versions_users_published_by"); + + b.HasOne("Tiku.Domain.Platform.PlatformConfigurationVersion", null) + .WithMany() + .HasForeignKey("RolledBackFromVersionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_configuration_versions_platform_configuration_vers~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformNotificationDelivery", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_notification_deliveries_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RecipientUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_notification_deliveries_users_recipient_user_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformNotificationTemplate", null) + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_notification_deliveries_platform_notification_temp~"); + }); + + 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/20260805095745_AuditTargetSelectionSource.cs b/Tiku.Infrastructure/Persistence/Migrations/20260805095745_AuditTargetSelectionSource.cs new file mode 100644 index 0000000..0cb4454 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260805095745_AuditTargetSelectionSource.cs @@ -0,0 +1,81 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Tiku.Infrastructure.Persistence.Migrations +{ + /// + public partial class AuditTargetSelectionSource : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "source_id", + table: "student_target_selection_history", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "source_type", + table: "student_target_selection_history", + type: "character varying(100)", + maxLength: 100, + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateIndex( + name: "ix_student_target_selection_history_tenant_id_entitlement_id", + table: "student_target_selection_history", + columns: new[] { "tenant_id", "entitlement_id" }); + + migrationBuilder.CreateIndex( + name: "ix_student_target_selection_history_tenant_id_source_type_sour~", + table: "student_target_selection_history", + columns: new[] { "tenant_id", "source_type", "source_id" }); + + migrationBuilder.AddCheckConstraint( + name: "ck_student_target_selection_source", + table: "student_target_selection_history", + sql: "(source_type = 'student_entitlement' and entitlement_id = source_id) or (source_type = 'class_assignment_grant' and entitlement_id is null)"); + + migrationBuilder.AddForeignKey( + name: "fk_student_target_selection_history_student_entitlements_v2_te~", + table: "student_target_selection_history", + columns: new[] { "tenant_id", "entitlement_id" }, + principalTable: "student_entitlements_v2", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_student_target_selection_history_student_entitlements_v2_te~", + table: "student_target_selection_history"); + + migrationBuilder.DropIndex( + name: "ix_student_target_selection_history_tenant_id_entitlement_id", + table: "student_target_selection_history"); + + migrationBuilder.DropIndex( + name: "ix_student_target_selection_history_tenant_id_source_type_sour~", + table: "student_target_selection_history"); + + migrationBuilder.DropCheckConstraint( + name: "ck_student_target_selection_source", + table: "student_target_selection_history"); + + migrationBuilder.DropColumn( + name: "source_id", + table: "student_target_selection_history"); + + migrationBuilder.DropColumn( + name: "source_type", + table: "student_target_selection_history"); + } + } +} diff --git a/Tiku.Infrastructure/Persistence/Migrations/20260806014558_ContentV2WorkingSync.Designer.cs b/Tiku.Infrastructure/Persistence/Migrations/20260806014558_ContentV2WorkingSync.Designer.cs new file mode 100644 index 0000000..453a311 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260806014558_ContentV2WorkingSync.Designer.cs @@ -0,0 +1,26581 @@ +// +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("20260806014558_ContentV2WorkingSync")] + partial class ContentV2WorkingSync + { + /// + 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.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.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("LearningProductId") + .HasColumnType("uuid") + .HasColumnName("learning_product_id"); + + 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", "LearningProductId") + .HasDatabaseName("ix_entitlements_tenant_id_learning_product_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.AssessmentPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_assessment_policies"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_assessment_policies_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_assessment_policies_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_assessment_policies_tenant_id_code"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_assessment_policies_tenant_id_id_current_version_id"); + + b.ToTable("assessment_policies", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentPolicyVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_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("DefaultScore") + .HasColumnType("numeric") + .HasColumnName("default_score"); + + b.Property("GradingMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("grading_mode"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("RoundingScale") + .HasColumnType("integer") + .HasColumnName("rounding_scale"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_assessment_policy_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_assessment_policy_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "AssessmentPolicyId", "Id") + .HasName("ak_assessment_policy_versions_tenant_id_assessment_policy_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_assessment_policy_versions_tenant_id_assessment_policy_id_v~"); + + b.ToTable("assessment_policy_versions", null, t => + { + t.HasCheckConstraint("ck_assessment_policy_default_score", "default_score > 0"); + + t.HasCheckConstraint("ck_assessment_policy_rounding", "rounding_scale between 0 and 6"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentScoreBand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("label"); + + b.Property("MaxScore") + .HasColumnType("numeric") + .HasColumnName("max_score"); + + b.Property("MinScore") + .HasColumnType("numeric") + .HasColumnName("min_score"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_assessment_score_bands"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_assessment_score_bands_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "MinScore", "MaxScore") + .IsUnique() + .HasDatabaseName("ix_assessment_score_bands_tenant_id_assessment_policy_version_~"); + + b.ToTable("assessment_score_bands", null, t => + { + t.HasCheckConstraint("ck_assessment_score_band", "max_score >= min_score"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("RuleHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("rule_hash"); + + b.Property("SegmentOrdinal") + .HasColumnType("integer") + .HasColumnName("segment_ordinal"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_audience_segments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_audience_segments_tenant_id_id"); + + b.HasIndex("TenantId", "ContentReleaseId", "RuleHash") + .IsUnique() + .HasDatabaseName("ix_audience_segments_tenant_id_content_release_id_rule_hash"); + + b.HasIndex("TenantId", "ContentReleaseId", "SegmentOrdinal") + .IsUnique() + .HasDatabaseName("ix_audience_segments_tenant_id_content_release_id_segment_ordi~"); + + b.ToTable("audience_segments", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegmentMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AudienceSegmentId") + .HasColumnType("uuid") + .HasColumnName("audience_segment_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_audience_segment_members"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_audience_segment_members_tenant_id_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_audience_segment_members_exam_target_profile_version_id"); + + b.HasIndex("TenantId", "AudienceSegmentId") + .HasDatabaseName("ix_audience_segment_members_tenant_id_audience_segment_id"); + + b.HasIndex("TenantId", "ContentReleaseId", "AudienceSegmentId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_audience_segment_members_tenant_id_content_release_id_audie~"); + + b.HasIndex("TenantId", "ContentReleaseId", "ExamTargetProfileVersionId", "AudienceSegmentId") + .HasDatabaseName("ix_audience_segment_members_tenant_id_content_release_id_exam_~"); + + b.ToTable("audience_segment_members", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BlueprintRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssemblyRules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("assembly_rules") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CollectionReleaseId") + .HasColumnType("uuid") + .HasColumnName("collection_release_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ExamPaperSpecificationVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_paper_specification_version_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("SourceBlueprintId") + .HasColumnType("uuid") + .HasColumnName("source_blueprint_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.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_blueprint_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_blueprint_releases_tenant_id_id"); + + b.HasIndex("TenantId", "CollectionReleaseId") + .HasDatabaseName("ix_blueprint_releases_tenant_id_collection_release_id"); + + b.HasIndex("TenantId", "ContentReleaseId") + .HasDatabaseName("ix_blueprint_releases_tenant_id_content_release_id"); + + b.HasIndex("TenantId", "ExamPaperSpecificationVersionId") + .HasDatabaseName("ix_blueprint_releases_tenant_id_exam_paper_specification_versi~"); + + b.HasIndex("TenantId", "SourceBlueprintId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_blueprint_releases_tenant_id_source_blueprint_id_version_no"); + + b.ToTable("blueprint_releases", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetDimensionRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessTargetPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("business_target_policy_version_id"); + + b.Property("MaxValues") + .HasColumnType("integer") + .HasColumnName("max_values"); + + b.Property("MinValues") + .HasColumnType("integer") + .HasColumnName("min_values"); + + b.Property("RequiredForProfile") + .HasColumnType("boolean") + .HasColumnName("required_for_profile"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.HasKey("Id") + .HasName("pk_business_target_dimension_requirements"); + + b.HasIndex("TargetDimensionDefinitionId") + .HasDatabaseName("ix_business_target_dimension_requirements_target_dimension_def~"); + + b.HasIndex("BusinessTargetPolicyVersionId", "TargetDimensionDefinitionId") + .IsUnique() + .HasDatabaseName("ix_business_target_dimension_requirements_business_target_poli~"); + + b.ToTable("business_target_dimension_requirements", null, t => + { + t.HasCheckConstraint("ck_business_target_dimension_cardinality", "min_values >= 0 and max_values >= min_values"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_business_target_policies"); + + b.HasIndex("BusinessLineId") + .IsUnique() + .HasDatabaseName("ix_business_target_policies_business_line_id"); + + b.HasIndex("Id", "CurrentVersionId") + .HasDatabaseName("ix_business_target_policies_id_current_version_id"); + + b.ToTable("business_target_policies", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicyVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessTargetPolicyId") + .HasColumnType("uuid") + .HasColumnName("business_target_policy_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IncludesBaseContent") + .HasColumnType("boolean") + .HasColumnName("includes_base_content"); + + b.Property("MaxActiveTargets") + .HasColumnType("integer") + .HasColumnName("max_active_targets"); + + b.Property("MaxAlternateTargets") + .HasColumnType("integer") + .HasColumnName("max_alternate_targets"); + + b.Property("MaxPrimaryTargets") + .HasColumnType("integer") + .HasColumnName("max_primary_targets"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("SelectionMode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("selection_mode"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TargetChangeCooldownDays") + .HasColumnType("integer") + .HasColumnName("target_change_cooldown_days"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_business_target_policy_versions"); + + b.HasAlternateKey("BusinessTargetPolicyId", "Id") + .HasName("ak_business_target_policy_versions_business_target_policy_id_id"); + + b.HasIndex("BusinessTargetPolicyId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_business_target_policy_versions_business_target_policy_id_v~"); + + b.ToTable("business_target_policy_versions", null, t => + { + t.HasCheckConstraint("ck_business_target_policy_active", "max_active_targets between 1 and 16"); + + t.HasCheckConstraint("ck_business_target_policy_alternate", "max_alternate_targets between 0 and max_active_targets"); + + t.HasCheckConstraint("ck_business_target_policy_cooldown", "target_change_cooldown_days between 0 and 365"); + + t.HasCheckConstraint("ck_business_target_policy_primary", "max_primary_targets between 0 and max_active_targets"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("SourceCollectionId") + .HasColumnType("uuid") + .HasColumnName("source_collection_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.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_collection_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_collection_releases_tenant_id_id"); + + b.HasIndex("TenantId", "ContentReleaseId") + .HasDatabaseName("ix_collection_releases_tenant_id_content_release_id"); + + b.HasIndex("TenantId", "SourceCollectionId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_collection_releases_tenant_id_source_collection_id_version_~"); + + b.ToTable("collection_releases", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionReleaseQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CollectionReleaseId") + .HasColumnType("uuid") + .HasColumnName("collection_release_id"); + + b.Property("ContentReleaseQuestionId") + .HasColumnType("uuid") + .HasColumnName("content_release_question_id"); + + b.Property("Required") + .HasColumnType("boolean") + .HasColumnName("required"); + + b.Property("Score") + .HasColumnType("numeric") + .HasColumnName("score"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_collection_release_questions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_collection_release_questions_tenant_id_id"); + + b.HasIndex("TenantId", "ContentReleaseQuestionId") + .HasDatabaseName("ix_collection_release_questions_tenant_id_content_release_ques~"); + + b.HasIndex("TenantId", "CollectionReleaseId", "ContentReleaseQuestionId") + .IsUnique() + .HasDatabaseName("ux_collection_release_question_item"); + + b.HasIndex("TenantId", "CollectionReleaseId", "SortOrder") + .IsUnique() + .HasDatabaseName("ux_collection_release_question_order"); + + b.ToTable("collection_release_questions", (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.ContentRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("CompilerVersion") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("compiler_version"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("ReleaseNo") + .HasColumnType("integer") + .HasColumnName("release_no"); + + b.Property("SourceFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("source_fingerprint"); + + 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_content_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_releases_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_content_releases_business_line_id"); + + b.HasIndex("TenantId", "CurriculumVersionId", "ReleaseNo") + .IsUnique() + .HasDatabaseName("ix_content_releases_tenant_id_curriculum_version_id_release_no"); + + b.HasIndex("TenantId", "BusinessLineId", "Status", "PublishedAt") + .HasDatabaseName("ix_content_releases_tenant_id_business_line_id_status_publishe~"); + + b.ToTable("content_releases", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentReleaseQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("AudienceSegmentId") + .HasColumnType("uuid") + .HasColumnName("audience_segment_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CurriculumNodeId") + .HasColumnType("uuid") + .HasColumnName("curriculum_node_id"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("Ordinal") + .HasColumnType("integer") + .HasColumnName("ordinal"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionAssetOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_asset_owner_tenant_id"); + + b.Property("QuestionPlacementId") + .HasColumnType("uuid") + .HasColumnName("question_placement_id"); + + b.Property("QuestionRevisionId") + .HasColumnType("uuid") + .HasColumnName("question_revision_id"); + + b.Property("QuestionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("question_type"); + + 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_content_release_questions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_release_questions_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId") + .HasDatabaseName("ix_content_release_questions_tenant_id_assessment_policy_versi~"); + + b.HasIndex("TenantId", "AudienceSegmentId") + .HasDatabaseName("ix_content_release_questions_tenant_id_audience_segment_id"); + + b.HasIndex("TenantId", "QuestionPlacementId") + .HasDatabaseName("ix_content_release_questions_tenant_id_question_placement_id"); + + b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId", "QuestionRevisionId") + .HasDatabaseName("ix_content_release_questions_question_asset_owner_tenant_id_qu~"); + + b.HasIndex("TenantId", "ContentReleaseId", "AudienceSegmentId", "CurriculumNodeId", "Ordinal") + .IsUnique() + .HasDatabaseName("ix_content_release_questions_tenant_id_content_release_id_audi~"); + + b.HasIndex("TenantId", "ContentReleaseId", "AudienceSegmentId", "CurriculumNodeId", "Status", "Difficulty", "QuestionType", "Ordinal") + .HasDatabaseName("ix_content_release_question_candidates"); + + b.ToTable("content_release_questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.Curriculum", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_curricula"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curricula_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_curricula_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_curricula_tenant_id_code"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_curricula_tenant_id_id_current_version_id"); + + b.ToTable("curricula", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNode", 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("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("kind"); + + b.Property("KnowledgeConceptId") + .HasColumnType("uuid") + .HasColumnName("knowledge_concept_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_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_curriculum_nodes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curriculum_nodes_tenant_id_id"); + + b.HasIndex("TenantId", "KnowledgeConceptId") + .HasDatabaseName("ix_curriculum_nodes_tenant_id_knowledge_concept_id"); + + b.HasIndex("TenantId", "ParentId") + .HasDatabaseName("ix_curriculum_nodes_tenant_id_parent_id"); + + b.HasIndex("TenantId", "CurriculumVersionId", "Code") + .IsUnique() + .HasDatabaseName("ix_curriculum_nodes_tenant_id_curriculum_version_id_code"); + + b.HasIndex("TenantId", "CurriculumVersionId", "ParentId", "SortOrder") + .HasDatabaseName("ix_curriculum_nodes_tenant_id_curriculum_version_id_parent_id_~"); + + b.ToTable("curriculum_nodes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNodeClosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AncestorNodeId") + .HasColumnType("uuid") + .HasColumnName("ancestor_node_id"); + + b.Property("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("Depth") + .HasColumnType("integer") + .HasColumnName("depth"); + + b.Property("DescendantNodeId") + .HasColumnType("uuid") + .HasColumnName("descendant_node_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_curriculum_node_closure"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curriculum_node_closure_tenant_id_id"); + + b.HasIndex("TenantId", "CurriculumVersionId", "AncestorNodeId", "DescendantNodeId") + .IsUnique() + .HasDatabaseName("ix_curriculum_node_closure_tenant_id_curriculum_version_id_anc~"); + + b.HasIndex("TenantId", "CurriculumVersionId", "DescendantNodeId", "Depth") + .HasDatabaseName("ix_curriculum_node_closure_tenant_id_curriculum_version_id_des~"); + + b.ToTable("curriculum_node_closure", null, t => + { + t.HasCheckConstraint("ck_curriculum_node_closure_depth", "depth >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumVersion", 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("CurriculumId") + .HasColumnType("uuid") + .HasColumnName("curriculum_id"); + + b.Property("ExamYear") + .HasColumnType("integer") + .HasColumnName("exam_year"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_curriculum_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curriculum_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "CurriculumId", "Id") + .HasName("ak_curriculum_versions_tenant_id_curriculum_id_id"); + + b.HasIndex("TenantId", "CurriculumId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_curriculum_versions_tenant_id_curriculum_id_version_no"); + + b.ToTable("curriculum_versions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_paper_specifications"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_exam_paper_specifications_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_exam_paper_specifications_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_exam_paper_specifications_tenant_id_code"); + + b.ToTable("exam_paper_specifications", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecificationVersion", 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("DurationMinutes") + .HasColumnType("integer") + .HasColumnName("duration_minutes"); + + b.Property("ExamYear") + .HasColumnType("integer") + .HasColumnName("exam_year"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Sections") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sections") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("SpecificationId") + .HasColumnType("uuid") + .HasColumnName("specification_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalScore") + .HasColumnType("numeric") + .HasColumnName("total_score"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_exam_paper_specification_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_exam_paper_specification_versions_tenant_id_id"); + + b.HasIndex("TenantId", "SpecificationId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_exam_paper_specification_versions_tenant_id_specification_i~"); + + b.ToTable("exam_paper_specification_versions", null, t => + { + t.HasCheckConstraint("ck_exam_paper_spec_duration", "duration_minutes > 0"); + + t.HasCheckConstraint("ck_exam_paper_spec_total_score", "total_score > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("code"); + + 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_exam_target_profiles"); + + b.HasIndex("BusinessLineId", "Code") + .IsUnique() + .HasDatabaseName("ix_exam_target_profiles_business_line_id_code"); + + b.HasIndex("Id", "CurrentVersionId") + .HasDatabaseName("ix_exam_target_profiles_id_current_version_id"); + + b.ToTable("exam_target_profiles", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.Property("TargetNodeId") + .HasColumnType("uuid") + .HasColumnName("target_node_id"); + + b.HasKey("Id") + .HasName("pk_exam_target_profile_values"); + + b.HasIndex("TargetDimensionDefinitionId") + .HasDatabaseName("ix_exam_target_profile_values_target_dimension_definition_id"); + + b.HasIndex("TargetNodeId") + .HasDatabaseName("ix_exam_target_profile_values_target_node_id"); + + b.HasIndex("ExamTargetProfileVersionId", "TargetDimensionDefinitionId", "TargetNodeId") + .IsUnique() + .HasDatabaseName("ix_exam_target_profile_values_exam_target_profile_version_id_t~"); + + b.ToTable("exam_target_profile_values", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileVersion", 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(500) + .HasColumnType("character varying(500)") + .HasColumnName("display_name"); + + b.Property("ExamTargetProfileId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_id"); + + b.Property("ExamYear") + .HasColumnType("integer") + .HasColumnName("exam_year"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_exam_target_profile_versions"); + + b.HasAlternateKey("ExamTargetProfileId", "Id") + .HasName("ak_exam_target_profile_versions_exam_target_profile_id_id"); + + b.HasIndex("ExamTargetProfileId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_exam_target_profile_versions_exam_target_profile_id_version~"); + + b.ToTable("exam_target_profile_versions", (string)null); + }); + + 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.KnowledgeConcept", 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(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_knowledge_concepts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_knowledge_concepts_tenant_id_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_knowledge_concepts_tenant_id_code"); + + b.ToTable("knowledge_concepts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ObjectiveGradingRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Configuration") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("configuration") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RuleType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("rule_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_objective_grading_rules"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_objective_grading_rules_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "RuleType") + .IsUnique() + .HasDatabaseName("ix_objective_grading_rules_tenant_id_assessment_policy_version~"); + + b.ToTable("objective_grading_rules", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageCellProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CellId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("cell_id"); + + b.Property("ContentHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("content_hash"); + + b.Property("EventSequence") + .HasColumnType("bigint") + .HasColumnName("event_sequence"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("PackageCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("package_code"); + + b.Property("PackageOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("package_owner_tenant_id"); + + b.Property("PlatformContentPackageVersionId") + .HasColumnType("uuid") + .HasColumnName("platform_content_package_version_id"); + + b.Property("ProjectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("projected_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_platform_content_package_cell_projections"); + + b.HasIndex("PackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasDatabaseName("ix_platform_package_cell_owner_version"); + + b.HasIndex("PlatformContentPackageVersionId", "CellId") + .HasDatabaseName("ix_platform_package_cell_version"); + + b.HasIndex("PackageOwnerTenantId", "PackageCode", "CellId") + .IsUnique() + .HasDatabaseName("ux_platform_package_cell_cursor"); + + b.ToTable("platform_content_package_cell_projections", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ContentHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("content_hash"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("PackageCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("package_code"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_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.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_platform_content_package_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_content_package_versions_tenant_id_id"); + + b.HasIndex("TenantId", "ContentHash") + .IsUnique() + .HasDatabaseName("ix_platform_content_package_versions_tenant_id_content_hash"); + + b.HasIndex("TenantId", "ContentReleaseId") + .HasDatabaseName("ix_platform_content_package_versions_tenant_id_content_release~"); + + b.HasIndex("TenantId", "PackageCode", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_platform_content_package_versions_tenant_id_package_code_ve~"); + + b.ToTable("platform_content_package_versions", (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.QuestionAsset", 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("CurrentRevisionId") + .HasColumnType("uuid") + .HasColumnName("current_revision_id"); + + b.Property("DeliveryFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("delivery_fingerprint"); + + b.Property("FamilyId") + .HasColumnType("uuid") + .HasColumnName("family_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.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.HasKey("Id") + .HasName("pk_question_assets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_assets_tenant_id_id"); + + b.HasIndex("TenantId", "DeliveryFingerprint") + .IsUnique() + .HasDatabaseName("ix_question_assets_tenant_id_delivery_fingerprint"); + + b.HasIndex("TenantId", "FamilyId") + .HasDatabaseName("ix_question_assets_tenant_id_family_id"); + + b.HasIndex("TenantId", "Id", "CurrentRevisionId") + .HasDatabaseName("ix_question_assets_tenant_id_id_current_revision_id"); + + b.ToTable("question_assets", (string)null); + }); + + 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.QuestionFamily", 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("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_families"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_families_tenant_id_id"); + + b.ToTable("question_families", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_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("CurriculumNodeId") + .HasColumnType("uuid") + .HasColumnName("curriculum_node_id"); + + b.Property("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("DifficultyOverride") + .HasColumnType("integer") + .HasColumnName("difficulty_override"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionAssetOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_asset_owner_tenant_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.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + 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_question_placements"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placements_tenant_id_id"); + + b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasDatabaseName("ix_question_placements_question_asset_owner_tenant_id_question~"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId") + .HasDatabaseName("ix_question_placements_tenant_id_assessment_policy_version_id"); + + b.HasIndex("TenantId", "CurriculumNodeId") + .HasDatabaseName("ix_question_placements_tenant_id_curriculum_node_id"); + + b.HasIndex("TenantId", "CurriculumVersionId") + .HasDatabaseName("ix_question_placements_tenant_id_curriculum_version_id"); + + b.HasIndex("TenantId", "QuestionAssetOwnerTenantId", "QuestionAssetId", "CurriculumVersionId", "CurriculumNodeId", "AssessmentPolicyVersionId") + .IsUnique() + .HasDatabaseName("ix_question_placements_tenant_id_question_asset_owner_tenant_i~"); + + b.ToTable("question_placements", null, t => + { + t.HasCheckConstraint("ck_question_placements_valid_period", "valid_to is null or valid_from is null or valid_to > valid_from"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementConcept", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("KnowledgeConceptId") + .HasColumnType("uuid") + .HasColumnName("knowledge_concept_id"); + + b.Property("PlacementId") + .HasColumnType("uuid") + .HasColumnName("placement_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_placement_concepts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placement_concepts_tenant_id_id"); + + b.HasIndex("TenantId", "KnowledgeConceptId") + .HasDatabaseName("ix_question_placement_concepts_tenant_id_knowledge_concept_id"); + + b.HasIndex("TenantId", "PlacementId", "KnowledgeConceptId") + .IsUnique() + .HasDatabaseName("ix_question_placement_concepts_tenant_id_placement_id_knowledg~"); + + b.ToTable("question_placement_concepts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleCondition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("operator"); + + b.Property("RuleGroupId") + .HasColumnType("uuid") + .HasColumnName("rule_group_id"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.Property("TargetNodeId") + .HasColumnType("uuid") + .HasColumnName("target_node_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_placement_rule_conditions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placement_rule_conditions_tenant_id_id"); + + b.HasIndex("TargetDimensionDefinitionId") + .HasDatabaseName("ix_question_placement_rule_conditions_target_dimension_definit~"); + + b.HasIndex("TargetNodeId") + .HasDatabaseName("ix_question_placement_rule_conditions_target_node_id"); + + b.HasIndex("TenantId", "RuleGroupId", "TargetDimensionDefinitionId", "TargetNodeId", "Operator") + .IsUnique() + .HasDatabaseName("ix_question_placement_rule_conditions_tenant_id_rule_group_id_~"); + + b.ToTable("question_placement_rule_conditions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("GroupOrder") + .HasColumnType("integer") + .HasColumnName("group_order"); + + b.Property("PlacementId") + .HasColumnType("uuid") + .HasColumnName("placement_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_placement_rule_groups"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placement_rule_groups_tenant_id_id"); + + b.HasIndex("TenantId", "PlacementId", "GroupOrder") + .IsUnique() + .HasDatabaseName("ix_question_placement_rule_groups_tenant_id_placement_id_group~"); + + b.ToTable("question_placement_rule_groups", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionRevision", 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("DeliveryFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("delivery_fingerprint"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("Explanation") + .HasColumnType("text") + .HasColumnName("explanation"); + + b.Property("NormalizedContent") + .IsRequired() + .HasColumnType("text") + .HasColumnName("normalized_content"); + + b.Property("Options") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("options") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("question_type"); + + b.Property("RevisionNo") + .HasColumnType("integer") + .HasColumnName("revision_no"); + + b.Property("SubQuestions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sub_questions") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TypeLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label"); + + b.HasKey("Id") + .HasName("pk_question_revisions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_revisions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "QuestionAssetId", "Id") + .HasName("ak_question_revisions_tenant_id_question_asset_id_id"); + + b.HasIndex("NormalizedContent") + .HasDatabaseName("ix_question_revisions_content_trgm"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("NormalizedContent"), "gin"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("NormalizedContent"), new[] { "gin_trgm_ops" }); + + b.HasIndex("TenantId", "DeliveryFingerprint") + .HasDatabaseName("ix_question_revisions_tenant_id_delivery_fingerprint"); + + b.HasIndex("TenantId", "QuestionAssetId", "RevisionNo") + .IsUnique() + .HasDatabaseName("ix_question_revisions_tenant_id_question_asset_id_revision_no"); + + b.ToTable("question_revisions", (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.ScoreAdjustmentRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Configuration") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("configuration") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RuleType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("rule_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_score_adjustment_rules"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_score_adjustment_rules_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "Priority") + .IsUnique() + .HasDatabaseName("ix_score_adjustment_rules_tenant_id_assessment_policy_version_~"); + + b.ToTable("score_adjustment_rules", (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.SubjectiveRubric", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("Criteria") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("criteria") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("MaxScore") + .HasColumnType("numeric") + .HasColumnName("max_score"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_subjective_rubrics"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_subjective_rubrics_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "Code") + .IsUnique() + .HasDatabaseName("ix_subjective_rubrics_tenant_id_assessment_policy_version_id_c~"); + + b.ToTable("subjective_rubrics", null, t => + { + t.HasCheckConstraint("ck_subjective_rubric_max_score", "max_score > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TargetDimensionDefinition", 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsHierarchical") + .HasColumnType("boolean") + .HasColumnName("is_hierarchical"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_target_dimension_definitions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_target_dimension_definitions_code"); + + b.ToTable("target_dimension_definitions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TargetNode", 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .IsRequired() + .HasColumnType("ltree") + .HasColumnName("path"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_target_nodes"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_target_nodes_parent_id"); + + b.HasIndex("Path") + .HasDatabaseName("ix_target_nodes_path"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Path"), "gist"); + + b.HasIndex("TargetDimensionDefinitionId", "Code") + .IsUnique() + .HasDatabaseName("ix_target_nodes_target_dimension_definition_id_code"); + + b.ToTable("target_nodes", (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("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", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ux_answer_records_idempotency"); + + 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.BusinessLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RegionAccessStrategy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("region_access_strategy"); + + b.Property("RequiresBaseRegion") + .HasColumnType("boolean") + .HasColumnName("requires_base_region"); + + b.Property("TargetRegionCooldownDays") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30) + .HasColumnName("target_region_cooldown_days"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_business_lines"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_business_lines_code"); + + b.ToTable("business_lines", null, t => + { + t.HasCheckConstraint("ck_business_lines_target_region_cooldown_days", "target_region_cooldown_days between 0 and 365"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ClassAssignmentGrant", 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("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_type"); + + b.Property("RevokedBy") + .HasColumnType("uuid") + .HasColumnName("revoked_by"); + + b.Property("RevokedReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("revoked_reason"); + + 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_class_assignment_grants"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_class_assignment_grants_tenant_id_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId") + .HasDatabaseName("ix_class_assignment_grants_tenant_id_product_access_manifest_v~"); + + b.HasIndex("TenantId", "ClassId", "Status", "StartsAt") + .HasDatabaseName("ix_class_assignment_grants_tenant_id_class_id_status_starts_at"); + + b.HasIndex("TenantId", "ResourceType", "ResourceId", "Status") + .HasDatabaseName("ix_class_assignment_grants_tenant_id_resource_type_resource_id~"); + + b.ToTable("class_assignment_grants", null, t => + { + t.HasCheckConstraint("ck_class_assignment_grant_period", "ends_at is null or ends_at > starts_at"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ClassContentAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ClassId") + .HasColumnType("uuid") + .HasColumnName("class_id"); + + b.Property("ContentSliceId") + .HasColumnType("uuid") + .HasColumnName("content_slice_id"); + + b.Property("ContentSliceOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("content_slice_owner_tenant_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("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_type"); + + b.Property("RevokedBy") + .HasColumnType("uuid") + .HasColumnName("revoked_by"); + + b.Property("RevokedReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("revoked_reason"); + + 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_class_content_assignments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_class_content_assignments_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_class_content_assignments_created_by"); + + b.HasIndex("RevokedBy") + .HasDatabaseName("ix_class_content_assignments_revoked_by"); + + b.HasIndex("ContentSliceOwnerTenantId", "ContentSliceId") + .HasDatabaseName("ix_class_content_assignments_content_slice_owner_tenant_id_con~"); + + b.HasIndex("TenantId", "ClassId", "Status", "StartsAt") + .HasDatabaseName("ix_class_content_assignments_tenant_id_class_id_status_starts_~"); + + b.HasIndex("TenantId", "ResourceType", "ResourceId", "Status") + .HasDatabaseName("ix_class_content_assignments_tenant_id_resource_type_resource_~"); + + b.ToTable("class_content_assignments", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ContentSlice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ContentVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("content_version"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("MarketRegionId") + .HasColumnType("uuid") + .HasColumnName("market_region_id"); + + b.Property("RegionScope") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("region_scope"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_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_content_slices"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_slices_tenant_id_id"); + + b.HasIndex("MarketRegionId") + .HasDatabaseName("ix_content_slices_market_region_id"); + + b.HasIndex("BusinessLineId", "RegionScope", "MarketRegionId") + .HasDatabaseName("ix_content_slices_business_line_id_region_scope_market_region_~"); + + b.HasIndex("TenantId", "ResourceType", "ResourceId", "Status") + .HasDatabaseName("ix_content_slices_tenant_id_resource_type_resource_id_status"); + + b.ToTable("content_slices", null, t => + { + t.HasCheckConstraint("ck_content_slices_region_scope", "(region_scope = 'national' and market_region_id is null) or (region_scope = 'region' and market_region_id is not null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.CurrentAnswer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerRecordId") + .HasColumnType("uuid") + .HasColumnName("answer_record_id"); + + b.Property("ClientSequence") + .HasColumnType("bigint") + .HasColumnName("client_sequence"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("Revision") + .HasColumnType("integer") + .HasColumnName("revision"); + + b.Property("SessionQuestionId") + .HasColumnType("uuid") + .HasColumnName("session_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("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_current_answers"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_current_answers_tenant_id_id"); + + b.HasIndex("TenantId", "AnswerRecordId") + .HasDatabaseName("ix_current_answers_tenant_id_answer_record_id"); + + b.HasIndex("TenantId", "PracticeSessionId", "SessionQuestionId") + .HasDatabaseName("ix_current_answers_tenant_id_practice_session_id_session_quest~"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "SessionQuestionId") + .IsUnique() + .HasDatabaseName("ux_current_answers_session_question"); + + b.ToTable("current_answers", (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.EffectiveAccessProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.PrimitiveCollection("AlternateProfileVersionIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("alternate_profile_version_ids"); + + b.PrimitiveCollection("AudienceSegmentIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("audience_segment_ids"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("CompiledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("compiled_at"); + + b.PrimitiveCollection("ContentReleaseIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("content_release_ids"); + + b.Property("ContentVersion") + .HasColumnType("bigint") + .HasColumnName("content_version"); + + b.Property("GrantVersion") + .HasColumnType("bigint") + .HasColumnName("grant_version"); + + b.PrimitiveCollection("ManifestVersionIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("manifest_version_ids"); + + b.Property("PrimaryProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("primary_profile_version_id"); + + b.Property("StrongRevocationVersion") + .HasColumnType("bigint") + .HasColumnName("strong_revocation_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("valid_until"); + + b.HasKey("Id") + .HasName("pk_effective_access_projections"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_effective_access_projections_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_effective_access_projections_business_line_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_effective_access_projections_user_id"); + + b.HasIndex("TenantId", "ValidUntil") + .HasDatabaseName("ix_effective_access_projections_tenant_id_valid_until"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ix_effective_access_projections_tenant_id_user_id_business_lin~"); + + b.ToTable("effective_access_projections", (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.LearningAccessVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentVersion") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("content_version"); + + b.Property("GrantVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("grant_version"); + + b.Property("StrongRevocationVersion") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("strong_revocation_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_learning_access_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_access_versions_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_learning_access_versions_user_id"); + + b.HasIndex("TenantId", "UserId") + .IsUnique() + .HasDatabaseName("ix_learning_access_versions_tenant_id_user_id"); + + b.ToTable("learning_access_versions", (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.LearningOutboxMessage", b => + { + b.Property("OccurredAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_at") + .HasDefaultValueSql("now()"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AggregateId") + .HasColumnType("uuid") + .HasColumnName("aggregate_id"); + + b.Property("AttemptCount") + .HasColumnType("integer") + .HasColumnName("attempt_count"); + + b.Property("AvailableAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("available_at") + .HasDefaultValueSql("now()"); + + b.Property("DeadLetteredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("dead_lettered_at"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("event_type"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .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("Payload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at"); + + b.Property("SchemaVersion") + .HasColumnType("integer") + .HasColumnName("schema_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("OccurredAt", "Id") + .HasName("pk_learning_outbox_messages"); + + b.HasAlternateKey("TenantId", "OccurredAt", "Id") + .HasName("ak_learning_outbox_messages_tenant_id_occurred_at_id"); + + b.HasIndex("AvailableAt", "LockExpiresAt", "OccurredAt", "Id") + .HasDatabaseName("ix_learning_outbox_pending") + .HasFilter("processed_at IS NULL AND dead_lettered_at IS NULL"); + + b.HasIndex("TenantId", "EventType", "AggregateId", "OccurredAt") + .IsUnique() + .HasDatabaseName("ix_learning_outbox_messages_tenant_id_event_type_aggregate_id_~"); + + b.ToTable("learning_outbox_messages", null, t => + { + t.HasCheckConstraint("ck_learning_outbox_messages_attempt_count", "attempt_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProduct", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + 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_learning_products"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_products_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_learning_products_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_learning_products_tenant_id_code"); + + b.ToTable("learning_products", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProductScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentSliceId") + .HasColumnType("uuid") + .HasColumnName("content_slice_id"); + + b.Property("ContentSliceOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("content_slice_owner_tenant_id"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasColumnName("product_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_learning_product_scopes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_product_scopes_tenant_id_id"); + + b.HasIndex("ContentSliceOwnerTenantId", "ContentSliceId") + .HasDatabaseName("ix_learning_product_scopes_content_slice_owner_tenant_id_conte~"); + + b.HasIndex("TenantId", "ProductId", "ContentSliceId") + .IsUnique() + .HasDatabaseName("ix_learning_product_scopes_tenant_id_product_id_content_slice_~"); + + b.ToTable("learning_product_scopes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProjectionReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("OutboxMessageId") + .HasColumnType("uuid") + .HasColumnName("outbox_message_id"); + + b.Property("ProcessedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at") + .HasDefaultValueSql("now()"); + + b.Property("ProjectionName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("projection_name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_learning_projection_receipts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_learning_projection_receipts_tenant_id_id"); + + b.HasIndex("TenantId", "OutboxMessageId", "ProjectionName") + .IsUnique() + .HasDatabaseName("ix_learning_projection_receipts_tenant_id_outbox_message_id_pr~"); + + b.ToTable("learning_projection_receipts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.MarketRegion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("ParentCode") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("parent_code"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_market_regions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_market_regions_code"); + + b.ToTable("market_regions", (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("AccessClassAssignmentId") + .HasColumnType("uuid") + .HasColumnName("access_class_assignment_id"); + + b.Property("AccessEntitlementId") + .HasColumnType("uuid") + .HasColumnName("access_entitlement_id"); + + b.Property("AccessGrantVersion") + .HasColumnType("bigint") + .HasColumnName("access_grant_version"); + + 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("AuthorizationExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("authorization_expires_at"); + + 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("StrongRevocationVersion") + .HasColumnType("bigint") + .HasColumnName("strong_revocation_version"); + + 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", "StrongRevocationVersion") + .HasDatabaseName("ix_practice_sessions_tenant_id_user_id_strong_revocation_versi~"); + + 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("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("ContentReleaseQuestionId") + .HasColumnType("uuid") + .HasColumnName("content_release_question_id"); + + b.Property("DifficultySnapshot") + .HasColumnType("integer") + .HasColumnName("difficulty_snapshot"); + + b.Property("GradingRulesSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("grading_rules_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Position") + .HasColumnType("integer") + .HasColumnName("position"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("QuestionOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_owner_tenant_id"); + + b.Property("QuestionPlacementId") + .HasColumnType("uuid") + .HasColumnName("question_placement_id"); + + b.Property("QuestionReferenceId") + .HasColumnType("uuid") + .HasColumnName("question_reference_id"); + + b.Property("QuestionRevisionId") + .HasColumnType("uuid") + .HasColumnName("question_revision_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", "ContentReleaseQuestionId") + .IsUnique() + .HasDatabaseName("ix_practice_session_questions_tenant_id_practice_session_id_co~") + .HasFilter("content_release_question_id is not null"); + + 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.ProductAccessManifest", 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LearningProductId") + .HasColumnType("uuid") + .HasColumnName("learning_product_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_product_access_manifests"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_access_manifests_tenant_id_id"); + + b.HasIndex("TenantId", "LearningProductId") + .IsUnique() + .HasDatabaseName("ix_product_access_manifests_tenant_id_learning_product_id"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_product_access_manifests_tenant_id_id_current_version_id"); + + b.ToTable("product_access_manifests", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductAccessManifestVersion", 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("DailyQuestionLimit") + .HasColumnType("integer") + .HasColumnName("daily_question_limit"); + + b.Property("MaxActiveTargets") + .HasColumnType("integer") + .HasColumnName("max_active_targets"); + + b.Property("MaxAlternateTargets") + .HasColumnType("integer") + .HasColumnName("max_alternate_targets"); + + b.Property("ProductAccessManifestId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_id"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalQuestionLimit") + .HasColumnType("integer") + .HasColumnName("total_question_limit"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_product_access_manifest_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_access_manifest_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "ProductAccessManifestId", "Id") + .HasName("ak_product_access_manifest_versions_tenant_id_product_access_m~"); + + b.HasIndex("TenantId", "ProductAccessManifestId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_product_access_manifest_versions_tenant_id_product_access_m~"); + + b.ToTable("product_access_manifest_versions", null, t => + { + t.HasCheckConstraint("ck_product_manifest_active_targets", "max_active_targets between 1 and 16"); + + t.HasCheckConstraint("ck_product_manifest_alternate_targets", "max_alternate_targets between 0 and max_active_targets"); + + t.HasCheckConstraint("ck_product_manifest_daily_question_limit", "daily_question_limit is null or daily_question_limit >= 0"); + + t.HasCheckConstraint("ck_product_manifest_total_question_limit", "total_question_limit is null or total_question_limit >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("content_owner_tenant_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("PlatformContentPackageOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("platform_content_package_owner_tenant_id"); + + b.Property("PlatformContentPackageVersionId") + .HasColumnType("uuid") + .HasColumnName("platform_content_package_version_id"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_product_manifest_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_manifest_releases_tenant_id_id"); + + b.HasIndex("ContentOwnerTenantId", "ContentReleaseId") + .HasDatabaseName("ix_product_manifest_releases_content_owner_tenant_id_content_r~"); + + b.HasIndex("PlatformContentPackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasDatabaseName("ix_product_manifest_releases_platform_content_package_owner_te~"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "ContentOwnerTenantId", "ContentReleaseId") + .IsUnique() + .HasDatabaseName("ix_product_manifest_releases_tenant_id_product_access_manifest~"); + + b.ToTable("product_manifest_releases", null, t => + { + t.HasCheckConstraint("ck_product_manifest_release_platform_pair", "(platform_content_package_owner_tenant_id is null) = (platform_content_package_version_id is null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("display_name"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_type"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_product_manifest_resources"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_manifest_resources_tenant_id_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "ResourceType", "ResourceId") + .IsUnique() + .HasDatabaseName("ux_product_manifest_resource"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "SortOrder", "Id") + .HasDatabaseName("ix_product_manifest_resource_order"); + + b.ToTable("product_manifest_resources", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("MayBeAlternate") + .HasColumnType("boolean") + .HasColumnName("may_be_alternate"); + + b.Property("MayBePrimary") + .HasColumnType("boolean") + .HasColumnName("may_be_primary"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_product_manifest_targets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_manifest_targets_tenant_id_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_product_manifest_targets_exam_target_profile_version_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_product_manifest_targets_tenant_id_product_access_manifest_~"); + + b.ToTable("product_manifest_targets", null, t => + { + t.HasCheckConstraint("ck_product_manifest_target_role", "may_be_primary or may_be_alternate"); + }); + }); + + 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.StudentEntitlement", 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("LearningProductId") + .HasColumnType("uuid") + .HasColumnName("learning_product_id"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + 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("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_type"); + + 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.Property("UsedQuestionCount") + .HasColumnType("integer") + .HasColumnName("used_question_count"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_student_entitlements_v2"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_entitlements_v2_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_entitlements_v2_user_id"); + + b.HasIndex("TenantId", "LearningProductId") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_learning_product_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_product_access_manifest_v~"); + + b.HasIndex("TenantId", "SourceType", "SourceId") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_source_type_source_id") + .HasFilter("source_id is not null"); + + b.HasIndex("TenantId", "UserId", "Status", "StartsAt", "EndsAt") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_user_id_status_starts_at_~"); + + b.ToTable("student_entitlements_v2", null, t => + { + t.HasCheckConstraint("ck_student_entitlement_v2_period", "ends_at is null or ends_at > starts_at"); + + t.HasCheckConstraint("ck_student_entitlements_v2_used_question_count", "used_question_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetRegionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ChangedBy") + .HasColumnType("uuid") + .HasColumnName("changed_by"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("effective_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("IsCurrent") + .HasColumnType("boolean") + .HasColumnName("is_current"); + + b.Property("MarketRegionId") + .HasColumnType("uuid") + .HasColumnName("market_region_id"); + + b.Property("Reason") + .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.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_student_target_region_history"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_target_region_history_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_student_target_region_history_business_line_id"); + + b.HasIndex("ChangedBy") + .HasDatabaseName("ix_student_target_region_history_changed_by"); + + b.HasIndex("MarketRegionId") + .HasDatabaseName("ix_student_target_region_history_market_region_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_target_region_history_user_id"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ux_student_target_region_current") + .HasFilter("is_current"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId", "EffectiveAt") + .HasDatabaseName("ix_student_target_region_history"); + + b.ToTable("student_target_region_history", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetSelectionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ChangedBy") + .HasColumnType("uuid") + .HasColumnName("changed_by"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("effective_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("EntitlementId") + .HasColumnType("uuid") + .HasColumnName("entitlement_id"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("IsCurrent") + .HasColumnType("boolean") + .HasColumnName("is_current"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("role"); + + b.Property("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_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.HasKey("Id") + .HasName("pk_student_target_selection_history"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_target_selection_history_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_student_target_selection_history_business_line_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_student_target_selection_history_exam_target_profile_versio~"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_target_selection_history_user_id"); + + b.HasIndex("TenantId", "EntitlementId") + .HasDatabaseName("ix_student_target_selection_history_tenant_id_entitlement_id"); + + b.HasIndex("TenantId", "SourceType", "SourceId") + .HasDatabaseName("ix_student_target_selection_history_tenant_id_source_type_sour~"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ux_student_target_primary") + .HasFilter("is_current and role = 'primary'"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId", "EffectiveAt") + .HasDatabaseName("ix_student_target_selection_history"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_student_target_selection_history_tenant_id_user_id_business~") + .HasFilter("is_current"); + + b.ToTable("student_target_selection_history", null, t => + { + t.HasCheckConstraint("ck_student_target_selection_source", "(source_type = 'student_entitlement' and entitlement_id = source_id) or (source_type = 'class_assignment_grant' and entitlement_id is null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantBusinessLicense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("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.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_tenant_business_licenses"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_business_licenses_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_tenant_business_licenses_business_line_id"); + + b.HasIndex("TenantId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ix_tenant_business_licenses_tenant_id_business_line_id"); + + b.ToTable("tenant_business_licenses", null, t => + { + t.HasCheckConstraint("ck_tenant_business_license_period", "ends_at is null or ends_at > starts_at"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowsAnyTargetRegion") + .HasColumnType("boolean") + .HasColumnName("allows_any_target_region"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("IncludesNational") + .HasColumnType("boolean") + .HasColumnName("includes_national"); + + b.Property("IsPrimary") + .HasColumnType("boolean") + .HasColumnName("is_primary"); + + 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.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_tenant_learning_licenses"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_learning_licenses_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_tenant_learning_licenses_business_line_id"); + + b.HasIndex("TenantId") + .IsUnique() + .HasDatabaseName("ix_tenant_learning_licenses_tenant_id") + .HasFilter("is_primary and status = 'active'"); + + b.HasIndex("TenantId", "BusinessLineId", "Status") + .HasDatabaseName("ix_tenant_learning_licenses_tenant_id_business_line_id_status"); + + b.ToTable("tenant_learning_licenses", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicenseRegion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("IsBaseRegion") + .HasColumnType("boolean") + .HasColumnName("is_base_region"); + + b.Property("LicenseId") + .HasColumnType("uuid") + .HasColumnName("license_id"); + + b.Property("MarketRegionId") + .HasColumnType("uuid") + .HasColumnName("market_region_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_tenant_learning_license_regions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_learning_license_regions_tenant_id_id"); + + b.HasIndex("MarketRegionId") + .HasDatabaseName("ix_tenant_learning_license_regions_market_region_id"); + + b.HasIndex("TenantId", "LicenseId", "MarketRegionId") + .IsUnique() + .HasDatabaseName("ix_tenant_learning_license_regions_tenant_id_license_id_market~"); + + b.ToTable("tenant_learning_license_regions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLicensedTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("IsBaseTarget") + .HasColumnType("boolean") + .HasColumnName("is_base_target"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_at"); + + b.Property("TenantBusinessLicenseId") + .HasColumnType("uuid") + .HasColumnName("tenant_business_license_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_tenant_licensed_targets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_licensed_targets_tenant_id_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_tenant_licensed_targets_exam_target_profile_version_id"); + + b.HasIndex("TenantId", "TenantBusinessLicenseId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_tenant_licensed_targets_tenant_id_tenant_business_license_i~"); + + b.ToTable("tenant_licensed_targets", null, t => + { + t.HasCheckConstraint("ck_tenant_licensed_target_period", "ends_at is null or ends_at > starts_at"); + }); + }); + + 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.Learning.WrongQuestionV2", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("QuestionAssetOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_asset_owner_tenant_id"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("LastQuestionPlacementId") + .HasColumnType("uuid") + .HasColumnName("last_question_placement_id"); + + b.Property("LastQuestionRevisionId") + .HasColumnType("uuid") + .HasColumnName("last_question_revision_id"); + + b.Property("LastWrongAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_wrong_at") + .HasDefaultValueSql("now()"); + + 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", "QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasName("pk_wrong_questions_v2"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_wrong_questions_v2_user_id"); + + b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId", "LastQuestionRevisionId") + .HasDatabaseName("ix_wrong_questions_v2_question_asset_owner_tenant_id_question_~"); + + b.HasIndex("TenantId", "UserId", "LastWrongAt") + .HasDatabaseName("ix_wrong_questions_v2_tenant_id_user_id_last_wrong_at"); + + b.ToTable("wrong_questions_v2", (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.PlatformApprovalPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AlwaysRequireApproval") + .HasColumnType("boolean") + .HasColumnName("always_require_approval"); + + b.Property("AmountThresholdCents") + .HasColumnType("integer") + .HasColumnName("amount_threshold_cents"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .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("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("ExpiresAfterHours") + .HasColumnType("integer") + .HasColumnName("expires_after_hours"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RequiredPermission") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("required_permission"); + + 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_platform_approval_policies"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_approval_policies_code"); + + b.ToTable("platform_approval_policies", null, t => + { + t.HasCheckConstraint("ck_platform_approval_policies_expiry", "expires_after_hours between 1 and 720"); + + t.HasCheckConstraint("ck_platform_approval_policies_threshold", "amount_threshold_cents is null or amount_threshold_cents > 0"); + + t.HasCheckConstraint("ck_platform_approval_policies_version", "version > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformApprovalRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("CommandType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("command_type"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("uuid") + .HasColumnName("concurrency_stamp"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DecidedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("decided_at"); + + b.Property("DecidedBy") + .HasColumnType("uuid") + .HasColumnName("decided_by"); + + b.Property("DecisionReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("decision_reason"); + + b.Property("Error") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("error"); + + b.Property("ExecutedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_at"); + + 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("PolicyCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("policy_code"); + + b.Property("PolicyVersion") + .HasColumnType("integer") + .HasColumnName("policy_version"); + + b.Property("RequestHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("request_hash"); + + b.Property("RequestNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("request_no"); + + b.Property("RequestReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("request_reason"); + + b.Property("RequestSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("request_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RequestedBy") + .HasColumnType("uuid") + .HasColumnName("requested_by"); + + b.Property("RequiredPermission") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("required_permission"); + + b.Property("ResultSnapshot") + .HasColumnType("jsonb") + .HasColumnName("result_snapshot"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TargetId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("target_id"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("target_type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_approval_requests"); + + b.HasIndex("DecidedBy") + .HasDatabaseName("ix_platform_approval_requests_decided_by"); + + b.HasIndex("RequestNo") + .IsUnique() + .HasDatabaseName("ix_platform_approval_requests_request_no"); + + b.HasIndex("RequestedBy", "CommandType", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_approval_requests_requested_by_command_type_idempo~"); + + b.HasIndex("Status", "ExpiresAt", "CreatedAt") + .HasDatabaseName("ix_platform_approval_requests_status_expires_at_created_at"); + + b.ToTable("platform_approval_requests", (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.PlatformConfigurationDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowRuntimeManagement") + .HasColumnType("boolean") + .HasColumnName("allow_runtime_management"); + + 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("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("IsSensitive") + .HasColumnType("boolean") + .HasColumnName("is_sensitive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("ValidationSchema") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("validation_schema") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ValueType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("value_type"); + + b.HasKey("Id") + .HasName("pk_platform_configuration_definitions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_configuration_definitions_code"); + + b.HasIndex("Category", "Code") + .HasDatabaseName("ix_platform_configuration_definitions_category_code"); + + b.ToTable("platform_configuration_definitions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformConfigurationVersion", 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("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id"); + + b.Property("Environment") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)") + .HasColumnName("environment"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("PublishedBy") + .HasColumnType("uuid") + .HasColumnName("published_by"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("RolledBackFromVersionId") + .HasColumnType("uuid") + .HasColumnName("rolled_back_from_version_id"); + + 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.Property("Value") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("value") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_platform_configuration_versions"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_platform_configuration_versions_created_by"); + + b.HasIndex("PublishedBy") + .HasDatabaseName("ix_platform_configuration_versions_published_by"); + + b.HasIndex("RolledBackFromVersionId") + .HasDatabaseName("ix_platform_configuration_versions_rolled_back_from_version_id"); + + b.HasIndex("DefinitionId", "Environment", "Status") + .HasDatabaseName("ix_platform_configuration_versions_definition_id_environment_s~"); + + b.HasIndex("DefinitionId", "Environment", "Version") + .IsUnique() + .HasDatabaseName("ix_platform_configuration_versions_definition_id_environment_v~"); + + b.ToTable("platform_configuration_versions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformNotificationDelivery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("body"); + + 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("IdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("LastError") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("last_error"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RecipientRoleCode") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("recipient_role_code"); + + b.Property("RecipientUserId") + .HasColumnType("uuid") + .HasColumnName("recipient_user_id"); + + 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("Subject") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("subject"); + + b.Property("TemplateId") + .HasColumnType("uuid") + .HasColumnName("template_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_platform_notification_deliveries"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_platform_notification_deliveries_created_by"); + + b.HasIndex("RecipientUserId") + .HasDatabaseName("ix_platform_notification_deliveries_recipient_user_id"); + + b.HasIndex("Status", "CreatedAt") + .HasDatabaseName("ix_platform_notification_deliveries_status_created_at"); + + b.HasIndex("TemplateId", "RecipientUserId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_platform_notification_deliveries_template_id_recipient_user~"); + + b.ToTable("platform_notification_deliveries", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformNotificationTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BodyTemplate") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("body_template"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("channel"); + + 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("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("SubjectTemplate") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("subject_template"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("variables") + .HasDefaultValueSql("'[]'::jsonb"); + + b.HasKey("Id") + .HasName("pk_platform_notification_templates"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_notification_templates_code"); + + b.ToTable("platform_notification_templates", (string)null); + }); + + 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("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + 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("QuestionType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("choice") + .HasColumnName("question_type"); + + 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("Tags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("tags") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TypeLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label"); + + 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("CellId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("cell-01") + .HasColumnName("cell_id"); + + 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.HasIndex("CellId", "Status") + .HasDatabaseName("ix_tenants_cell_id_status"); + + 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.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.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"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "LearningProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_entitlements_learning_products_tenant_id_learning_product_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.AssessmentPolicy", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_assessment_policies_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_policies_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "AssessmentPolicyId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_assessment_policies_assessment_policy_versions_tenant_id_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentPolicyVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_policy_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicy", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_policy_versions_assessment_policies_tenant_id_as~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentScoreBand", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_score_bands_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_score_bands_assessment_policy_versions_tenant_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegment", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segments_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segments_content_releases_tenant_id_content_releas~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegmentMember", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_audience_segment_members_exam_target_profile_versions_exam_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segment_members_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AudienceSegment", null) + .WithMany() + .HasForeignKey("TenantId", "AudienceSegmentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segment_members_audience_segments_tenant_id_audien~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BlueprintRelease", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_blueprint_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CollectionRelease", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_blueprint_releases_collection_releases_tenant_id_collection~"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_blueprint_releases_content_releases_tenant_id_content_relea~"); + + b.HasOne("Tiku.Domain.Content.ExamPaperSpecificationVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ExamPaperSpecificationVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_blueprint_releases_exam_paper_specification_versions_tenant~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetDimensionRequirement", b => + { + b.HasOne("Tiku.Domain.Content.BusinessTargetPolicyVersion", null) + .WithMany() + .HasForeignKey("BusinessTargetPolicyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_target_dimension_requirements_business_target_poli~"); + + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_business_target_dimension_requirements_target_dimension_def~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicy", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_business_target_policies_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Content.BusinessTargetPolicyVersion", null) + .WithMany() + .HasForeignKey("Id", "CurrentVersionId") + .HasPrincipalKey("BusinessTargetPolicyId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_business_target_policies_business_target_policy_versions_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicyVersion", b => + { + b.HasOne("Tiku.Domain.Content.BusinessTargetPolicy", null) + .WithMany() + .HasForeignKey("BusinessTargetPolicyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_target_policy_versions_business_target_policies_bu~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionRelease", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_collection_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_collection_releases_content_releases_tenant_id_content_rele~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionReleaseQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_collection_release_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CollectionRelease", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_collection_release_questions_collection_releases_tenant_id_~"); + + b.HasOne("Tiku.Domain.Content.ContentReleaseQuestion", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseQuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_collection_release_questions_content_release_questions_tena~"); + }); + + 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.ContentRelease", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_releases_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_releases_curriculum_versions_tenant_id_curriculum_v~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentReleaseQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_release_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_assessment_policy_versions_tenant~"); + + b.HasOne("Tiku.Domain.Content.AudienceSegment", null) + .WithMany() + .HasForeignKey("TenantId", "AudienceSegmentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_audience_segments_tenant_id_audie~"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_release_questions_content_releases_tenant_id_conten~"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacement", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionPlacementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_question_placements_tenant_id_que~"); + + b.HasOne("Tiku.Domain.Content.QuestionRevision", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId", "QuestionRevisionId") + .HasPrincipalKey("TenantId", "QuestionAssetId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_question_revisions_question_asset~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.Curriculum", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_curricula_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curricula_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "CurriculumId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_curricula_curriculum_versions_tenant_id_id_current_version_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNode", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_nodes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_nodes_curriculum_versions_tenant_id_curriculum_v~"); + + b.HasOne("Tiku.Domain.Content.KnowledgeConcept", null) + .WithMany() + .HasForeignKey("TenantId", "KnowledgeConceptId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_curriculum_nodes_knowledge_concepts_tenant_id_knowledge_con~"); + + b.HasOne("Tiku.Domain.Content.CurriculumNode", null) + .WithMany() + .HasForeignKey("TenantId", "ParentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_curriculum_nodes_curriculum_nodes_tenant_id_parent_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNodeClosure", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_node_closure_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.Curriculum", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_versions_curricula_tenant_id_curriculum_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecification", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_paper_specifications_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_paper_specifications_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecificationVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_paper_specification_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ExamPaperSpecification", null) + .WithMany() + .HasForeignKey("TenantId", "SpecificationId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_paper_specification_versions_exam_paper_specifications~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfile", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_target_profiles_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("Id", "CurrentVersionId") + .HasPrincipalKey("ExamTargetProfileId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_exam_target_profiles_exam_target_profile_versions_id_curren~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileValue", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_values_exam_target_profile_versions_exa~"); + + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_values_target_dimension_definitions_tar~"); + + b.HasOne("Tiku.Domain.Content.TargetNode", null) + .WithMany() + .HasForeignKey("TargetNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_values_target_nodes_target_node_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileVersion", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfile", null) + .WithMany() + .HasForeignKey("ExamTargetProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_versions_exam_target_profiles_exam_targ~"); + }); + + 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.KnowledgeConcept", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_knowledge_concepts_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ObjectiveGradingRule", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_objective_grading_rules_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_objective_grading_rules_assessment_policy_versions_tenant_i~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageCellProjection", b => + { + b.HasOne("Tiku.Domain.Content.PlatformContentPackageVersion", null) + .WithMany() + .HasForeignKey("PackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_content_package_cell_projections_platform_content_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_content_package_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_content_package_versions_content_releases_tenant_i~"); + }); + + 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.QuestionAsset", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_assets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionFamily", null) + .WithMany() + .HasForeignKey("TenantId", "FamilyId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_assets_question_families_tenant_id_family_id"); + + b.HasOne("Tiku.Domain.Content.QuestionRevision", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentRevisionId") + .HasPrincipalKey("TenantId", "QuestionAssetId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_assets_question_revisions_tenant_id_id_current_rev~"); + }); + + 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.QuestionFamily", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_families_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacement", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placements_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionAsset", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_question_assets_question_asset_owner_te~"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_assessment_policy_versions_tenant_id_as~"); + + b.HasOne("Tiku.Domain.Content.CurriculumNode", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_curriculum_nodes_tenant_id_curriculum_n~"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_curriculum_versions_tenant_id_curriculu~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementConcept", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_concepts_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.KnowledgeConcept", null) + .WithMany() + .HasForeignKey("TenantId", "KnowledgeConceptId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placement_concepts_knowledge_concepts_tenant_id_kn~"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacement", null) + .WithMany() + .HasForeignKey("TenantId", "PlacementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_concepts_question_placements_tenant_id_p~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleCondition", b => + { + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_target_dimension_definit~"); + + b.HasOne("Tiku.Domain.Content.TargetNode", null) + .WithMany() + .HasForeignKey("TargetNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_target_nodes_target_node~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacementRuleGroup", null) + .WithMany() + .HasForeignKey("TenantId", "RuleGroupId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_question_placement_rule_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleGroup", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_groups_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacement", null) + .WithMany() + .HasForeignKey("TenantId", "PlacementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_groups_question_placements_tenant_i~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionRevision", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_revisions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionAsset", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_revisions_question_assets_tenant_id_question_asset~"); + }); + + 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.ScoreAdjustmentRule", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_score_adjustment_rules_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_score_adjustment_rules_assessment_policy_versions_tenant_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.SubjectiveRubric", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subjective_rubrics_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subjective_rubrics_assessment_policy_versions_tenant_id_ass~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TargetNode", b => + { + b.HasOne("Tiku.Domain.Content.TargetNode", null) + .WithMany() + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_target_nodes_target_nodes_parent_id"); + + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_target_nodes_target_dimension_definitions_target_dimension_~"); + }); + + 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.ClassAssignmentGrant", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_assignment_grants_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Tenancy.TenantClass", null) + .WithMany() + .HasForeignKey("TenantId", "ClassId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_assignment_grants_tenant_classes_tenant_id_class_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_class_assignment_grants_product_access_manifest_versions_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ClassContentAssignment", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_class_content_assignments_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RevokedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_class_content_assignments_users_revoked_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_content_assignments_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ContentSlice", null) + .WithMany() + .HasForeignKey("ContentSliceOwnerTenantId", "ContentSliceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_class_content_assignments_content_slices_content_slice_owne~"); + + b.HasOne("Tiku.Domain.Tenancy.TenantClass", null) + .WithMany() + .HasForeignKey("TenantId", "ClassId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_class_content_assignments_tenant_classes_tenant_id_class_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ContentSlice", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_slices_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Learning.MarketRegion", null) + .WithMany() + .HasForeignKey("MarketRegionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_content_slices_market_regions_market_region_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_slices_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.CurrentAnswer", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_current_answers_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.AnswerRecord", null) + .WithMany() + .HasForeignKey("TenantId", "AnswerRecordId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_current_answers_answer_records_tenant_id_answer_record_id"); + + b.HasOne("Tiku.Domain.Learning.PracticeSessionQuestion", null) + .WithMany() + .HasForeignKey("TenantId", "PracticeSessionId", "SessionQuestionId") + .HasPrincipalKey("TenantId", "PracticeSessionId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_current_answers_practice_session_questions_tenant_id_practi~"); + }); + + 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.EffectiveAccessProjection", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_effective_access_projections_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_effective_access_projections_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_effective_access_projections_users_user_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.LearningAccessVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_access_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_access_versions_users_user_id"); + }); + + 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.LearningOutboxMessage", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_outbox_messages_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProduct", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_learning_products_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_products_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProductScope", b => + { + b.HasOne("Tiku.Domain.Learning.ContentSlice", null) + .WithMany() + .HasForeignKey("ContentSliceOwnerTenantId", "ContentSliceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_learning_product_scopes_content_slices_content_slice_owner_~"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "ProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_product_scopes_learning_products_tenant_id_product~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.LearningProjectionReceipt", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_learning_projection_receipts_tenants_tenant_id"); + }); + + 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) + .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) + .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.ProductAccessManifest", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifests_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "LearningProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifests_learning_products_tenant_id_learni~"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "ProductAccessManifestId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_product_access_manifests_product_access_manifest_versions_t~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductAccessManifestVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifest_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifest", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifest_versions_product_access_manifests_t~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestRelease", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("ContentOwnerTenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_product_manifest_releases_content_releases_content_owner_te~"); + + b.HasOne("Tiku.Domain.Content.PlatformContentPackageVersion", null) + .WithMany() + .HasForeignKey("PlatformContentPackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_product_manifest_releases_platform_content_package_versions~"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_releases_product_access_manifest_versions_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestResource", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_resources_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_resources_product_access_manifest_versions~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestTarget", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_product_manifest_targets_exam_target_profile_versions_exam_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_targets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_targets_product_access_manifest_versions_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.StudentEntitlement", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_users_user_id"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "LearningProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_learning_products_tenant_id_learnin~"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_product_access_manifest_versions_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetRegionHistory", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_business_lines_business_line_~"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ChangedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_student_target_region_history_users_changed_by"); + + b.HasOne("Tiku.Domain.Learning.MarketRegion", null) + .WithMany() + .HasForeignKey("MarketRegionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_market_regions_market_region_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_region_history_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetSelectionHistory", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_business_lines_business_li~"); + + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_exam_target_profile_versio~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_users_user_id"); + + b.HasOne("Tiku.Domain.Learning.StudentEntitlement", null) + .WithMany() + .HasForeignKey("TenantId", "EntitlementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_student_target_selection_history_student_entitlements_v2_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantBusinessLicense", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_business_licenses_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_business_licenses_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicense", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_learning_licenses_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_learning_licenses_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicenseRegion", b => + { + b.HasOne("Tiku.Domain.Learning.MarketRegion", null) + .WithMany() + .HasForeignKey("MarketRegionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_learning_license_regions_market_regions_market_regio~"); + + b.HasOne("Tiku.Domain.Learning.TenantLearningLicense", null) + .WithMany() + .HasForeignKey("TenantId", "LicenseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_learning_license_regions_tenant_learning_licenses_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantLicensedTarget", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_licensed_targets_exam_target_profile_versions_exam_t~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_licensed_targets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.TenantBusinessLicense", null) + .WithMany() + .HasForeignKey("TenantId", "TenantBusinessLicenseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_licensed_targets_tenant_business_licenses_tenant_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.Learning.WrongQuestionV2", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_users_user_id"); + + b.HasOne("Tiku.Domain.Content.QuestionAsset", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_question_assets_question_asset_owner_ten~"); + + b.HasOne("Tiku.Domain.Content.QuestionRevision", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId", "LastQuestionRevisionId") + .HasPrincipalKey("TenantId", "QuestionAssetId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_question_revisions_question_asset_owner_~"); + }); + + 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.PlatformApprovalRequest", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("DecidedBy") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_approval_requests_users_decided_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RequestedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_approval_requests_users_requested_by"); + }); + + 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.PlatformConfigurationVersion", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_configuration_versions_users_created_by"); + + b.HasOne("Tiku.Domain.Platform.PlatformConfigurationDefinition", null) + .WithMany() + .HasForeignKey("DefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_configuration_versions_platform_configuration_defi~"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("PublishedBy") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_configuration_versions_users_published_by"); + + b.HasOne("Tiku.Domain.Platform.PlatformConfigurationVersion", null) + .WithMany() + .HasForeignKey("RolledBackFromVersionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_platform_configuration_versions_platform_configuration_vers~"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformNotificationDelivery", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_notification_deliveries_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RecipientUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_notification_deliveries_users_recipient_user_id"); + + b.HasOne("Tiku.Domain.Platform.PlatformNotificationTemplate", null) + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_notification_deliveries_platform_notification_temp~"); + }); + + 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/20260806014558_ContentV2WorkingSync.cs b/Tiku.Infrastructure/Persistence/Migrations/20260806014558_ContentV2WorkingSync.cs new file mode 100644 index 0000000..cba409d --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260806014558_ContentV2WorkingSync.cs @@ -0,0 +1,116 @@ +using System; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Tiku.Infrastructure.Persistence.Migrations +{ + /// + public partial class ContentV2WorkingSync : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "question_taxonomy_assignments"); + + migrationBuilder.DropTable( + name: "taxonomy_nodes"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "taxonomy_nodes", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + code = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + depth = table.Column(type: "integer", nullable: false), + is_active = table.Column(type: "boolean", nullable: false), + metadata = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + node_type = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + parent_id = table.Column(type: "uuid", nullable: true), + parent_owner_tenant_id = table.Column(type: "uuid", nullable: true), + path = table.Column(type: "ltree", nullable: true), + sort_order = table.Column(type: "integer", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_taxonomy_nodes", x => x.id); + table.UniqueConstraint("ak_taxonomy_nodes_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_taxonomy_nodes_parent_pair", "(parent_owner_tenant_id is null) = (parent_id is null)"); + table.ForeignKey( + name: "fk_taxonomy_nodes_taxonomy_nodes_parent_owner_tenant_id_parent~", + columns: x => new { x.parent_owner_tenant_id, x.parent_id }, + principalTable: "taxonomy_nodes", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_taxonomy_nodes_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "question_taxonomy_assignments", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + is_primary = table.Column(type: "boolean", nullable: false), + question_id = table.Column(type: "uuid", nullable: false), + taxonomy_node_id = table.Column(type: "uuid", nullable: false), + taxonomy_owner_tenant_id = table.Column(type: "uuid", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_question_taxonomy_assignments", x => x.id); + table.UniqueConstraint("ak_question_taxonomy_assignments_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_question_taxonomy_assignments_questions_tenant_id_question_~", + columns: x => new { x.tenant_id, x.question_id }, + principalTable: "questions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_question_taxonomy_assignments_taxonomy_nodes_taxonomy_owner~", + columns: x => new { x.taxonomy_owner_tenant_id, x.taxonomy_node_id }, + principalTable: "taxonomy_nodes", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "ix_question_taxonomy_assignments_taxonomy_owner_tenant_id_taxo~", + table: "question_taxonomy_assignments", + columns: new[] { "taxonomy_owner_tenant_id", "taxonomy_node_id" }); + + migrationBuilder.CreateIndex( + name: "ix_question_taxonomy_assignments_tenant_id_question_id_taxonom~", + table: "question_taxonomy_assignments", + columns: new[] { "tenant_id", "question_id", "taxonomy_owner_tenant_id", "taxonomy_node_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_taxonomy_nodes_parent_owner_tenant_id_parent_id", + table: "taxonomy_nodes", + columns: new[] { "parent_owner_tenant_id", "parent_id" }); + + migrationBuilder.CreateIndex( + name: "ix_taxonomy_nodes_tenant_id_code", + table: "taxonomy_nodes", + columns: new[] { "tenant_id", "code" }, + unique: true); + } + } +} diff --git a/Tiku.Infrastructure/Persistence/Migrations/MigrationBuilderDatabaseObjectExtensions.cs b/Tiku.Infrastructure/Persistence/Migrations/MigrationBuilderDatabaseObjectExtensions.cs index 26dfea7..b4f96a1 100644 --- a/Tiku.Infrastructure/Persistence/Migrations/MigrationBuilderDatabaseObjectExtensions.cs +++ b/Tiku.Infrastructure/Persistence/Migrations/MigrationBuilderDatabaseObjectExtensions.cs @@ -155,4 +155,151 @@ internal static class MigrationBuilderDatabaseObjectExtensions DROP INDEX IF EXISTS ix_scoreline_records_tenant_id_year_school_name_major_name_id; """); } + + public static void EnsureContentV2ImmutabilityGuards(this MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + CREATE OR REPLACE FUNCTION tiku_guard_question_revision_immutable() + RETURNS trigger + LANGUAGE plpgsql + AS $$ + BEGIN + RAISE EXCEPTION 'question revisions are immutable'; + END; + $$; + + DROP TRIGGER IF EXISTS trg_question_revisions_immutable ON question_revisions; + CREATE TRIGGER trg_question_revisions_immutable + BEFORE UPDATE OR DELETE ON question_revisions + FOR EACH ROW EXECUTE FUNCTION tiku_guard_question_revision_immutable(); + + CREATE OR REPLACE FUNCTION tiku_guard_published_manifest_version_immutable() + RETURNS trigger + LANGUAGE plpgsql + AS $$ + BEGIN + IF OLD.status = 'published' THEN + RAISE EXCEPTION 'published product access manifest versions are immutable'; + END IF; + IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; + RETURN NEW; + END; + $$; + + DROP TRIGGER IF EXISTS trg_product_manifest_versions_published_immutable + ON product_access_manifest_versions; + CREATE TRIGGER trg_product_manifest_versions_published_immutable + BEFORE UPDATE OR DELETE ON product_access_manifest_versions + FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_manifest_version_immutable(); + + CREATE OR REPLACE FUNCTION tiku_guard_published_manifest_child_immutable() + RETURNS trigger + LANGUAGE plpgsql + AS $$ + DECLARE + parent_tenant_id uuid; + parent_version_id uuid; + parent_status text; + BEGIN + IF TG_OP = 'DELETE' THEN + parent_tenant_id := OLD.tenant_id; + parent_version_id := OLD.product_access_manifest_version_id; + ELSE + parent_tenant_id := NEW.tenant_id; + parent_version_id := NEW.product_access_manifest_version_id; + END IF; + SELECT status INTO parent_status + FROM product_access_manifest_versions + WHERE tenant_id = parent_tenant_id AND id = parent_version_id; + IF parent_status = 'published' THEN + RAISE EXCEPTION 'children of a published product access manifest version are immutable'; + END IF; + IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; + RETURN NEW; + END; + $$; + + DROP TRIGGER IF EXISTS trg_product_manifest_releases_published_immutable ON product_manifest_releases; + DROP TRIGGER IF EXISTS trg_product_manifest_targets_published_immutable ON product_manifest_targets; + DROP TRIGGER IF EXISTS trg_product_manifest_resources_published_immutable ON product_manifest_resources; + CREATE TRIGGER trg_product_manifest_releases_published_immutable + BEFORE INSERT OR UPDATE OR DELETE ON product_manifest_releases + FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_manifest_child_immutable(); + CREATE TRIGGER trg_product_manifest_targets_published_immutable + BEFORE INSERT OR UPDATE OR DELETE ON product_manifest_targets + FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_manifest_child_immutable(); + CREATE TRIGGER trg_product_manifest_resources_published_immutable + BEFORE INSERT OR UPDATE OR DELETE ON product_manifest_resources + FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_manifest_child_immutable(); + + CREATE OR REPLACE FUNCTION tiku_guard_published_content_release_immutable() + RETURNS trigger + LANGUAGE plpgsql + AS $$ + BEGIN + IF OLD.status = 'published' THEN + RAISE EXCEPTION 'published content releases are immutable'; + END IF; + IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; + RETURN NEW; + END; + $$; + + DROP TRIGGER IF EXISTS trg_content_releases_published_immutable ON content_releases; + CREATE TRIGGER trg_content_releases_published_immutable + BEFORE UPDATE OR DELETE ON content_releases + FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_content_release_immutable(); + + CREATE OR REPLACE FUNCTION tiku_guard_published_content_release_child_immutable() + RETURNS trigger + LANGUAGE plpgsql + AS $$ + DECLARE + parent_tenant_id uuid; + parent_release_id uuid; + parent_status text; + BEGIN + IF TG_OP = 'DELETE' THEN + parent_tenant_id := OLD.tenant_id; + parent_release_id := OLD.content_release_id; + ELSE + parent_tenant_id := NEW.tenant_id; + parent_release_id := NEW.content_release_id; + END IF; + SELECT status INTO parent_status + FROM content_releases + WHERE tenant_id = parent_tenant_id AND id = parent_release_id; + IF parent_status = 'published' THEN + RAISE EXCEPTION 'children of a published content release are immutable'; + END IF; + IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; + RETURN NEW; + END; + $$; + + DROP TRIGGER IF EXISTS trg_audience_segments_release_immutable ON audience_segments; + DROP TRIGGER IF EXISTS trg_audience_segment_members_release_immutable ON audience_segment_members; + DROP TRIGGER IF EXISTS trg_content_release_questions_release_immutable ON content_release_questions; + CREATE TRIGGER trg_audience_segments_release_immutable + BEFORE INSERT OR UPDATE OR DELETE ON audience_segments + FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_content_release_child_immutable(); + CREATE TRIGGER trg_audience_segment_members_release_immutable + BEFORE INSERT OR UPDATE OR DELETE ON audience_segment_members + FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_content_release_child_immutable(); + CREATE TRIGGER trg_content_release_questions_release_immutable + BEFORE INSERT OR UPDATE OR DELETE ON content_release_questions + FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_content_release_child_immutable(); + """); + } + + public static void DropContentV2ImmutabilityGuards(this MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + DROP FUNCTION IF EXISTS tiku_guard_published_content_release_child_immutable() CASCADE; + DROP FUNCTION IF EXISTS tiku_guard_published_content_release_immutable() CASCADE; + DROP FUNCTION IF EXISTS tiku_guard_published_manifest_child_immutable() CASCADE; + DROP FUNCTION IF EXISTS tiku_guard_published_manifest_version_immutable() CASCADE; + DROP FUNCTION IF EXISTS tiku_guard_question_revision_immutable() CASCADE; + """); + } } diff --git a/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs b/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs index fb8704b..5a31d8d 100644 --- a/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs +++ b/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs @@ -391,56 +391,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations 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") @@ -1020,97 +970,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations 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") @@ -3895,6 +3754,664 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.ToTable("app_assets", (string)null); }); + modelBuilder.Entity("Tiku.Domain.Content.AssessmentPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_assessment_policies"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_assessment_policies_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_assessment_policies_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_assessment_policies_tenant_id_code"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_assessment_policies_tenant_id_id_current_version_id"); + + b.ToTable("assessment_policies", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentPolicyVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_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("DefaultScore") + .HasColumnType("numeric") + .HasColumnName("default_score"); + + b.Property("GradingMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("grading_mode"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("RoundingScale") + .HasColumnType("integer") + .HasColumnName("rounding_scale"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_assessment_policy_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_assessment_policy_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "AssessmentPolicyId", "Id") + .HasName("ak_assessment_policy_versions_tenant_id_assessment_policy_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_assessment_policy_versions_tenant_id_assessment_policy_id_v~"); + + b.ToTable("assessment_policy_versions", null, t => + { + t.HasCheckConstraint("ck_assessment_policy_default_score", "default_score > 0"); + + t.HasCheckConstraint("ck_assessment_policy_rounding", "rounding_scale between 0 and 6"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentScoreBand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("label"); + + b.Property("MaxScore") + .HasColumnType("numeric") + .HasColumnName("max_score"); + + b.Property("MinScore") + .HasColumnType("numeric") + .HasColumnName("min_score"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_assessment_score_bands"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_assessment_score_bands_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "MinScore", "MaxScore") + .IsUnique() + .HasDatabaseName("ix_assessment_score_bands_tenant_id_assessment_policy_version_~"); + + b.ToTable("assessment_score_bands", null, t => + { + t.HasCheckConstraint("ck_assessment_score_band", "max_score >= min_score"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("RuleHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("rule_hash"); + + b.Property("SegmentOrdinal") + .HasColumnType("integer") + .HasColumnName("segment_ordinal"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_audience_segments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_audience_segments_tenant_id_id"); + + b.HasIndex("TenantId", "ContentReleaseId", "RuleHash") + .IsUnique() + .HasDatabaseName("ix_audience_segments_tenant_id_content_release_id_rule_hash"); + + b.HasIndex("TenantId", "ContentReleaseId", "SegmentOrdinal") + .IsUnique() + .HasDatabaseName("ix_audience_segments_tenant_id_content_release_id_segment_ordi~"); + + b.ToTable("audience_segments", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegmentMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AudienceSegmentId") + .HasColumnType("uuid") + .HasColumnName("audience_segment_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_audience_segment_members"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_audience_segment_members_tenant_id_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_audience_segment_members_exam_target_profile_version_id"); + + b.HasIndex("TenantId", "AudienceSegmentId") + .HasDatabaseName("ix_audience_segment_members_tenant_id_audience_segment_id"); + + b.HasIndex("TenantId", "ContentReleaseId", "AudienceSegmentId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_audience_segment_members_tenant_id_content_release_id_audie~"); + + b.HasIndex("TenantId", "ContentReleaseId", "ExamTargetProfileVersionId", "AudienceSegmentId") + .HasDatabaseName("ix_audience_segment_members_tenant_id_content_release_id_exam_~"); + + b.ToTable("audience_segment_members", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BlueprintRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssemblyRules") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("assembly_rules") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CollectionReleaseId") + .HasColumnType("uuid") + .HasColumnName("collection_release_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ExamPaperSpecificationVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_paper_specification_version_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("SourceBlueprintId") + .HasColumnType("uuid") + .HasColumnName("source_blueprint_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.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_blueprint_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_blueprint_releases_tenant_id_id"); + + b.HasIndex("TenantId", "CollectionReleaseId") + .HasDatabaseName("ix_blueprint_releases_tenant_id_collection_release_id"); + + b.HasIndex("TenantId", "ContentReleaseId") + .HasDatabaseName("ix_blueprint_releases_tenant_id_content_release_id"); + + b.HasIndex("TenantId", "ExamPaperSpecificationVersionId") + .HasDatabaseName("ix_blueprint_releases_tenant_id_exam_paper_specification_versi~"); + + b.HasIndex("TenantId", "SourceBlueprintId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_blueprint_releases_tenant_id_source_blueprint_id_version_no"); + + b.ToTable("blueprint_releases", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetDimensionRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessTargetPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("business_target_policy_version_id"); + + b.Property("MaxValues") + .HasColumnType("integer") + .HasColumnName("max_values"); + + b.Property("MinValues") + .HasColumnType("integer") + .HasColumnName("min_values"); + + b.Property("RequiredForProfile") + .HasColumnType("boolean") + .HasColumnName("required_for_profile"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.HasKey("Id") + .HasName("pk_business_target_dimension_requirements"); + + b.HasIndex("TargetDimensionDefinitionId") + .HasDatabaseName("ix_business_target_dimension_requirements_target_dimension_def~"); + + b.HasIndex("BusinessTargetPolicyVersionId", "TargetDimensionDefinitionId") + .IsUnique() + .HasDatabaseName("ix_business_target_dimension_requirements_business_target_poli~"); + + b.ToTable("business_target_dimension_requirements", null, t => + { + t.HasCheckConstraint("ck_business_target_dimension_cardinality", "min_values >= 0 and max_values >= min_values"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_business_target_policies"); + + b.HasIndex("BusinessLineId") + .IsUnique() + .HasDatabaseName("ix_business_target_policies_business_line_id"); + + b.HasIndex("Id", "CurrentVersionId") + .HasDatabaseName("ix_business_target_policies_id_current_version_id"); + + b.ToTable("business_target_policies", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicyVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessTargetPolicyId") + .HasColumnType("uuid") + .HasColumnName("business_target_policy_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IncludesBaseContent") + .HasColumnType("boolean") + .HasColumnName("includes_base_content"); + + b.Property("MaxActiveTargets") + .HasColumnType("integer") + .HasColumnName("max_active_targets"); + + b.Property("MaxAlternateTargets") + .HasColumnType("integer") + .HasColumnName("max_alternate_targets"); + + b.Property("MaxPrimaryTargets") + .HasColumnType("integer") + .HasColumnName("max_primary_targets"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("SelectionMode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("selection_mode"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TargetChangeCooldownDays") + .HasColumnType("integer") + .HasColumnName("target_change_cooldown_days"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_business_target_policy_versions"); + + b.HasAlternateKey("BusinessTargetPolicyId", "Id") + .HasName("ak_business_target_policy_versions_business_target_policy_id_id"); + + b.HasIndex("BusinessTargetPolicyId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_business_target_policy_versions_business_target_policy_id_v~"); + + b.ToTable("business_target_policy_versions", null, t => + { + t.HasCheckConstraint("ck_business_target_policy_active", "max_active_targets between 1 and 16"); + + t.HasCheckConstraint("ck_business_target_policy_alternate", "max_alternate_targets between 0 and max_active_targets"); + + t.HasCheckConstraint("ck_business_target_policy_cooldown", "target_change_cooldown_days between 0 and 365"); + + t.HasCheckConstraint("ck_business_target_policy_primary", "max_primary_targets between 0 and max_active_targets"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("SourceCollectionId") + .HasColumnType("uuid") + .HasColumnName("source_collection_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.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_collection_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_collection_releases_tenant_id_id"); + + b.HasIndex("TenantId", "ContentReleaseId") + .HasDatabaseName("ix_collection_releases_tenant_id_content_release_id"); + + b.HasIndex("TenantId", "SourceCollectionId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_collection_releases_tenant_id_source_collection_id_version_~"); + + b.ToTable("collection_releases", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionReleaseQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CollectionReleaseId") + .HasColumnType("uuid") + .HasColumnName("collection_release_id"); + + b.Property("ContentReleaseQuestionId") + .HasColumnType("uuid") + .HasColumnName("content_release_question_id"); + + b.Property("Required") + .HasColumnType("boolean") + .HasColumnName("required"); + + b.Property("Score") + .HasColumnType("numeric") + .HasColumnName("score"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_collection_release_questions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_collection_release_questions_tenant_id_id"); + + b.HasIndex("TenantId", "ContentReleaseQuestionId") + .HasDatabaseName("ix_collection_release_questions_tenant_id_content_release_ques~"); + + b.HasIndex("TenantId", "CollectionReleaseId", "ContentReleaseQuestionId") + .IsUnique() + .HasDatabaseName("ux_collection_release_question_item"); + + b.HasIndex("TenantId", "CollectionReleaseId", "SortOrder") + .IsUnique() + .HasDatabaseName("ux_collection_release_question_order"); + + b.ToTable("collection_release_questions", (string)null); + }); + modelBuilder.Entity("Tiku.Domain.Content.ContentAsset", b => { b.Property("Id") @@ -5007,6 +5524,733 @@ namespace Tiku.Infrastructure.Persistence.Migrations }); }); + modelBuilder.Entity("Tiku.Domain.Content.ContentRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("CompilerVersion") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("compiler_version"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("ReleaseNo") + .HasColumnType("integer") + .HasColumnName("release_no"); + + b.Property("SourceFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("source_fingerprint"); + + 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_content_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_releases_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_content_releases_business_line_id"); + + b.HasIndex("TenantId", "CurriculumVersionId", "ReleaseNo") + .IsUnique() + .HasDatabaseName("ix_content_releases_tenant_id_curriculum_version_id_release_no"); + + b.HasIndex("TenantId", "BusinessLineId", "Status", "PublishedAt") + .HasDatabaseName("ix_content_releases_tenant_id_business_line_id_status_publishe~"); + + b.ToTable("content_releases", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentReleaseQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("AudienceSegmentId") + .HasColumnType("uuid") + .HasColumnName("audience_segment_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CurriculumNodeId") + .HasColumnType("uuid") + .HasColumnName("curriculum_node_id"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("Ordinal") + .HasColumnType("integer") + .HasColumnName("ordinal"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionAssetOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_asset_owner_tenant_id"); + + b.Property("QuestionPlacementId") + .HasColumnType("uuid") + .HasColumnName("question_placement_id"); + + b.Property("QuestionRevisionId") + .HasColumnType("uuid") + .HasColumnName("question_revision_id"); + + b.Property("QuestionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("question_type"); + + 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_content_release_questions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_content_release_questions_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId") + .HasDatabaseName("ix_content_release_questions_tenant_id_assessment_policy_versi~"); + + b.HasIndex("TenantId", "AudienceSegmentId") + .HasDatabaseName("ix_content_release_questions_tenant_id_audience_segment_id"); + + b.HasIndex("TenantId", "QuestionPlacementId") + .HasDatabaseName("ix_content_release_questions_tenant_id_question_placement_id"); + + b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId", "QuestionRevisionId") + .HasDatabaseName("ix_content_release_questions_question_asset_owner_tenant_id_qu~"); + + b.HasIndex("TenantId", "ContentReleaseId", "AudienceSegmentId", "CurriculumNodeId", "Ordinal") + .IsUnique() + .HasDatabaseName("ix_content_release_questions_tenant_id_content_release_id_audi~"); + + b.HasIndex("TenantId", "ContentReleaseId", "AudienceSegmentId", "CurriculumNodeId", "Status", "Difficulty", "QuestionType", "Ordinal") + .HasDatabaseName("ix_content_release_question_candidates"); + + b.ToTable("content_release_questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.Curriculum", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_curricula"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curricula_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_curricula_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_curricula_tenant_id_code"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_curricula_tenant_id_id_current_version_id"); + + b.ToTable("curricula", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNode", 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("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("kind"); + + b.Property("KnowledgeConceptId") + .HasColumnType("uuid") + .HasColumnName("knowledge_concept_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_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_curriculum_nodes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curriculum_nodes_tenant_id_id"); + + b.HasIndex("TenantId", "KnowledgeConceptId") + .HasDatabaseName("ix_curriculum_nodes_tenant_id_knowledge_concept_id"); + + b.HasIndex("TenantId", "ParentId") + .HasDatabaseName("ix_curriculum_nodes_tenant_id_parent_id"); + + b.HasIndex("TenantId", "CurriculumVersionId", "Code") + .IsUnique() + .HasDatabaseName("ix_curriculum_nodes_tenant_id_curriculum_version_id_code"); + + b.HasIndex("TenantId", "CurriculumVersionId", "ParentId", "SortOrder") + .HasDatabaseName("ix_curriculum_nodes_tenant_id_curriculum_version_id_parent_id_~"); + + b.ToTable("curriculum_nodes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNodeClosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AncestorNodeId") + .HasColumnType("uuid") + .HasColumnName("ancestor_node_id"); + + b.Property("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("Depth") + .HasColumnType("integer") + .HasColumnName("depth"); + + b.Property("DescendantNodeId") + .HasColumnType("uuid") + .HasColumnName("descendant_node_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_curriculum_node_closure"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curriculum_node_closure_tenant_id_id"); + + b.HasIndex("TenantId", "CurriculumVersionId", "AncestorNodeId", "DescendantNodeId") + .IsUnique() + .HasDatabaseName("ix_curriculum_node_closure_tenant_id_curriculum_version_id_anc~"); + + b.HasIndex("TenantId", "CurriculumVersionId", "DescendantNodeId", "Depth") + .HasDatabaseName("ix_curriculum_node_closure_tenant_id_curriculum_version_id_des~"); + + b.ToTable("curriculum_node_closure", null, t => + { + t.HasCheckConstraint("ck_curriculum_node_closure_depth", "depth >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumVersion", 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("CurriculumId") + .HasColumnType("uuid") + .HasColumnName("curriculum_id"); + + b.Property("ExamYear") + .HasColumnType("integer") + .HasColumnName("exam_year"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_curriculum_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_curriculum_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "CurriculumId", "Id") + .HasName("ak_curriculum_versions_tenant_id_curriculum_id_id"); + + b.HasIndex("TenantId", "CurriculumId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_curriculum_versions_tenant_id_curriculum_id_version_no"); + + b.ToTable("curriculum_versions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_paper_specifications"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_exam_paper_specifications_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_exam_paper_specifications_business_line_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_exam_paper_specifications_tenant_id_code"); + + b.ToTable("exam_paper_specifications", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecificationVersion", 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("DurationMinutes") + .HasColumnType("integer") + .HasColumnName("duration_minutes"); + + b.Property("ExamYear") + .HasColumnType("integer") + .HasColumnName("exam_year"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Sections") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sections") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("SpecificationId") + .HasColumnType("uuid") + .HasColumnName("specification_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalScore") + .HasColumnType("numeric") + .HasColumnName("total_score"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_exam_paper_specification_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_exam_paper_specification_versions_tenant_id_id"); + + b.HasIndex("TenantId", "SpecificationId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_exam_paper_specification_versions_tenant_id_specification_i~"); + + b.ToTable("exam_paper_specification_versions", null, t => + { + t.HasCheckConstraint("ck_exam_paper_spec_duration", "duration_minutes > 0"); + + t.HasCheckConstraint("ck_exam_paper_spec_total_score", "total_score > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("code"); + + 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_exam_target_profiles"); + + b.HasIndex("BusinessLineId", "Code") + .IsUnique() + .HasDatabaseName("ix_exam_target_profiles_business_line_id_code"); + + b.HasIndex("Id", "CurrentVersionId") + .HasDatabaseName("ix_exam_target_profiles_id_current_version_id"); + + b.ToTable("exam_target_profiles", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.Property("TargetNodeId") + .HasColumnType("uuid") + .HasColumnName("target_node_id"); + + b.HasKey("Id") + .HasName("pk_exam_target_profile_values"); + + b.HasIndex("TargetDimensionDefinitionId") + .HasDatabaseName("ix_exam_target_profile_values_target_dimension_definition_id"); + + b.HasIndex("TargetNodeId") + .HasDatabaseName("ix_exam_target_profile_values_target_node_id"); + + b.HasIndex("ExamTargetProfileVersionId", "TargetDimensionDefinitionId", "TargetNodeId") + .IsUnique() + .HasDatabaseName("ix_exam_target_profile_values_exam_target_profile_version_id_t~"); + + b.ToTable("exam_target_profile_values", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileVersion", 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(500) + .HasColumnType("character varying(500)") + .HasColumnName("display_name"); + + b.Property("ExamTargetProfileId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_id"); + + b.Property("ExamYear") + .HasColumnType("integer") + .HasColumnName("exam_year"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_exam_target_profile_versions"); + + b.HasAlternateKey("ExamTargetProfileId", "Id") + .HasName("ak_exam_target_profile_versions_exam_target_profile_id_id"); + + b.HasIndex("ExamTargetProfileId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_exam_target_profile_versions_exam_target_profile_id_version~"); + + b.ToTable("exam_target_profile_versions", (string)null); + }); + modelBuilder.Entity("Tiku.Domain.Content.HandbookChapter", b => { b.Property("Id") @@ -5403,6 +6647,252 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.ToTable("images", (string)null); }); + modelBuilder.Entity("Tiku.Domain.Content.KnowledgeConcept", 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(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_knowledge_concepts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_knowledge_concepts_tenant_id_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_knowledge_concepts_tenant_id_code"); + + b.ToTable("knowledge_concepts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ObjectiveGradingRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Configuration") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("configuration") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RuleType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("rule_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_objective_grading_rules"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_objective_grading_rules_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "RuleType") + .IsUnique() + .HasDatabaseName("ix_objective_grading_rules_tenant_id_assessment_policy_version~"); + + b.ToTable("objective_grading_rules", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageCellProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CellId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("cell_id"); + + b.Property("ContentHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("content_hash"); + + b.Property("EventSequence") + .HasColumnType("bigint") + .HasColumnName("event_sequence"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("PackageCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("package_code"); + + b.Property("PackageOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("package_owner_tenant_id"); + + b.Property("PlatformContentPackageVersionId") + .HasColumnType("uuid") + .HasColumnName("platform_content_package_version_id"); + + b.Property("ProjectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("projected_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_platform_content_package_cell_projections"); + + b.HasIndex("PackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasDatabaseName("ix_platform_package_cell_owner_version"); + + b.HasIndex("PlatformContentPackageVersionId", "CellId") + .HasDatabaseName("ix_platform_package_cell_version"); + + b.HasIndex("PackageOwnerTenantId", "PackageCode", "CellId") + .IsUnique() + .HasDatabaseName("ux_platform_package_cell_cursor"); + + b.ToTable("platform_content_package_cell_projections", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ContentHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("content_hash"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("PackageCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("package_code"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_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.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_platform_content_package_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_content_package_versions_tenant_id_id"); + + b.HasIndex("TenantId", "ContentHash") + .IsUnique() + .HasDatabaseName("ix_platform_content_package_versions_tenant_id_content_hash"); + + b.HasIndex("TenantId", "ContentReleaseId") + .HasDatabaseName("ix_platform_content_package_versions_tenant_id_content_release~"); + + b.HasIndex("TenantId", "PackageCode", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_platform_content_package_versions_tenant_id_package_code_ve~"); + + b.ToTable("platform_content_package_versions", (string)null); + }); + modelBuilder.Entity("Tiku.Domain.Content.PracticeBlueprint", b => { b.Property("Id") @@ -5556,6 +7046,77 @@ namespace Tiku.Infrastructure.Persistence.Migrations }); }); + modelBuilder.Entity("Tiku.Domain.Content.QuestionAsset", 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("CurrentRevisionId") + .HasColumnType("uuid") + .HasColumnName("current_revision_id"); + + b.Property("DeliveryFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("delivery_fingerprint"); + + b.Property("FamilyId") + .HasColumnType("uuid") + .HasColumnName("family_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.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + b.HasKey("Id") + .HasName("pk_question_assets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_assets_tenant_id_id"); + + b.HasIndex("TenantId", "DeliveryFingerprint") + .IsUnique() + .HasDatabaseName("ix_question_assets_tenant_id_delivery_fingerprint"); + + b.HasIndex("TenantId", "FamilyId") + .HasDatabaseName("ix_question_assets_tenant_id_family_id"); + + b.HasIndex("TenantId", "Id", "CurrentRevisionId") + .HasDatabaseName("ix_question_assets_tenant_id_id_current_revision_id"); + + b.ToTable("question_assets", (string)null); + }); + modelBuilder.Entity("Tiku.Domain.Content.QuestionCollection", b => { b.Property("Id") @@ -5804,6 +7365,394 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.ToTable("question_collection_items", (string)null); }); + modelBuilder.Entity("Tiku.Domain.Content.QuestionFamily", 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("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + 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_families"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_families_tenant_id_id"); + + b.ToTable("question_families", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_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("CurriculumNodeId") + .HasColumnType("uuid") + .HasColumnName("curriculum_node_id"); + + b.Property("CurriculumVersionId") + .HasColumnType("uuid") + .HasColumnName("curriculum_version_id"); + + b.Property("DifficultyOverride") + .HasColumnType("integer") + .HasColumnName("difficulty_override"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionAssetOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_asset_owner_tenant_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.Property("UpdatedBy") + .HasColumnType("uuid") + .HasColumnName("updated_by"); + + 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_question_placements"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placements_tenant_id_id"); + + b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasDatabaseName("ix_question_placements_question_asset_owner_tenant_id_question~"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId") + .HasDatabaseName("ix_question_placements_tenant_id_assessment_policy_version_id"); + + b.HasIndex("TenantId", "CurriculumNodeId") + .HasDatabaseName("ix_question_placements_tenant_id_curriculum_node_id"); + + b.HasIndex("TenantId", "CurriculumVersionId") + .HasDatabaseName("ix_question_placements_tenant_id_curriculum_version_id"); + + b.HasIndex("TenantId", "QuestionAssetOwnerTenantId", "QuestionAssetId", "CurriculumVersionId", "CurriculumNodeId", "AssessmentPolicyVersionId") + .IsUnique() + .HasDatabaseName("ix_question_placements_tenant_id_question_asset_owner_tenant_i~"); + + b.ToTable("question_placements", null, t => + { + t.HasCheckConstraint("ck_question_placements_valid_period", "valid_to is null or valid_from is null or valid_to > valid_from"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementConcept", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("KnowledgeConceptId") + .HasColumnType("uuid") + .HasColumnName("knowledge_concept_id"); + + b.Property("PlacementId") + .HasColumnType("uuid") + .HasColumnName("placement_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_placement_concepts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placement_concepts_tenant_id_id"); + + b.HasIndex("TenantId", "KnowledgeConceptId") + .HasDatabaseName("ix_question_placement_concepts_tenant_id_knowledge_concept_id"); + + b.HasIndex("TenantId", "PlacementId", "KnowledgeConceptId") + .IsUnique() + .HasDatabaseName("ix_question_placement_concepts_tenant_id_placement_id_knowledg~"); + + b.ToTable("question_placement_concepts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleCondition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("operator"); + + b.Property("RuleGroupId") + .HasColumnType("uuid") + .HasColumnName("rule_group_id"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.Property("TargetNodeId") + .HasColumnType("uuid") + .HasColumnName("target_node_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_placement_rule_conditions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placement_rule_conditions_tenant_id_id"); + + b.HasIndex("TargetDimensionDefinitionId") + .HasDatabaseName("ix_question_placement_rule_conditions_target_dimension_definit~"); + + b.HasIndex("TargetNodeId") + .HasDatabaseName("ix_question_placement_rule_conditions_target_node_id"); + + b.HasIndex("TenantId", "RuleGroupId", "TargetDimensionDefinitionId", "TargetNodeId", "Operator") + .IsUnique() + .HasDatabaseName("ix_question_placement_rule_conditions_tenant_id_rule_group_id_~"); + + b.ToTable("question_placement_rule_conditions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("GroupOrder") + .HasColumnType("integer") + .HasColumnName("group_order"); + + b.Property("PlacementId") + .HasColumnType("uuid") + .HasColumnName("placement_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_question_placement_rule_groups"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_placement_rule_groups_tenant_id_id"); + + b.HasIndex("TenantId", "PlacementId", "GroupOrder") + .IsUnique() + .HasDatabaseName("ix_question_placement_rule_groups_tenant_id_placement_id_group~"); + + b.ToTable("question_placement_rule_groups", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionRevision", 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("DeliveryFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("delivery_fingerprint"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("Explanation") + .HasColumnType("text") + .HasColumnName("explanation"); + + b.Property("NormalizedContent") + .IsRequired() + .HasColumnType("text") + .HasColumnName("normalized_content"); + + b.Property("Options") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("options") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("question_type"); + + b.Property("RevisionNo") + .HasColumnType("integer") + .HasColumnName("revision_no"); + + b.Property("SubQuestions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sub_questions") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TypeLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label"); + + b.HasKey("Id") + .HasName("pk_question_revisions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_revisions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "QuestionAssetId", "Id") + .HasName("ak_question_revisions_tenant_id_question_asset_id_id"); + + b.HasIndex("NormalizedContent") + .HasDatabaseName("ix_question_revisions_content_trgm"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("NormalizedContent"), "gin"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("NormalizedContent"), new[] { "gin_trgm_ops" }); + + b.HasIndex("TenantId", "DeliveryFingerprint") + .HasDatabaseName("ix_question_revisions_tenant_id_delivery_fingerprint"); + + b.HasIndex("TenantId", "QuestionAssetId", "RevisionNo") + .IsUnique() + .HasDatabaseName("ix_question_revisions_tenant_id_question_asset_id_revision_no"); + + b.ToTable("question_revisions", (string)null); + }); + modelBuilder.Entity("Tiku.Domain.Content.QuestionTypeGroup", b => { b.Property("Id") @@ -5956,6 +7905,51 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.ToTable("question_videos", (string)null); }); + modelBuilder.Entity("Tiku.Domain.Content.ScoreAdjustmentRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Configuration") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("configuration") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RuleType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("rule_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_score_adjustment_rules"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_score_adjustment_rules_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "Priority") + .IsUnique() + .HasDatabaseName("ix_score_adjustment_rules_tenant_id_assessment_policy_version_~"); + + b.ToTable("score_adjustment_rules", (string)null); + }); + modelBuilder.Entity("Tiku.Domain.Content.SubjectShare", b => { b.Property("TenantId") @@ -6005,6 +7999,177 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.ToTable("subject_shares", (string)null); }); + modelBuilder.Entity("Tiku.Domain.Content.SubjectiveRubric", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("Criteria") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("criteria") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("MaxScore") + .HasColumnType("numeric") + .HasColumnName("max_score"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_subjective_rubrics"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_subjective_rubrics_tenant_id_id"); + + b.HasIndex("TenantId", "AssessmentPolicyVersionId", "Code") + .IsUnique() + .HasDatabaseName("ix_subjective_rubrics_tenant_id_assessment_policy_version_id_c~"); + + b.ToTable("subjective_rubrics", null, t => + { + t.HasCheckConstraint("ck_subjective_rubric_max_score", "max_score > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TargetDimensionDefinition", 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsHierarchical") + .HasColumnType("boolean") + .HasColumnName("is_hierarchical"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_target_dimension_definitions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_target_dimension_definitions_code"); + + b.ToTable("target_dimension_definitions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TargetNode", 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("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .IsRequired() + .HasColumnType("ltree") + .HasColumnName("path"); + + b.Property("TargetDimensionDefinitionId") + .HasColumnType("uuid") + .HasColumnName("target_dimension_definition_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_target_nodes"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_target_nodes_parent_id"); + + b.HasIndex("Path") + .HasDatabaseName("ix_target_nodes_path"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Path"), "gist"); + + b.HasIndex("TargetDimensionDefinitionId", "Code") + .IsUnique() + .HasDatabaseName("ix_target_nodes_target_dimension_definition_id_code"); + + b.ToTable("target_nodes", (string)null); + }); + modelBuilder.Entity("Tiku.Domain.Content.TenantQuestionBankPreference", b => { b.Property("Id") @@ -8762,6 +10927,10 @@ namespace Tiku.Infrastructure.Persistence.Migrations .IsUnique() .HasDatabaseName("ix_answer_records_tenant_id_user_id_practice_session_id_client~"); + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ux_answer_records_idempotency"); + b.HasIndex("TenantId", "UserId", "PracticeSessionId", "SessionQuestionId", "Revision") .IsUnique() .HasDatabaseName("ux_answer_records_session_question_revision"); @@ -8769,73 +10938,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.ToTable("answer_records", (string)null); }); - modelBuilder.Entity("Tiku.Domain.Learning.AnswerSubmissionIdempotency", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id") - .HasDefaultValueSql("gen_random_uuid()"); - - b.Property("AnswerRecordId") - .HasColumnType("uuid") - .HasColumnName("answer_record_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("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_answer_submission_idempotencies"); - - b.HasAlternateKey("TenantId", "Id") - .HasName("ak_answer_submission_idempotencies_tenant_id_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_answer_submission_idempotencies_user_id"); - - b.HasIndex("TenantId", "AnswerRecordId") - .HasDatabaseName("ix_answer_submission_idempotencies_tenant_id_answer_record_id"); - - b.HasIndex("TenantId", "UserId", "PracticeSessionId", "IdempotencyKey") - .IsUnique() - .HasDatabaseName("ux_answer_submission_idempotency"); - - b.ToTable("answer_submission_idempotencies", (string)null); - }); - modelBuilder.Entity("Tiku.Domain.Learning.BusinessLine", b => { b.Property("Id") @@ -8901,6 +11003,96 @@ namespace Tiku.Infrastructure.Persistence.Migrations }); }); + modelBuilder.Entity("Tiku.Domain.Learning.ClassAssignmentGrant", 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("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_type"); + + b.Property("RevokedBy") + .HasColumnType("uuid") + .HasColumnName("revoked_by"); + + b.Property("RevokedReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("revoked_reason"); + + 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_class_assignment_grants"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_class_assignment_grants_tenant_id_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId") + .HasDatabaseName("ix_class_assignment_grants_tenant_id_product_access_manifest_v~"); + + b.HasIndex("TenantId", "ClassId", "Status", "StartsAt") + .HasDatabaseName("ix_class_assignment_grants_tenant_id_class_id_status_starts_at"); + + b.HasIndex("TenantId", "ResourceType", "ResourceId", "Status") + .HasDatabaseName("ix_class_assignment_grants_tenant_id_resource_type_resource_id~"); + + b.ToTable("class_assignment_grants", null, t => + { + t.HasCheckConstraint("ck_class_assignment_grant_period", "ends_at is null or ends_at > starts_at"); + }); + }); + modelBuilder.Entity("Tiku.Domain.Learning.ClassContentAssignment", b => { b.Property("Id") @@ -9236,6 +11428,92 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.ToTable("dashboard_daily_stats", (string)null); }); + modelBuilder.Entity("Tiku.Domain.Learning.EffectiveAccessProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.PrimitiveCollection("AlternateProfileVersionIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("alternate_profile_version_ids"); + + b.PrimitiveCollection("AudienceSegmentIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("audience_segment_ids"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("CompiledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("compiled_at"); + + b.PrimitiveCollection("ContentReleaseIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("content_release_ids"); + + b.Property("ContentVersion") + .HasColumnType("bigint") + .HasColumnName("content_version"); + + b.Property("GrantVersion") + .HasColumnType("bigint") + .HasColumnName("grant_version"); + + b.PrimitiveCollection("ManifestVersionIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("manifest_version_ids"); + + b.Property("PrimaryProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("primary_profile_version_id"); + + b.Property("StrongRevocationVersion") + .HasColumnType("bigint") + .HasColumnName("strong_revocation_version"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("valid_until"); + + b.HasKey("Id") + .HasName("pk_effective_access_projections"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_effective_access_projections_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_effective_access_projections_business_line_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_effective_access_projections_user_id"); + + b.HasIndex("TenantId", "ValidUntil") + .HasDatabaseName("ix_effective_access_projections_tenant_id_valid_until"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ix_effective_access_projections_tenant_id_user_id_business_lin~"); + + b.ToTable("effective_access_projections", (string)null); + }); + modelBuilder.Entity("Tiku.Domain.Learning.ExamDate", b => { b.Property("Id") @@ -9501,6 +11779,12 @@ namespace Tiku.Infrastructure.Persistence.Migrations modelBuilder.Entity("Tiku.Domain.Learning.LearningOutboxMessage", b => { + b.Property("OccurredAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_at") + .HasDefaultValueSql("now()"); + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid") @@ -9545,12 +11829,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("character varying(200)") .HasColumnName("locked_by"); - b.Property("OccurredAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("occurred_at") - .HasDefaultValueSql("now()"); - b.Property("Payload") .ValueGeneratedOnAdd() .HasColumnType("jsonb") @@ -9569,20 +11847,20 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("uuid") .HasColumnName("tenant_id"); - b.HasKey("Id") + b.HasKey("OccurredAt", "Id") .HasName("pk_learning_outbox_messages"); - b.HasAlternateKey("TenantId", "Id") - .HasName("ak_learning_outbox_messages_tenant_id_id"); - - b.HasIndex("TenantId", "EventType", "AggregateId") - .IsUnique() - .HasDatabaseName("ix_learning_outbox_messages_tenant_id_event_type_aggregate_id"); + b.HasAlternateKey("TenantId", "OccurredAt", "Id") + .HasName("ak_learning_outbox_messages_tenant_id_occurred_at_id"); b.HasIndex("AvailableAt", "LockExpiresAt", "OccurredAt", "Id") .HasDatabaseName("ix_learning_outbox_pending") .HasFilter("processed_at IS NULL AND dead_lettered_at IS NULL"); + b.HasIndex("TenantId", "EventType", "AggregateId", "OccurredAt") + .IsUnique() + .HasDatabaseName("ix_learning_outbox_messages_tenant_id_event_type_aggregate_id_~"); + b.ToTable("learning_outbox_messages", null, t => { t.HasCheckConstraint("ck_learning_outbox_messages_attempt_count", "attempt_count >= 0"); @@ -10148,6 +12426,14 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnName("id") .HasDefaultValueSql("gen_random_uuid()"); + b.Property("AssessmentPolicyVersionId") + .HasColumnType("uuid") + .HasColumnName("assessment_policy_version_id"); + + b.Property("ContentReleaseQuestionId") + .HasColumnType("uuid") + .HasColumnName("content_release_question_id"); + b.Property("DifficultySnapshot") .HasColumnType("integer") .HasColumnName("difficulty_snapshot"); @@ -10166,7 +12452,11 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("uuid") .HasColumnName("practice_session_id"); - b.Property("QuestionId") + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("QuestionId") .HasColumnType("uuid") .HasColumnName("question_id"); @@ -10174,10 +12464,18 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasColumnType("uuid") .HasColumnName("question_owner_tenant_id"); - b.Property("QuestionReferenceId") + b.Property("QuestionPlacementId") + .HasColumnType("uuid") + .HasColumnName("question_placement_id"); + + b.Property("QuestionReferenceId") .HasColumnType("uuid") .HasColumnName("question_reference_id"); + b.Property("QuestionRevisionId") + .HasColumnType("uuid") + .HasColumnName("question_revision_id"); + b.Property("QuestionType") .IsRequired() .ValueGeneratedOnAdd() @@ -10186,7 +12484,7 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasDefaultValue("choice") .HasColumnName("question_type"); - b.Property("QuestionVersionId") + b.Property("QuestionVersionId") .HasColumnType("uuid") .HasColumnName("question_version_id"); @@ -10228,6 +12526,11 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.HasIndex("QuestionOwnerTenantId", "QuestionId", "QuestionVersionId") .HasDatabaseName("ix_practice_session_questions_question_owner_tenant_id_questio~"); + b.HasIndex("TenantId", "PracticeSessionId", "ContentReleaseQuestionId") + .IsUnique() + .HasDatabaseName("ix_practice_session_questions_tenant_id_practice_session_id_co~") + .HasFilter("content_release_question_id is not null"); + b.HasIndex("TenantId", "PracticeSessionId", "Position") .IsUnique() .HasDatabaseName("ix_practice_session_questions_tenant_id_practice_session_id_po~"); @@ -10541,6 +12844,288 @@ namespace Tiku.Infrastructure.Persistence.Migrations }); }); + modelBuilder.Entity("Tiku.Domain.Learning.ProductAccessManifest", 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("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LearningProductId") + .HasColumnType("uuid") + .HasColumnName("learning_product_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_product_access_manifests"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_access_manifests_tenant_id_id"); + + b.HasIndex("TenantId", "LearningProductId") + .IsUnique() + .HasDatabaseName("ix_product_access_manifests_tenant_id_learning_product_id"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_product_access_manifests_tenant_id_id_current_version_id"); + + b.ToTable("product_access_manifests", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductAccessManifestVersion", 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("DailyQuestionLimit") + .HasColumnType("integer") + .HasColumnName("daily_question_limit"); + + b.Property("MaxActiveTargets") + .HasColumnType("integer") + .HasColumnName("max_active_targets"); + + b.Property("MaxAlternateTargets") + .HasColumnType("integer") + .HasColumnName("max_alternate_targets"); + + b.Property("ProductAccessManifestId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_id"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalQuestionLimit") + .HasColumnType("integer") + .HasColumnName("total_question_limit"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_product_access_manifest_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_access_manifest_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "ProductAccessManifestId", "Id") + .HasName("ak_product_access_manifest_versions_tenant_id_product_access_m~"); + + b.HasIndex("TenantId", "ProductAccessManifestId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_product_access_manifest_versions_tenant_id_product_access_m~"); + + b.ToTable("product_access_manifest_versions", null, t => + { + t.HasCheckConstraint("ck_product_manifest_active_targets", "max_active_targets between 1 and 16"); + + t.HasCheckConstraint("ck_product_manifest_alternate_targets", "max_alternate_targets between 0 and max_active_targets"); + + t.HasCheckConstraint("ck_product_manifest_daily_question_limit", "daily_question_limit is null or daily_question_limit >= 0"); + + t.HasCheckConstraint("ck_product_manifest_total_question_limit", "total_question_limit is null or total_question_limit >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ContentOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("content_owner_tenant_id"); + + b.Property("ContentReleaseId") + .HasColumnType("uuid") + .HasColumnName("content_release_id"); + + b.Property("PlatformContentPackageOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("platform_content_package_owner_tenant_id"); + + b.Property("PlatformContentPackageVersionId") + .HasColumnType("uuid") + .HasColumnName("platform_content_package_version_id"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_product_manifest_releases"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_manifest_releases_tenant_id_id"); + + b.HasIndex("ContentOwnerTenantId", "ContentReleaseId") + .HasDatabaseName("ix_product_manifest_releases_content_owner_tenant_id_content_r~"); + + b.HasIndex("PlatformContentPackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasDatabaseName("ix_product_manifest_releases_platform_content_package_owner_te~"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "ContentOwnerTenantId", "ContentReleaseId") + .IsUnique() + .HasDatabaseName("ix_product_manifest_releases_tenant_id_product_access_manifest~"); + + b.ToTable("product_manifest_releases", null, t => + { + t.HasCheckConstraint("ck_product_manifest_release_platform_pair", "(platform_content_package_owner_tenant_id is null) = (platform_content_package_version_id is null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("display_name"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("resource_type"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_product_manifest_resources"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_manifest_resources_tenant_id_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "ResourceType", "ResourceId") + .IsUnique() + .HasDatabaseName("ux_product_manifest_resource"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "SortOrder", "Id") + .HasDatabaseName("ix_product_manifest_resource_order"); + + b.ToTable("product_manifest_resources", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("MayBeAlternate") + .HasColumnType("boolean") + .HasColumnName("may_be_alternate"); + + b.Property("MayBePrimary") + .HasColumnType("boolean") + .HasColumnName("may_be_primary"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_product_manifest_targets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_product_manifest_targets_tenant_id_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_product_manifest_targets_exam_target_profile_version_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_product_manifest_targets_tenant_id_product_access_manifest_~"); + + b.ToTable("product_manifest_targets", null, t => + { + t.HasCheckConstraint("ck_product_manifest_target_role", "may_be_primary or may_be_alternate"); + }); + }); + modelBuilder.Entity("Tiku.Domain.Learning.RecentPractice", b => { b.Property("Id") @@ -10913,6 +13498,113 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.ToTable("revenue_daily_stats", (string)null); }); + modelBuilder.Entity("Tiku.Domain.Learning.StudentEntitlement", 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("LearningProductId") + .HasColumnType("uuid") + .HasColumnName("learning_product_id"); + + b.Property("ProductAccessManifestVersionId") + .HasColumnType("uuid") + .HasColumnName("product_access_manifest_version_id"); + + 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("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_type"); + + 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.Property("UsedQuestionCount") + .HasColumnType("integer") + .HasColumnName("used_question_count"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_student_entitlements_v2"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_entitlements_v2_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_entitlements_v2_user_id"); + + b.HasIndex("TenantId", "LearningProductId") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_learning_product_id"); + + b.HasIndex("TenantId", "ProductAccessManifestVersionId") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_product_access_manifest_v~"); + + b.HasIndex("TenantId", "SourceType", "SourceId") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_source_type_source_id") + .HasFilter("source_id is not null"); + + b.HasIndex("TenantId", "UserId", "Status", "StartsAt", "EndsAt") + .HasDatabaseName("ix_student_entitlements_v2_tenant_id_user_id_status_starts_at_~"); + + b.ToTable("student_entitlements_v2", null, t => + { + t.HasCheckConstraint("ck_student_entitlement_v2_period", "ends_at is null or ends_at > starts_at"); + + t.HasCheckConstraint("ck_student_entitlements_v2_used_question_count", "used_question_count >= 0"); + }); + }); + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetRegionHistory", b => { b.Property("Id") @@ -10999,6 +13691,191 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.ToTable("student_target_region_history", (string)null); }); + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetSelectionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + b.Property("ChangedBy") + .HasColumnType("uuid") + .HasColumnName("changed_by"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("effective_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("EntitlementId") + .HasColumnType("uuid") + .HasColumnName("entitlement_id"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("IsCurrent") + .HasColumnType("boolean") + .HasColumnName("is_current"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("role"); + + b.Property("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_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.HasKey("Id") + .HasName("pk_student_target_selection_history"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_target_selection_history_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_student_target_selection_history_business_line_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_student_target_selection_history_exam_target_profile_versio~"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_target_selection_history_user_id"); + + b.HasIndex("TenantId", "EntitlementId") + .HasDatabaseName("ix_student_target_selection_history_tenant_id_entitlement_id"); + + b.HasIndex("TenantId", "SourceType", "SourceId") + .HasDatabaseName("ix_student_target_selection_history_tenant_id_source_type_sour~"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ux_student_target_primary") + .HasFilter("is_current and role = 'primary'"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId", "EffectiveAt") + .HasDatabaseName("ix_student_target_selection_history"); + + b.HasIndex("TenantId", "UserId", "BusinessLineId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_student_target_selection_history_tenant_id_user_id_business~") + .HasFilter("is_current"); + + b.ToTable("student_target_selection_history", null, t => + { + t.HasCheckConstraint("ck_student_target_selection_source", "(source_type = 'student_entitlement' and entitlement_id = source_id) or (source_type = 'class_assignment_grant' and entitlement_id is null)"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantBusinessLicense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BusinessLineId") + .HasColumnType("uuid") + .HasColumnName("business_line_id"); + + 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("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.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_tenant_business_licenses"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_business_licenses_tenant_id_id"); + + b.HasIndex("BusinessLineId") + .HasDatabaseName("ix_tenant_business_licenses_business_line_id"); + + b.HasIndex("TenantId", "BusinessLineId") + .IsUnique() + .HasDatabaseName("ix_tenant_business_licenses_tenant_id_business_line_id"); + + b.ToTable("tenant_business_licenses", null, t => + { + t.HasCheckConstraint("ck_tenant_business_license_period", "ends_at is null or ends_at > starts_at"); + }); + }); + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicense", b => { b.Property("Id") @@ -11120,6 +13997,57 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.ToTable("tenant_learning_license_regions", (string)null); }); + modelBuilder.Entity("Tiku.Domain.Learning.TenantLicensedTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("EndsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_at"); + + b.Property("ExamTargetProfileVersionId") + .HasColumnType("uuid") + .HasColumnName("exam_target_profile_version_id"); + + b.Property("IsBaseTarget") + .HasColumnType("boolean") + .HasColumnName("is_base_target"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_at"); + + b.Property("TenantBusinessLicenseId") + .HasColumnType("uuid") + .HasColumnName("tenant_business_license_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_tenant_licensed_targets"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_licensed_targets_tenant_id_id"); + + b.HasIndex("ExamTargetProfileVersionId") + .HasDatabaseName("ix_tenant_licensed_targets_exam_target_profile_version_id"); + + b.HasIndex("TenantId", "TenantBusinessLicenseId", "ExamTargetProfileVersionId") + .IsUnique() + .HasDatabaseName("ix_tenant_licensed_targets_tenant_id_tenant_business_license_i~"); + + b.ToTable("tenant_licensed_targets", null, t => + { + t.HasCheckConstraint("ck_tenant_licensed_target_period", "ends_at is null or ends_at > starts_at"); + }); + }); + modelBuilder.Entity("Tiku.Domain.Learning.UserScoreEvent", b => { b.Property("Id") @@ -11248,6 +14176,63 @@ namespace Tiku.Infrastructure.Persistence.Migrations b.ToTable("wrong_questions", (string)null); }); + modelBuilder.Entity("Tiku.Domain.Learning.WrongQuestionV2", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("QuestionAssetOwnerTenantId") + .HasColumnType("uuid") + .HasColumnName("question_asset_owner_tenant_id"); + + b.Property("QuestionAssetId") + .HasColumnType("uuid") + .HasColumnName("question_asset_id"); + + b.Property("LastQuestionPlacementId") + .HasColumnType("uuid") + .HasColumnName("last_question_placement_id"); + + b.Property("LastQuestionRevisionId") + .HasColumnType("uuid") + .HasColumnName("last_question_revision_id"); + + b.Property("LastWrongAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_wrong_at") + .HasDefaultValueSql("now()"); + + 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", "QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasName("pk_wrong_questions_v2"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_wrong_questions_v2_user_id"); + + b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId", "LastQuestionRevisionId") + .HasDatabaseName("ix_wrong_questions_v2_question_asset_owner_tenant_id_question_~"); + + b.HasIndex("TenantId", "UserId", "LastWrongAt") + .HasDatabaseName("ix_wrong_questions_v2_tenant_id_user_id_last_wrong_at"); + + b.ToTable("wrong_questions_v2", (string)null); + }); + modelBuilder.Entity("Tiku.Domain.Operations.Announcement", b => { b.Property("Id") @@ -18457,25 +21442,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations .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) @@ -18620,23 +21586,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations .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) @@ -19306,6 +22255,230 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_app_assets_tenants_tenant_id"); }); + modelBuilder.Entity("Tiku.Domain.Content.AssessmentPolicy", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_assessment_policies_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_policies_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "AssessmentPolicyId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_assessment_policies_assessment_policy_versions_tenant_id_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentPolicyVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_policy_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicy", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_policy_versions_assessment_policies_tenant_id_as~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AssessmentScoreBand", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_score_bands_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_assessment_score_bands_assessment_policy_versions_tenant_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegment", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segments_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segments_content_releases_tenant_id_content_releas~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.AudienceSegmentMember", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_audience_segment_members_exam_target_profile_versions_exam_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segment_members_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AudienceSegment", null) + .WithMany() + .HasForeignKey("TenantId", "AudienceSegmentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audience_segment_members_audience_segments_tenant_id_audien~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BlueprintRelease", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_blueprint_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CollectionRelease", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_blueprint_releases_collection_releases_tenant_id_collection~"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_blueprint_releases_content_releases_tenant_id_content_relea~"); + + b.HasOne("Tiku.Domain.Content.ExamPaperSpecificationVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ExamPaperSpecificationVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_blueprint_releases_exam_paper_specification_versions_tenant~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetDimensionRequirement", b => + { + b.HasOne("Tiku.Domain.Content.BusinessTargetPolicyVersion", null) + .WithMany() + .HasForeignKey("BusinessTargetPolicyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_target_dimension_requirements_business_target_poli~"); + + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_business_target_dimension_requirements_target_dimension_def~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicy", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_business_target_policies_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Content.BusinessTargetPolicyVersion", null) + .WithMany() + .HasForeignKey("Id", "CurrentVersionId") + .HasPrincipalKey("BusinessTargetPolicyId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_business_target_policies_business_target_policy_versions_id~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.BusinessTargetPolicyVersion", b => + { + b.HasOne("Tiku.Domain.Content.BusinessTargetPolicy", null) + .WithMany() + .HasForeignKey("BusinessTargetPolicyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_target_policy_versions_business_target_policies_bu~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionRelease", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_collection_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_collection_releases_content_releases_tenant_id_content_rele~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CollectionReleaseQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_collection_release_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CollectionRelease", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_collection_release_questions_collection_releases_tenant_id_~"); + + b.HasOne("Tiku.Domain.Content.ContentReleaseQuestion", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseQuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_collection_release_questions_content_release_questions_tena~"); + }); + modelBuilder.Entity("Tiku.Domain.Content.ContentAsset", b => { b.HasOne("Tiku.Domain.Identity.User", null) @@ -19569,6 +22742,251 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_content_nodes_regions_tenant_id_region_id"); }); + modelBuilder.Entity("Tiku.Domain.Content.ContentRelease", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_releases_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_releases_curriculum_versions_tenant_id_curriculum_v~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ContentReleaseQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_release_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_assessment_policy_versions_tenant~"); + + b.HasOne("Tiku.Domain.Content.AudienceSegment", null) + .WithMany() + .HasForeignKey("TenantId", "AudienceSegmentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_audience_segments_tenant_id_audie~"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_content_release_questions_content_releases_tenant_id_conten~"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacement", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionPlacementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_question_placements_tenant_id_que~"); + + b.HasOne("Tiku.Domain.Content.QuestionRevision", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId", "QuestionRevisionId") + .HasPrincipalKey("TenantId", "QuestionAssetId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_content_release_questions_question_revisions_question_asset~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.Curriculum", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_curricula_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curricula_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "CurriculumId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_curricula_curriculum_versions_tenant_id_id_current_version_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNode", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_nodes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_nodes_curriculum_versions_tenant_id_curriculum_v~"); + + b.HasOne("Tiku.Domain.Content.KnowledgeConcept", null) + .WithMany() + .HasForeignKey("TenantId", "KnowledgeConceptId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_curriculum_nodes_knowledge_concepts_tenant_id_knowledge_con~"); + + b.HasOne("Tiku.Domain.Content.CurriculumNode", null) + .WithMany() + .HasForeignKey("TenantId", "ParentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_curriculum_nodes_curriculum_nodes_tenant_id_parent_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumNodeClosure", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_node_closure_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.CurriculumVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.Curriculum", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_curriculum_versions_curricula_tenant_id_curriculum_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecification", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_paper_specifications_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_paper_specifications_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamPaperSpecificationVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_paper_specification_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ExamPaperSpecification", null) + .WithMany() + .HasForeignKey("TenantId", "SpecificationId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_paper_specification_versions_exam_paper_specifications~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfile", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_target_profiles_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("Id", "CurrentVersionId") + .HasPrincipalKey("ExamTargetProfileId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_exam_target_profiles_exam_target_profile_versions_id_curren~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileValue", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_values_exam_target_profile_versions_exa~"); + + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_values_target_dimension_definitions_tar~"); + + b.HasOne("Tiku.Domain.Content.TargetNode", null) + .WithMany() + .HasForeignKey("TargetNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_values_target_nodes_target_node_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ExamTargetProfileVersion", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfile", null) + .WithMany() + .HasForeignKey("ExamTargetProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_target_profile_versions_exam_target_profiles_exam_targ~"); + }); + modelBuilder.Entity("Tiku.Domain.Content.HandbookChapter", b => { b.HasOne("Tiku.Domain.Tenancy.Tenant", null) @@ -19686,6 +23104,63 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_images_tenants_tenant_id"); }); + modelBuilder.Entity("Tiku.Domain.Content.KnowledgeConcept", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_knowledge_concepts_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.ObjectiveGradingRule", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_objective_grading_rules_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_objective_grading_rules_assessment_policy_versions_tenant_i~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageCellProjection", b => + { + b.HasOne("Tiku.Domain.Content.PlatformContentPackageVersion", null) + .WithMany() + .HasForeignKey("PackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_content_package_cell_projections_platform_content_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.PlatformContentPackageVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_content_package_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("TenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_platform_content_package_versions_content_releases_tenant_i~"); + }); + modelBuilder.Entity("Tiku.Domain.Content.PracticeBlueprint", b => { b.HasOne("Tiku.Domain.Identity.User", null) @@ -19736,6 +23211,30 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_practice_blueprints_regions_tenant_id_region_id"); }); + modelBuilder.Entity("Tiku.Domain.Content.QuestionAsset", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_assets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionFamily", null) + .WithMany() + .HasForeignKey("TenantId", "FamilyId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_assets_question_families_tenant_id_family_id"); + + b.HasOne("Tiku.Domain.Content.QuestionRevision", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentRevisionId") + .HasPrincipalKey("TenantId", "QuestionAssetId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_assets_question_revisions_tenant_id_id_current_rev~"); + }); + modelBuilder.Entity("Tiku.Domain.Content.QuestionCollection", b => { b.HasOne("Tiku.Domain.Identity.User", null) @@ -19834,6 +23333,152 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_question_collection_items_tenant_question_references_tenant~"); }); + modelBuilder.Entity("Tiku.Domain.Content.QuestionFamily", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_families_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacement", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placements_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionAsset", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_question_assets_question_asset_owner_te~"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_assessment_policy_versions_tenant_id_as~"); + + b.HasOne("Tiku.Domain.Content.CurriculumNode", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumNodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_curriculum_nodes_tenant_id_curriculum_n~"); + + b.HasOne("Tiku.Domain.Content.CurriculumVersion", null) + .WithMany() + .HasForeignKey("TenantId", "CurriculumVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placements_curriculum_versions_tenant_id_curriculu~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementConcept", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_concepts_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.KnowledgeConcept", null) + .WithMany() + .HasForeignKey("TenantId", "KnowledgeConceptId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placement_concepts_knowledge_concepts_tenant_id_kn~"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacement", null) + .WithMany() + .HasForeignKey("TenantId", "PlacementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_concepts_question_placements_tenant_id_p~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleCondition", b => + { + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_target_dimension_definit~"); + + b.HasOne("Tiku.Domain.Content.TargetNode", null) + .WithMany() + .HasForeignKey("TargetNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_target_nodes_target_node~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacementRuleGroup", null) + .WithMany() + .HasForeignKey("TenantId", "RuleGroupId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_conditions_question_placement_rule_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionPlacementRuleGroup", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_groups_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionPlacement", null) + .WithMany() + .HasForeignKey("TenantId", "PlacementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_placement_rule_groups_question_placements_tenant_i~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.QuestionRevision", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_revisions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.QuestionAsset", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_revisions_question_assets_tenant_id_question_asset~"); + }); + modelBuilder.Entity("Tiku.Domain.Content.QuestionTypeGroup", b => { b.HasOne("Tiku.Domain.Tenancy.Tenant", null) @@ -19875,6 +23520,24 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_question_videos_video_explanations_tenant_id_video_id"); }); + modelBuilder.Entity("Tiku.Domain.Content.ScoreAdjustmentRule", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_score_adjustment_rules_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_score_adjustment_rules_assessment_policy_versions_tenant_id~"); + }); + modelBuilder.Entity("Tiku.Domain.Content.SubjectShare", b => { b.HasOne("Tiku.Domain.Tenancy.Tenant", null) @@ -19901,6 +23564,40 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_subject_shares_subjects_tenant_id_target_subject_id"); }); + modelBuilder.Entity("Tiku.Domain.Content.SubjectiveRubric", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subjective_rubrics_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.AssessmentPolicyVersion", null) + .WithMany() + .HasForeignKey("TenantId", "AssessmentPolicyVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subjective_rubrics_assessment_policy_versions_tenant_id_ass~"); + }); + + modelBuilder.Entity("Tiku.Domain.Content.TargetNode", b => + { + b.HasOne("Tiku.Domain.Content.TargetNode", null) + .WithMany() + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_target_nodes_target_nodes_parent_id"); + + b.HasOne("Tiku.Domain.Content.TargetDimensionDefinition", null) + .WithMany() + .HasForeignKey("TargetDimensionDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_target_nodes_target_dimension_definitions_target_dimension_~"); + }); + modelBuilder.Entity("Tiku.Domain.Content.TenantQuestionBankPreference", b => { b.HasOne("Tiku.Domain.Identity.User", null) @@ -20541,22 +24238,30 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_answer_records_practice_sessions_tenant_id_user_id_practice~"); }); - modelBuilder.Entity("Tiku.Domain.Learning.AnswerSubmissionIdempotency", b => + modelBuilder.Entity("Tiku.Domain.Learning.ClassAssignmentGrant", b => { - b.HasOne("Tiku.Domain.Identity.User", null) + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) .WithMany() - .HasForeignKey("UserId") + .HasForeignKey("TenantId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("fk_answer_submission_idempotencies_users_user_id"); + .HasConstraintName("fk_class_assignment_grants_tenants_tenant_id"); - b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + b.HasOne("Tiku.Domain.Tenancy.TenantClass", null) .WithMany() - .HasForeignKey("TenantId", "UserId", "PracticeSessionId") - .HasPrincipalKey("TenantId", "UserId", "Id") + .HasForeignKey("TenantId", "ClassId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("fk_answer_submission_idempotencies_practice_sessions_tenant_id~"); + .HasConstraintName("fk_class_assignment_grants_tenant_classes_tenant_id_class_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_class_assignment_grants_product_access_manifest_versions_te~"); }); modelBuilder.Entity("Tiku.Domain.Learning.ClassContentAssignment", b => @@ -20663,6 +24368,30 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_dashboard_daily_stats_regions_tenant_id_region_id"); }); + modelBuilder.Entity("Tiku.Domain.Learning.EffectiveAccessProjection", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_effective_access_projections_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_effective_access_projections_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_effective_access_projections_users_user_id"); + }); + modelBuilder.Entity("Tiku.Domain.Learning.ExamDate", b => { b.HasOne("Tiku.Domain.Tenancy.Tenant", null) @@ -20925,7 +24654,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations .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) @@ -20933,7 +24661,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasForeignKey("TenantId", "QuestionOwnerTenantId", "QuestionId") .HasPrincipalKey("TenantId", "QuestionOwnerTenantId", "QuestionId") .OnDelete(DeleteBehavior.Restrict) - .IsRequired() .HasConstraintName("fk_practice_session_questions_tenant_question_references_tenan~"); }); @@ -21002,6 +24729,125 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_practice_session_report_sections_practice_session_reports_t~"); }); + modelBuilder.Entity("Tiku.Domain.Learning.ProductAccessManifest", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifests_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "LearningProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifests_learning_products_tenant_id_learni~"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "ProductAccessManifestId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_product_access_manifests_product_access_manifest_versions_t~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductAccessManifestVersion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifest_versions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifest", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_access_manifest_versions_product_access_manifests_t~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestRelease", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_releases_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Content.ContentRelease", null) + .WithMany() + .HasForeignKey("ContentOwnerTenantId", "ContentReleaseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_product_manifest_releases_content_releases_content_owner_te~"); + + b.HasOne("Tiku.Domain.Content.PlatformContentPackageVersion", null) + .WithMany() + .HasForeignKey("PlatformContentPackageOwnerTenantId", "PlatformContentPackageVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_product_manifest_releases_platform_content_package_versions~"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_releases_product_access_manifest_versions_~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestResource", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_resources_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_resources_product_access_manifest_versions~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ProductManifestTarget", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_product_manifest_targets_exam_target_profile_versions_exam_~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_targets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_product_manifest_targets_product_access_manifest_versions_t~"); + }); + modelBuilder.Entity("Tiku.Domain.Learning.RecentPractice", b => { b.HasOne("Tiku.Domain.Tenancy.Tenant", null) @@ -21088,6 +24934,39 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_revenue_daily_stats_regions_tenant_id_region_id"); }); + modelBuilder.Entity("Tiku.Domain.Learning.StudentEntitlement", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_users_user_id"); + + b.HasOne("Tiku.Domain.Learning.LearningProduct", null) + .WithMany() + .HasForeignKey("TenantId", "LearningProductId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_learning_products_tenant_id_learnin~"); + + b.HasOne("Tiku.Domain.Learning.ProductAccessManifestVersion", null) + .WithMany() + .HasForeignKey("TenantId", "ProductAccessManifestVersionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_entitlements_v2_product_access_manifest_versions_te~"); + }); + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetRegionHistory", b => { b.HasOne("Tiku.Domain.Learning.BusinessLine", null) @@ -21125,6 +25004,61 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_student_target_region_history_users_user_id"); }); + modelBuilder.Entity("Tiku.Domain.Learning.StudentTargetSelectionHistory", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_business_lines_business_li~"); + + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_exam_target_profile_versio~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_target_selection_history_users_user_id"); + + b.HasOne("Tiku.Domain.Learning.StudentEntitlement", null) + .WithMany() + .HasForeignKey("TenantId", "EntitlementId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_student_target_selection_history_student_entitlements_v2_te~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.TenantBusinessLicense", b => + { + b.HasOne("Tiku.Domain.Learning.BusinessLine", null) + .WithMany() + .HasForeignKey("BusinessLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_business_licenses_business_lines_business_line_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_business_licenses_tenants_tenant_id"); + }); + modelBuilder.Entity("Tiku.Domain.Learning.TenantLearningLicense", b => { b.HasOne("Tiku.Domain.Learning.BusinessLine", null) @@ -21160,6 +25094,31 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_tenant_learning_license_regions_tenant_learning_licenses_te~"); }); + modelBuilder.Entity("Tiku.Domain.Learning.TenantLicensedTarget", b => + { + b.HasOne("Tiku.Domain.Content.ExamTargetProfileVersion", null) + .WithMany() + .HasForeignKey("ExamTargetProfileVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tenant_licensed_targets_exam_target_profile_versions_exam_t~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_licensed_targets_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.TenantBusinessLicense", null) + .WithMany() + .HasForeignKey("TenantId", "TenantBusinessLicenseId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_licensed_targets_tenant_business_licenses_tenant_id_~"); + }); + modelBuilder.Entity("Tiku.Domain.Learning.UserScoreEvent", b => { b.HasOne("Tiku.Domain.Tenancy.Tenant", null) @@ -21210,6 +25169,39 @@ namespace Tiku.Infrastructure.Persistence.Migrations .HasConstraintName("fk_wrong_questions_tenant_question_references_tenant_id_questi~"); }); + modelBuilder.Entity("Tiku.Domain.Learning.WrongQuestionV2", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_users_user_id"); + + b.HasOne("Tiku.Domain.Content.QuestionAsset", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_question_assets_question_asset_owner_ten~"); + + b.HasOne("Tiku.Domain.Content.QuestionRevision", null) + .WithMany() + .HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId", "LastQuestionRevisionId") + .HasPrincipalKey("TenantId", "QuestionAssetId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_wrong_questions_v2_question_revisions_question_asset_owner_~"); + }); + modelBuilder.Entity("Tiku.Domain.Operations.Announcement", b => { b.HasOne("Tiku.Domain.Tenancy.Tenant", null) diff --git a/Tiku.Infrastructure/Persistence/ModulePersistence.cs b/Tiku.Infrastructure/Persistence/ModulePersistence.cs index 4733450..8f89551 100644 --- a/Tiku.Infrastructure/Persistence/ModulePersistence.cs +++ b/Tiku.Infrastructure/Persistence/ModulePersistence.cs @@ -84,8 +84,6 @@ public interface ICatalogPersistence : IModulePersistence DbSet Majors { get; } DbSet Subjects { get; } DbSet Categories { get; } - DbSet TaxonomyNodes { get; } - DbSet QuestionTaxonomyAssignments { get; } DbSet ScorelineFields { get; } DbSet ScorelineRecords { get; } } @@ -107,6 +105,43 @@ public interface IQuestionBankPersistence : IModulePersistence DbSet ContentImportIssues { get; } DbSet TenantQuestionBankPreferences { get; } DbSet TenantQuestionReferences { get; } + DbSet QuestionAssets { get; } + DbSet QuestionRevisions { get; } + DbSet QuestionFamilies { get; } + DbSet QuestionPlacements { get; } + DbSet QuestionPlacementConcepts { get; } + DbSet QuestionPlacementRuleGroups { get; } + DbSet QuestionPlacementRuleConditions { get; } + DbSet KnowledgeConcepts { get; } + DbSet Curricula { get; } + DbSet CurriculumVersions { get; } + DbSet CurriculumNodes { get; } + DbSet CurriculumNodeClosures { get; } + DbSet ExamPaperSpecifications { get; } + DbSet ExamPaperSpecificationVersions { get; } + DbSet TargetDimensionDefinitions { get; } + DbSet TargetNodes { get; } + DbSet ExamTargetProfiles { get; } + DbSet ExamTargetProfileVersions { get; } + DbSet ExamTargetProfileValues { get; } + DbSet BusinessTargetPolicies { get; } + DbSet BusinessTargetPolicyVersions { get; } + DbSet BusinessTargetDimensionRequirements { get; } + DbSet AssessmentPolicies { get; } + DbSet AssessmentPolicyVersions { get; } + DbSet ObjectiveGradingRules { get; } + DbSet SubjectiveRubrics { get; } + DbSet AssessmentScoreBands { get; } + DbSet ScoreAdjustmentRules { get; } + DbSet ContentReleases { get; } + DbSet AudienceSegments { get; } + DbSet AudienceSegmentMembers { get; } + DbSet ContentReleaseQuestions { get; } + DbSet CollectionReleases { get; } + DbSet CollectionReleaseQuestions { get; } + DbSet BlueprintReleases { get; } + DbSet PlatformContentPackageVersions { get; } + DbSet PlatformContentPackageCellProjections { get; } } public interface IContentAssetPersistence : IModulePersistence @@ -138,6 +173,17 @@ public interface ILearningPersistence : IModulePersistence DbSet StudentTargetRegionHistory { get; } DbSet ClassContentAssignments { get; } DbSet LearningAccessVersions { get; } + DbSet TenantBusinessLicenses { get; } + DbSet TenantLicensedTargets { get; } + DbSet ProductAccessManifests { get; } + DbSet ProductAccessManifestVersions { get; } + DbSet ProductManifestReleases { get; } + DbSet ProductManifestTargets { get; } + DbSet ProductManifestResources { get; } + DbSet StudentEntitlementsV2 { get; } + DbSet StudentTargetSelectionHistory { get; } + DbSet ClassAssignmentGrants { get; } + DbSet EffectiveAccessProjections { get; } DbSet UserWordProgress { get; } DbSet UserWordFavorites { get; } DbSet AiRecommendationReports { get; } @@ -145,12 +191,12 @@ public interface ILearningPersistence : IModulePersistence DbSet PracticeSessionQuestions { get; } DbSet AnswerRecords { get; } DbSet CurrentAnswers { get; } - DbSet AnswerSubmissionIdempotencies { get; } DbSet LearningOperationIdempotencies { get; } DbSet LearningOutboxMessages { get; } DbSet LearningProjectionReceipts { get; } DbSet FavoriteQuestions { get; } DbSet WrongQuestions { get; } + DbSet WrongQuestionsV2 { get; } DbSet RecentPractices { get; } DbSet ExamDates { get; } DbSet Reports { get; } @@ -288,11 +334,6 @@ public interface IPlatformAdministrationPersistence : IIdentityPersistence, ITen { } -public interface IPlatformQuestionBankAdministrationPersistence : IQuestionBankPersistence, - IContentAssetPersistence, ITenancyPersistence, IJobsOperationsPersistence -{ -} - public interface IPlatformTenantCapabilitiesPersistence : IIdentityPersistence, ITenancyPersistence, ICommercePersistence, IGrowthPersistence, IJobsOperationsPersistence, IPlatformControlPlanePersistence { diff --git a/Tiku.Infrastructure/Persistence/Modules/Catalog/TikuDbContext.Catalog.cs b/Tiku.Infrastructure/Persistence/Modules/Catalog/TikuDbContext.Catalog.cs index 8926e37..479729e 100644 --- a/Tiku.Infrastructure/Persistence/Modules/Catalog/TikuDbContext.Catalog.cs +++ b/Tiku.Infrastructure/Persistence/Modules/Catalog/TikuDbContext.Catalog.cs @@ -12,8 +12,6 @@ public sealed partial class TikuDbContext public DbSet Majors => Set(); public DbSet Subjects => Set(); public DbSet Categories => Set(); - public DbSet TaxonomyNodes => Set(); - public DbSet QuestionTaxonomyAssignments => Set(); public DbSet ScorelineFields => Set(); public DbSet ScorelineRecords => Set(); -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/Persistence/Modules/Content/TikuDbContext.Content.cs b/Tiku.Infrastructure/Persistence/Modules/Content/TikuDbContext.Content.cs index 4e55a8f..12904bb 100644 --- a/Tiku.Infrastructure/Persistence/Modules/Content/TikuDbContext.Content.cs +++ b/Tiku.Infrastructure/Persistence/Modules/Content/TikuDbContext.Content.cs @@ -37,4 +37,42 @@ public sealed partial class TikuDbContext public DbSet TenantQuestionBankPreferences => Set(); public DbSet TenantQuestionReferences => Set(); public DbSet AiRecommendationReports => Set(); -} \ No newline at end of file + public DbSet QuestionAssets => Set(); + public DbSet QuestionRevisions => Set(); + public DbSet QuestionFamilies => Set(); + public DbSet QuestionPlacements => Set(); + public DbSet QuestionPlacementConcepts => Set(); + public DbSet QuestionPlacementRuleGroups => Set(); + public DbSet QuestionPlacementRuleConditions => Set(); + public DbSet KnowledgeConcepts => Set(); + public DbSet Curricula => Set(); + public DbSet CurriculumVersions => Set(); + public DbSet CurriculumNodes => Set(); + public DbSet CurriculumNodeClosures => Set(); + public DbSet ExamPaperSpecifications => Set(); + public DbSet ExamPaperSpecificationVersions => Set(); + public DbSet TargetDimensionDefinitions => Set(); + public DbSet TargetNodes => Set(); + public DbSet ExamTargetProfiles => Set(); + public DbSet ExamTargetProfileVersions => Set(); + public DbSet ExamTargetProfileValues => Set(); + public DbSet BusinessTargetPolicies => Set(); + public DbSet BusinessTargetPolicyVersions => Set(); + public DbSet BusinessTargetDimensionRequirements => Set(); + public DbSet AssessmentPolicies => Set(); + public DbSet AssessmentPolicyVersions => Set(); + public DbSet ObjectiveGradingRules => Set(); + public DbSet SubjectiveRubrics => Set(); + public DbSet AssessmentScoreBands => Set(); + public DbSet ScoreAdjustmentRules => Set(); + public DbSet ContentReleases => Set(); + public DbSet AudienceSegments => Set(); + public DbSet AudienceSegmentMembers => Set(); + public DbSet ContentReleaseQuestions => Set(); + public DbSet CollectionReleases => Set(); + public DbSet CollectionReleaseQuestions => Set(); + public DbSet BlueprintReleases => Set(); + public DbSet PlatformContentPackageVersions => Set(); + public DbSet PlatformContentPackageCellProjections => + Set(); +} diff --git a/Tiku.Infrastructure/Persistence/Modules/Learning/TikuDbContext.Learning.cs b/Tiku.Infrastructure/Persistence/Modules/Learning/TikuDbContext.Learning.cs index 6b5bb7d..494eb66 100644 --- a/Tiku.Infrastructure/Persistence/Modules/Learning/TikuDbContext.Learning.cs +++ b/Tiku.Infrastructure/Persistence/Modules/Learning/TikuDbContext.Learning.cs @@ -15,16 +15,27 @@ public sealed partial class TikuDbContext public DbSet StudentTargetRegionHistory => Set(); public DbSet ClassContentAssignments => Set(); public DbSet LearningAccessVersions => Set(); + public DbSet TenantBusinessLicenses => Set(); + public DbSet TenantLicensedTargets => Set(); + public DbSet ProductAccessManifests => Set(); + public DbSet ProductAccessManifestVersions => Set(); + public DbSet ProductManifestReleases => Set(); + public DbSet ProductManifestTargets => Set(); + public DbSet ProductManifestResources => Set(); + public DbSet StudentEntitlementsV2 => Set(); + public DbSet StudentTargetSelectionHistory => Set(); + public DbSet ClassAssignmentGrants => Set(); + public DbSet EffectiveAccessProjections => Set(); public DbSet PracticeSessions => Set(); public DbSet PracticeSessionQuestions => Set(); public DbSet AnswerRecords => Set(); public DbSet CurrentAnswers => Set(); - public DbSet AnswerSubmissionIdempotencies => Set(); public DbSet LearningOperationIdempotencies => Set(); public DbSet LearningOutboxMessages => Set(); public DbSet LearningProjectionReceipts => Set(); public DbSet FavoriteQuestions => Set(); public DbSet WrongQuestions => Set(); + public DbSet WrongQuestionsV2 => Set(); public DbSet RecentPractices => Set(); public DbSet ExamDates => Set(); public DbSet Reports => Set(); diff --git a/Tiku.Infrastructure/Persistence/TikuDbContext.cs b/Tiku.Infrastructure/Persistence/TikuDbContext.cs index 2213487..41f8fe4 100644 --- a/Tiku.Infrastructure/Persistence/TikuDbContext.cs +++ b/Tiku.Infrastructure/Persistence/TikuDbContext.cs @@ -5,7 +5,9 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Tiku.Application.Security; using Tiku.Domain.Common; +using Tiku.Domain.Content; using Tiku.Domain.Identity; +using Tiku.Domain.Learning; using Tiku.Domain.QuestionBanks; namespace Tiku.Infrastructure.Persistence; @@ -17,8 +19,8 @@ public sealed partial class TikuDbContext( IQuestionBankPersistence, IContentAssetPersistence, ILearningPersistence, ILearningAccessPersistence, ICommercePersistence, IPointsPersistence, IGrowthPersistence, IJobsOperationsPersistence, IPlatformControlPlanePersistence, - IPlatformAdministrationPersistence, IPlatformQuestionBankAdministrationPersistence, - IPlatformTenantCapabilitiesPersistence, IPlatformBillingPersistence, IOwnerActivationPersistence, + IPlatformAdministrationPersistence, IPlatformTenantCapabilitiesPersistence, IPlatformBillingPersistence, + IOwnerActivationPersistence, IBootstrapPersistence { public TikuDbContext(DbContextOptions options) @@ -145,6 +147,20 @@ public sealed partial class TikuDbContext( throw new InvalidOperationException( "Published question delivery versions are immutable; create a new version instead."); + ThrowIfChanged("Question revisions are immutable; create a new revision instead."); + ThrowIfPublishedVersionChanged( + "Published curriculum versions are immutable; create a new version instead."); + ThrowIfPublishedVersionChanged( + "Published exam target profile versions are immutable; create a new version instead."); + ThrowIfPublishedVersionChanged( + "Published business target policy versions are immutable; create a new version instead."); + ThrowIfPublishedVersionChanged( + "Published assessment policy versions are immutable; create a new version instead."); + ThrowIfPublishedVersionChanged( + "Published exam paper specification versions are immutable; create a new version instead."); + ThrowIfPublishedManifestChanged(); + ThrowIfChanged("Published release questions are immutable; publish a new release instead."); + var changedAnswerAttempt = ChangeTracker.Entries() .FirstOrDefault(entry => entry.State is EntityState.Modified or EntityState.Deleted); if (changedAnswerAttempt is not null) @@ -165,4 +181,32 @@ public sealed partial class TikuDbContext( if (entry.State is EntityState.Added or EntityState.Modified) entry.Entity.UpdatedAt = now; } } + + private void ThrowIfChanged(string message) + where TEntity : class + { + if (ChangeTracker.Entries().Any(entry => + entry.State is EntityState.Modified or EntityState.Deleted)) + throw new InvalidOperationException(message); + } + + private void ThrowIfPublishedVersionChanged(string message) + where TEntity : class + { + if (ChangeTracker.Entries().Any(entry => + (entry.State is EntityState.Modified or EntityState.Deleted) && + entry.OriginalValues.GetValue("Status") == + ContentDefinitionStatus.Published)) + throw new InvalidOperationException(message); + } + + private void ThrowIfPublishedManifestChanged() + { + if (ChangeTracker.Entries().Any(entry => + (entry.State is EntityState.Modified or EntityState.Deleted) && + entry.OriginalValues.GetValue(nameof(ProductAccessManifestVersion.Status)) == + AccessManifestStatus.Published)) + throw new InvalidOperationException( + "Published product access manifest versions are immutable; create a new version instead."); + } } diff --git a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Assets/PlatformQuestionAssetFoundation.cs b/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Assets/PlatformQuestionAssetFoundation.cs deleted file mode 100644 index 16e7286..0000000 --- a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Assets/PlatformQuestionAssetFoundation.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -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; -using Tiku.Domain.Content; -using Tiku.Domain.Operations; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.Infrastructure.PlatformAdmin; - -internal abstract partial class PlatformQuestionBankServiceBase -{ - protected Task SignQuestionAssetUploadCoreAsync(PlatformAdminActor actor, - AssetUploadSignCommand command, CancellationToken cancellationToken = default) - { - return ExecuteAsync(actor, "签发公共题库图片上传地址", async (provider, _, tenantId, token) => - await provider.GetRequiredService().SignUploadAsync( - new AssetManagementActor(tenantId, actor.UserId), - command with { AssetType = "image", Category = "question", IsPublic = true }, token), - cancellationToken); - } - - protected Task ConfirmQuestionAssetUploadCoreAsync(PlatformAdminActor actor, - AssetUploadConfirmCommand command, CancellationToken cancellationToken = default) - { - return ExecuteAsync(actor, "确认公共题库图片上传", async (provider, _, tenantId, token) => - await provider.GetRequiredService() - .ConfirmUploadAsync(new AssetManagementActor(tenantId, actor.UserId), command, token), - cancellationToken); - } - -} diff --git a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Assets/PlatformQuestionAssetService.cs b/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Assets/PlatformQuestionAssetService.cs deleted file mode 100644 index 90cdb24..0000000 --- a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Assets/PlatformQuestionAssetService.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Tiku.Application.Assets; -using Tiku.Application.PlatformAdmin; -using Tiku.Application.Security; -using Tiku.Application.Tenancy; - -namespace Tiku.Infrastructure.PlatformAdmin; - -internal sealed class PlatformQuestionAssetService( - ICurrentAccessContext currentAccessContext, - ITenantExecutionScope tenantExecutionScope) - : PlatformQuestionBankServiceBase(currentAccessContext, tenantExecutionScope), IPlatformQuestionAssetService -{ - public Task SignQuestionAssetUploadAsync( - PlatformAdminActor actor, - AssetUploadSignCommand command, - CancellationToken cancellationToken = default) => - SignQuestionAssetUploadCoreAsync(actor, command, cancellationToken); - - public Task ConfirmQuestionAssetUploadAsync( - PlatformAdminActor actor, - AssetUploadConfirmCommand command, - CancellationToken cancellationToken = default) => - ConfirmQuestionAssetUploadCoreAsync(actor, command, cancellationToken); -} diff --git a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Catalog/PlatformQuestionBankCatalogFoundation.cs b/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Catalog/PlatformQuestionBankCatalogFoundation.cs deleted file mode 100644 index 23e0ec7..0000000 --- a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Catalog/PlatformQuestionBankCatalogFoundation.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -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; -using Tiku.Domain.Content; -using Tiku.Domain.Operations; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.Infrastructure.PlatformAdmin; - -internal abstract partial class PlatformQuestionBankServiceBase -{ - protected Task> GetBanksCoreAsync( - PlatformAdminActor actor, - PlatformQuestionBankFilter filter, - CancellationToken cancellationToken = default) - { - return ExecuteAsync>(actor, "查询平台公共题库", - async (_, dbContext, tenantId, token) => - { - var query = dbContext.QuestionBanks.AsNoTracking().Where(item => item.TenantId == tenantId); - if (!string.IsNullOrWhiteSpace(filter.Keyword)) - { - var keyword = filter.Keyword.Trim(); - query = query.Where(item => item.Name.Contains(keyword)); - } - - if (!string.IsNullOrWhiteSpace(filter.Status) && - !filter.Status.Equals("all", StringComparison.OrdinalIgnoreCase)) - { - var status = ParseBankStatus(filter.Status); - query = query.Where(item => item.Status == status); - } - - var banks = await query.OrderBy(item => item.Name).ThenBy(item => item.CreatedAt).ToArrayAsync(token); - var entryIds = banks.Where(item => item.ContentEntryId.HasValue) - .Select(item => item.ContentEntryId!.Value).ToArray(); - var nodeCounts = await dbContext.ContentNodes.AsNoTracking() - .Where(item => item.TenantId == tenantId && entryIds.Contains(item.EntryId) && item.IsActive) - .GroupBy(item => item.EntryId) - .Select(group => new { EntryId = group.Key, Count = group.Count() }) - .ToDictionaryAsync(item => item.EntryId, item => item.Count, token); - var questionCounts = await dbContext.Questions.AsNoTracking() - .Where(item => - item.TenantId == tenantId && item.QuestionBankId.HasValue && - item.Status != QuestionStatus.Archived) - .GroupBy(item => item.QuestionBankId!.Value) - .Select(group => new { BankId = group.Key, Count = group.Count() }) - .ToDictionaryAsync(item => item.BankId, item => item.Count, token); - return banks.Select(item => ToBankItem( - item, - item.ContentEntryId.HasValue && nodeCounts.TryGetValue(item.ContentEntryId.Value, out var nodes) - ? nodes - : 0, - questionCounts.TryGetValue(item.Id, out var questions) ? questions : 0)).ToArray(); - }, cancellationToken); - } - - protected Task UpsertBankCoreAsync( - PlatformAdminActor actor, - UpsertPlatformQuestionBankCommand command, - CancellationToken cancellationToken = default) - { - return ExecuteAsync(actor, "保存平台公共题库", async (_, dbContext, tenantId, token) => - { - if (string.IsNullOrWhiteSpace(command.Name)) throw Error("题库名称不能为空。", "question_bank_name_required"); - - var bank = command.Id.HasValue - ? await dbContext.QuestionBanks.SingleOrDefaultAsync( - item => item.TenantId == tenantId && item.Id == command.Id.Value, token) - : null; - if (command.Id.HasValue && bank is null) throw Error("公共题库不存在。", "question_bank_not_found"); - - ContentEntry? trackedEntry = null; - if (bank is null) - { - var entry = new ContentEntry - { - TenantId = tenantId, - EntryKey = $"public-question-bank-{Guid.NewGuid():N}", - Name = command.Name.Trim(), - EntryType = ContentEntryType.QuestionPractice, - Visibility = ContentVisibility.Public, - IsActive = true, - CreatedBy = actor.UserId, - UpdatedBy = actor.UserId - }; - trackedEntry = entry; - bank = new QuestionBank - { - TenantId = tenantId, - ContentEntryId = entry.Id, - Status = QuestionBankStatus.Active - }; - dbContext.ContentEntries.Add(entry); - dbContext.QuestionBanks.Add(bank); - } - else if (!bank.ContentEntryId.HasValue) - { - var entry = new ContentEntry - { - TenantId = tenantId, - EntryKey = $"public-question-bank-{bank.Id:N}", - Name = command.Name.Trim(), - EntryType = ContentEntryType.QuestionPractice, - Visibility = ContentVisibility.Public, - IsActive = true, - CreatedBy = actor.UserId, - UpdatedBy = actor.UserId - }; - trackedEntry = entry; - dbContext.ContentEntries.Add(entry); - bank.ContentEntryId = entry.Id; - } - - bank.Name = command.Name.Trim(); - bank.Metadata = ObjectOrDefault(command.Metadata); - if (bank.ContentEntryId.HasValue) - { - var entry = trackedEntry ?? - await dbContext.ContentEntries.SingleAsync( - item => item.TenantId == tenantId && item.Id == bank.ContentEntryId.Value, token); - entry.Name = bank.Name; - entry.UpdatedBy = actor.UserId; - } - - AddAudit(dbContext, actor, "platform.question_bank.saved", bank.Id, new { bank.Name }); - await dbContext.SaveChangesAsync(token); - return ToBankItem(bank, 0, 0); - }, cancellationToken); - } - - protected Task ArchiveBankCoreAsync( - PlatformAdminActor actor, - Guid bankId, - CancellationToken cancellationToken = default) - { - return ExecuteAsync(actor, "归档平台公共题库", async (_, dbContext, tenantId, token) => - { - var bank = await RequireBankAsync(dbContext, tenantId, bankId, token); - if (await dbContext.Questions.AnyAsync( - item => item.TenantId == tenantId && item.QuestionBankId == bankId && - item.Status != QuestionStatus.Archived, token)) - throw Error("题库中仍有未归档题目,不能归档题库。", "question_bank_not_empty"); - - if (bank.ContentEntryId.HasValue && await dbContext.ContentNodes.AnyAsync( - item => item.TenantId == tenantId && item.EntryId == bank.ContentEntryId && item.IsActive, token)) - throw Error("题库中仍有启用的内容层级,不能归档题库。", "question_bank_nodes_not_archived"); - - bank.Status = QuestionBankStatus.Archived; - AddAudit(dbContext, actor, "platform.question_bank.archived", bank.Id, new { bank.Name }); - await dbContext.SaveChangesAsync(token); - return ToBankItem(bank, 0, 0); - }, cancellationToken); - } - -} diff --git a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Catalog/PlatformQuestionBankCatalogService.cs b/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Catalog/PlatformQuestionBankCatalogService.cs deleted file mode 100644 index c7a7f74..0000000 --- a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Catalog/PlatformQuestionBankCatalogService.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Tiku.Application.PlatformAdmin; -using Tiku.Application.Security; -using Tiku.Application.Tenancy; - -namespace Tiku.Infrastructure.PlatformAdmin; - -internal sealed class PlatformQuestionBankCatalogService( - ICurrentAccessContext currentAccessContext, - ITenantExecutionScope tenantExecutionScope) - : PlatformQuestionBankServiceBase(currentAccessContext, tenantExecutionScope), IPlatformQuestionBankCatalogService -{ - public Task> GetBanksAsync( - PlatformAdminActor actor, - PlatformQuestionBankFilter filter, - CancellationToken cancellationToken = default) => - GetBanksCoreAsync(actor, filter, cancellationToken); - - public Task UpsertBankAsync( - PlatformAdminActor actor, - UpsertPlatformQuestionBankCommand command, - CancellationToken cancellationToken = default) => - UpsertBankCoreAsync(actor, command, cancellationToken); - - public Task ArchiveBankAsync( - PlatformAdminActor actor, - Guid bankId, - CancellationToken cancellationToken = default) => - ArchiveBankCoreAsync(actor, bankId, cancellationToken); -} diff --git a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Foundation/PlatformQuestionBankFoundation.cs b/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Foundation/PlatformQuestionBankFoundation.cs deleted file mode 100644 index 98c8fc2..0000000 --- a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Foundation/PlatformQuestionBankFoundation.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -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; -using Tiku.Domain.Content; -using Tiku.Domain.Operations; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.Infrastructure.PlatformAdmin; - -internal abstract partial class PlatformQuestionBankServiceBase( - ICurrentAccessContext currentAccessContext, - ITenantExecutionScope tenantExecutionScope) -{ - private const int MaxPageSize = 100; - - private static readonly HashSet SupportedQuestionTypes = new(StringComparer.OrdinalIgnoreCase) - { - "choice", "multiple_choice", "true_false", "fill_blank", "short_answer", "reading", "programming" - }; - - private async Task ExecuteAsync(PlatformAdminActor actor, string reason, - Func> action, - CancellationToken cancellationToken) - { - var access = await currentAccessContext.GetAsync(cancellationToken); - if (access.UserId != actor.UserId || - !access.HasPlatformPermission(BackendPermissions.PlatformQuestionBankManage)) - throw Error("需要平台公共题库管理权限。", "platform_question_bank_access_denied"); - var correlationId = Guid.NewGuid().ToString("N"); - var platformTenantId = await tenantExecutionScope.ExecuteAsync( - new SystemScopeRequest(null, SystemScopeCallerType.Platform, "PlatformQuestionBankService", - "解析平台公共题库所属租户", correlationId, true), - async (provider, token) => - { - var dbContext = provider.GetRequiredService(); - var tenantIds = await dbContext.Tenants.AsNoTracking() - .Where(item => item.Mode == TenantMode.PlatformOwned).Select(item => item.Id).Take(2) - .ToArrayAsync(token); - if (tenantIds.Length != 1) - throw Error(tenantIds.Length == 0 ? "平台内容所属租户尚未初始化,请先运行数据库迁移器。" : "检测到多个平台内容所属租户,请先修复数据。", - tenantIds.Length == 0 - ? "platform_question_owner_missing" - : "platform_question_owner_ambiguous"); - return tenantIds[0]; - }, cancellationToken); - return await tenantExecutionScope.ExecuteAsync( - new SystemScopeRequest(platformTenantId, SystemScopeCallerType.Platform, - "PlatformQuestionBankService", reason, correlationId), - async (provider, token) => - { - var dbContext = provider.GetRequiredService(); - return await action(provider, dbContext, platformTenantId, token); - }, cancellationToken); - } - - private static async Task RequireBankAsync(IPlatformQuestionBankAdministrationPersistence dbContext, Guid tenantId, Guid bankId, - CancellationToken token) - { - return await dbContext.QuestionBanks.SingleOrDefaultAsync( - item => item.TenantId == tenantId && item.Id == bankId, token) ?? - throw Error("公共题库不存在。", "question_bank_not_found"); - } - - private static async Task RequireBankWithEntryAsync(IPlatformQuestionBankAdministrationPersistence dbContext, Guid tenantId, - Guid bankId, CancellationToken token) - { - var bank = await RequireBankAsync(dbContext, tenantId, bankId, token); - if (!bank.ContentEntryId.HasValue) throw Error("题库内容入口尚未初始化,请先编辑并保存题库。", "question_bank_entry_missing"); - return bank; - } - - private static async Task RequireNodeAsync(IPlatformQuestionBankAdministrationPersistence dbContext, Guid tenantId, QuestionBank bank, - Guid nodeId, CancellationToken token) - { - return await dbContext.ContentNodes.SingleOrDefaultAsync( - item => item.TenantId == tenantId && item.EntryId == bank.ContentEntryId && item.Id == nodeId && - item.IsActive, token) ?? throw Error("所选内容节点不存在或已归档。", "question_bank_node_not_found"); - } - - private static PlatformQuestionBankItem ToBankItem(QuestionBank item, int nodeCount, int questionCount) - { - return new PlatformQuestionBankItem(item.Id, item.ContentEntryId, item.Name, item.Status, nodeCount, - questionCount, item.Metadata, - item.CreatedAt, item.UpdatedAt); - } - - private static PlatformQuestionBankNodeItem ToNodeItem(ContentNode item, Guid bankId, int questionCount) - { - return new PlatformQuestionBankNodeItem(item.Id, bankId, item.EntryId, item.ParentId, item.NodeKey, item.Name, - item.NodeType, item.Depth, - item.SortOrder, item.IsActive, item.IsSelectable, item.IsLeaf, questionCount, item.Metadata); - } - - private static PlatformQuestionItem ToQuestionItem(Question item, QuestionVersion? version) - { - return new PlatformQuestionItem(item.Id, version?.Id, item.QuestionBankId!.Value, item.EntryId!.Value, - item.ContentNodeId!.Value, - item.LegacyId, item.Type, item.TypeLabel, item.Difficulty, item.Tags, version?.Content, - version?.Options ?? JsonDefaults.Array(), version?.CorrectOptionIndex, - version?.CorrectOptionIndices ?? JsonDefaults.Array(), version?.AnswerText, version?.Explanation, - version?.SubQuestions ?? JsonDefaults.Array(), version?.CodeLang, version?.CodeTemplate, item.MediaUrl, - item.Status, version?.VersionNo ?? 0, item.CreatedAt, item.UpdatedAt); - } - - private static async Task LoadImportDetailAsync(IPlatformQuestionBankAdministrationPersistence dbContext, Guid tenantId, - Guid jobId, CancellationToken token) - { - var job = await dbContext.ContentImportJobs.AsNoTracking() - .SingleOrDefaultAsync(item => item.TenantId == tenantId && item.Id == jobId, token) ?? - throw Error("导入任务不存在。", "question_import_not_found"); - var items = await dbContext.ContentImportItems.AsNoTracking() - .Where(item => item.TenantId == tenantId && item.JobId == jobId).OrderBy(item => item.RowNo) - .Select(item => ToImportItem(item)).ToArrayAsync(token); - var issues = await dbContext.ContentImportIssues.AsNoTracking() - .Where(item => item.TenantId == tenantId && item.JobId == jobId).OrderBy(item => item.RowNo) - .Select(item => ToIssueItem(item)).ToArrayAsync(token); - return new ContentImportJobDetail(ToJobItem(job), items, issues); - } - - private static ContentImportJobItem ToJobItem(ContentImportJob item) - { - return new ContentImportJobItem(item.Id, item.TargetRegionId, item.TargetSubjectId, item.TargetCategoryId, - item.TargetContentNodeId, - item.TargetQuestionBankId, item.ImportType, item.SourceFormat, item.Status, item.SourceName, - item.SourceHash, item.DryRun, item.TotalCount, item.ValidCount, item.ErrorCount, item.WarningCount, - item.InsertedCount, item.UpdatedCount, item.SkippedCount, item.Summary, item.ErrorMessage, item.StartedAt, - item.FinishedAt, item.CreatedAt, item.UpdatedAt); - } - - private static ContentImportItemModel ToImportItem(ContentImportItem item) - { - return new ContentImportItemModel(item.Id, item.JobId, item.RowNo, item.ExternalId, item.Status, - item.TargetType, item.TargetId, - item.SourcePayload, item.NormalizedPayload, item.ContentHash, item.IssuesCount); - } - - private static ContentImportIssueModel ToIssueItem(ContentImportIssue item) - { - return new ContentImportIssueModel(item.Id, item.JobId, item.ItemId, item.RowNo, item.Severity, item.Code, - item.FieldPath, item.Message, - item.Details); - } - - private static ContentImportIssue NewIssue(Guid tenantId, Guid jobId, Guid itemId, int rowNo, string code, - string field, string message) - { - return new ContentImportIssue - { - TenantId = tenantId, - JobId = jobId, - ItemId = itemId, - RowNo = rowNo, - Severity = ImportIssueSeverity.Error, - Code = code, - FieldPath = field, - Message = message - }; - } - - private static void AddAudit(IPlatformQuestionBankAdministrationPersistence dbContext, PlatformAdminActor actor, string action, Guid targetId, - object details) - { - dbContext.AuditLogs.Add(new AuditLog - { - ActorUserId = actor.UserId, - Action = action, - TargetType = "question_bank", - TargetId = targetId.ToString("N"), - Details = JsonSerializer.SerializeToElement(details) - }); - } - - private static PlatformAdminException Error(string message, string code) - { - return new PlatformAdminException(message, code); - } - - private static string NormalizeImportFormat(string? value) - { - return value?.Trim().ToLowerInvariant() switch - { - "simple" or "json" => "simple", - "structured-v2" or "structured" or "v2" => "structured", - _ => throw Error("导入格式不受支持。", "question_import_format_invalid") - }; - } - - private static QuestionBankStatus ParseBankStatus(string? value) - { - return value?.Trim().ToLowerInvariant() switch - { - "archived" or "已归档" => QuestionBankStatus.Archived, - _ => QuestionBankStatus.Active - }; - } - - private static QuestionStatus ParseQuestionStatus(string? value) - { - return value?.Trim().ToLowerInvariant() switch - { - "draft" or "草稿" => QuestionStatus.Draft, - "archived" or "已归档" => QuestionStatus.Archived, - _ => QuestionStatus.Published - }; - } - - private static ContentNodeType ParseNodeType(string? value, ContentNodeType fallback) - { - return value?.Trim().ToLowerInvariant() switch - { - "subject" => ContentNodeType.Subject, - "chapter" => ContentNodeType.Chapter, - "paper" => ContentNodeType.Paper, - "category" => ContentNodeType.Category, - _ => fallback - }; - } - - private static string? Normalize(string? value) - { - return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); - } - - private static JsonElement ObjectOrDefault(JsonElement value) - { - return value.ValueKind == JsonValueKind.Object ? value.Clone() : JsonDefaults.Object(); - } - - private static JsonElement ArrayOrDefault(JsonElement value) - { - return value.ValueKind == JsonValueKind.Array ? value.Clone() : JsonDefaults.Array(); - } - - private static string Hash(string value) - { - return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant(); - } - - private static string? GetString(JsonElement value, string name) - { - return value.ValueKind == JsonValueKind.Object && value.TryGetProperty(name, out var property) && - property.ValueKind == JsonValueKind.String - ? property.GetString() - : null; - } - - private static int? GetInt(JsonElement value, string name) - { - return value.ValueKind == JsonValueKind.Object && value.TryGetProperty(name, out var property) && - property.TryGetInt32(out var result) - ? result - : null; - } - - private static JsonElement GetElement(JsonElement value, string name, JsonElement fallback) - { - return value.ValueKind == JsonValueKind.Object && value.TryGetProperty(name, out var property) - ? property.Clone() - : fallback; - } - - private sealed record ImportPathPart(string Key, string Name, ContentNodeType Type, int Order); - - private sealed record ImportRow( - int RowNo, - JsonElement Question, - IReadOnlyCollection Path, - Guid? TargetNodeId); -} diff --git a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Imports/PlatformQuestionImportFoundation.cs b/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Imports/PlatformQuestionImportFoundation.cs deleted file mode 100644 index 7c28df6..0000000 --- a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Imports/PlatformQuestionImportFoundation.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -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; -using Tiku.Domain.Content; -using Tiku.Domain.Operations; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.Infrastructure.PlatformAdmin; - -internal abstract partial class PlatformQuestionBankServiceBase -{ - protected Task PreviewImportCoreAsync(PlatformAdminActor actor, - PlatformQuestionImportCommand command, CancellationToken cancellationToken = default) - { - return ImportAsync(actor, command, false, cancellationToken); - } - - protected Task ExecuteImportCoreAsync(PlatformAdminActor actor, - PlatformQuestionImportCommand command, CancellationToken cancellationToken = default) - { - return ImportAsync(actor, command, true, cancellationToken); - } - - protected Task GetImportCoreAsync(PlatformAdminActor actor, Guid jobId, - CancellationToken cancellationToken = default) - { - return ExecuteAsync(actor, "查询公共题库导入结果", - async (_, dbContext, tenantId, token) => await LoadImportDetailAsync(dbContext, tenantId, jobId, token), - cancellationToken); - } - - private async Task ImportAsync(PlatformAdminActor actor, - PlatformQuestionImportCommand command, bool execute, CancellationToken cancellationToken) - { - return await ExecuteAsync(actor, execute ? "执行公共题库导入" : "预检公共题库导入", async (_, dbContext, tenantId, token) => - { - var bank = await RequireBankWithEntryAsync(dbContext, tenantId, command.QuestionBankId, token); - var format = NormalizeImportFormat(command.Format); - ContentNode? targetNode = null; - if (command.ContentNodeId.HasValue) - targetNode = await RequireNodeAsync(dbContext, tenantId, bank, command.ContentNodeId.Value, token); - if (format == "simple" && targetNode is null) - throw Error("普通批量导入必须选择章节或试卷。", "import_target_node_required"); - - var job = new ContentImportJob - { - TenantId = tenantId, - CreatedBy = actor.UserId, - TargetQuestionBankId = bank.Id, - TargetContentNodeId = targetNode?.Id, - ImportType = ContentImportType.Questions, - SourceFormat = ImportSourceFormat.Json, - Status = execute ? ContentImportStatus.Importing : ContentImportStatus.Preview, - SourceName = string.IsNullOrWhiteSpace(command.SourceName) ? null : command.SourceName.Trim(), - SourceHash = Hash(command.Payload.GetRawText()), - DryRun = !execute, - RawPayload = command.Payload.Clone(), - StartedAt = execute ? DateTimeOffset.UtcNow : null - }; - if (execute) dbContext.ContentImportJobs.Add(job); - - var rows = format == "simple" - ? ExtractSimpleRows(command.Payload, targetNode!.Id) - : ExtractStructuredRows(command.Payload, targetNode?.Id); - job.TotalCount = rows.Count; - var importItems = new List(); - var issues = new List(); - var createdNodes = 0; - var inserted = 0; - var updated = 0; - var skipped = 0; - var nodeCache = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var row in rows) - { - var item = new ContentImportItem - { - TenantId = tenantId, - JobId = job.Id, - RowNo = row.RowNo, - ExternalId = GetString(row.Question, "legacyId") ?? GetString(row.Question, "id"), - SourcePayload = row.Question.Clone(), - NormalizedPayload = row.Question.Clone() - }; - var validation = ValidateImportRow(row.Question); - if (validation is not null) - { - item.Status = ContentImportItemStatus.Invalid; - item.IssuesCount = 1; - issues.Add(NewIssue(tenantId, job.Id, item.Id, row.RowNo, validation.Value.Code, - validation.Value.Field, validation.Value.Message)); - job.ErrorCount++; - importItems.Add(item); - continue; - } - - job.ValidCount++; - item.Status = ContentImportItemStatus.Valid; - if (execute) - { - var node = targetNode; - if (format == "structured") - { - (node, var made) = await EnsureStructuredPathAsync(dbContext, actor, tenantId, bank, row.Path, - targetNode, nodeCache, token); - createdNodes += made; - } - - if (node is null) throw Error("导入题目没有可用的目标章节。", "import_target_node_required"); - var result = - await UpsertImportedQuestionAsync(dbContext, actor, tenantId, bank, node, row.Question, token); - item.TargetType = "question"; - item.TargetId = result.Question.Id; - item.ContentHash = result.SourceHash; - item.Status = result.Action switch - { - "inserted" => ContentImportItemStatus.Inserted, - "updated" => ContentImportItemStatus.Updated, - _ => ContentImportItemStatus.Skipped - }; - if (result.Action == "inserted") inserted++; - else if (result.Action == "updated") updated++; - else skipped++; - } - - importItems.Add(item); - } - - job.InsertedCount = inserted; - job.UpdatedCount = updated; - job.SkippedCount = skipped; - job.Status = !execute ? ContentImportStatus.Preview : - job.ErrorCount > 0 ? ContentImportStatus.CompletedWithErrors : ContentImportStatus.Completed; - job.FinishedAt = execute ? DateTimeOffset.UtcNow : null; - job.Summary = JsonSerializer.SerializeToElement(new - { format, createdNodes, inserted, updated, skipped, invalid = job.ErrorCount }); - job.NormalizedPayload = JsonSerializer.SerializeToElement(rows.Select(item => item.Question)); - if (execute) - { - dbContext.ContentImportItems.AddRange(importItems); - dbContext.ContentImportIssues.AddRange(issues); - AddAudit(dbContext, actor, "platform.question_bank.import_executed", job.Id, - new { bankId = bank.Id, format, job.TotalCount, job.ErrorCount }); - await dbContext.SaveChangesAsync(token); - } - - var detail = new ContentImportJobDetail(ToJobItem(job), importItems.Select(ToImportItem).ToArray(), - issues.Select(ToIssueItem).ToArray()); - return new PlatformQuestionImportResult(detail, createdNodes, inserted, updated, skipped); - }, cancellationToken); - } - - private static async Task<(ContentNode Node, int Created)> EnsureStructuredPathAsync(IPlatformQuestionBankAdministrationPersistence dbContext, - PlatformAdminActor actor, Guid tenantId, QuestionBank bank, IReadOnlyCollection path, - ContentNode? root, Dictionary cache, CancellationToken token) - { - var parent = root; - var created = 0; - foreach (var part in path) - { - var cacheKey = $"{parent?.Id:N}/{part.Key}"; - if (!cache.TryGetValue(cacheKey, out var node)) - { - var parentId = parent?.Id; - node = await dbContext.ContentNodes.SingleOrDefaultAsync( - item => item.TenantId == tenantId && item.EntryId == bank.ContentEntryId && - item.ParentId == parentId && item.NodeKey == part.Key, token); - if (node is null) - { - node = new ContentNode - { - TenantId = tenantId, - EntryId = bank.ContentEntryId!.Value, - ParentId = parent?.Id, - NodeKey = part.Key, - Name = part.Name, - NodeType = part.Type, - Depth = parent is null ? 0 : parent.Depth + 1, - Path = parent is null ? null : $"{parent.Path}.n{Guid.NewGuid():N}", - SortOrder = part.Order, - IsActive = true, - IsSelectable = true, - IsLeaf = part.Type is ContentNodeType.Chapter or ContentNodeType.Paper, - CreatedBy = actor.UserId, - UpdatedBy = actor.UserId - }; - node.Path = parent is null ? $"n{node.Id:N}" : $"{parent.Path}.n{node.Id:N}"; - dbContext.ContentNodes.Add(node); - await dbContext.SaveChangesAsync(token); - created++; - } - - cache[cacheKey] = node; - } - - parent = node; - } - - if (parent is null) throw Error("结构化导入未包含可用的章节或试卷。", "structured_import_path_required"); - return (parent, created); - } - - private static async Task<(Question Question, string Action, string SourceHash)> UpsertImportedQuestionAsync( - IPlatformQuestionBankAdministrationPersistence dbContext, PlatformAdminActor actor, Guid tenantId, QuestionBank bank, ContentNode node, - JsonElement payload, CancellationToken token) - { - var legacyId = GetString(payload, "legacyId") ?? GetString(payload, "id"); - var sourceHash = GetString(payload, "sourceHash") ?? Hash(payload.GetRawText()); - var question = !string.IsNullOrWhiteSpace(legacyId) - ? await dbContext.Questions.SingleOrDefaultAsync( - item => item.TenantId == tenantId && item.LegacyId == legacyId, token) - : await dbContext.Questions.Where(item => item.TenantId == tenantId && item.QuestionBankId == bank.Id) - .Join( - dbContext.QuestionVersions.Where(item => - item.TenantId == tenantId && item.SourceHash == sourceHash), item => item.CurrentVersionId, - version => version.Id, (item, _) => item) - .SingleOrDefaultAsync(token); - if (question is not null && question.QuestionBankId != bank.Id) - throw Error("题目稳定编号已被其他题库使用。", "question_legacy_id_conflict"); - if (question?.CurrentVersionId is { } currentId && await dbContext.QuestionVersions.AnyAsync( - item => item.TenantId == tenantId && item.Id == currentId && item.SourceHash == sourceHash, token)) - return (question, "skipped", sourceHash); - var isNew = question is null; - question ??= new Question - { TenantId = tenantId, QuestionBankId = bank.Id, EntryId = bank.ContentEntryId, LegacyId = legacyId }; - if (isNew) dbContext.Questions.Add(question); - var command = FromImportPayload(bank.Id, node.Id, payload, legacyId, sourceHash); - ApplyQuestion(question, command, bank.ContentEntryId!.Value, node.Id); - await dbContext.SaveChangesAsync(token); - var nextVersion = await dbContext.QuestionVersions - .Where(item => item.TenantId == tenantId && item.QuestionId == question.Id) - .Select(item => (int?)item.VersionNo).MaxAsync(token) ?? 0; - var version = BuildVersion(actor, tenantId, question.Id, nextVersion + 1, command); - dbContext.QuestionVersions.Add(version); - question.CurrentVersionId = version.Id; - await dbContext.SaveChangesAsync(token); - return (question, isNew ? "inserted" : "updated", sourceHash); - } - - private static List ExtractSimpleRows(JsonElement payload, Guid nodeId) - { - var array = payload.ValueKind == JsonValueKind.Array - ? payload - : - payload.ValueKind == JsonValueKind.Object && payload.TryGetProperty("questions", out var questions) - ? - questions - : default; - if (array.ValueKind != JsonValueKind.Array) - throw Error("普通导入内容必须是题目数组,或包含 questions 数组。", "import_payload_invalid"); - return array.EnumerateArray().Select((item, index) => new ImportRow(index + 1, item.Clone(), [], nodeId)) - .ToList(); - } - - private static List ExtractStructuredRows(JsonElement payload, Guid? rootNodeId) - { - if (payload.ValueKind != JsonValueKind.Object) - throw Error("结构化导入内容必须是 JSON 对象。", "structured_import_payload_invalid"); - if (payload.TryGetProperty("_tikuExport", out var marker) && marker.GetString() != "2.0") - throw Error("仅支持 2.0 结构化题库文件。", "structured_import_version_invalid"); - var rows = new List(); - WalkStructured(payload, [], rows, rootNodeId); - if (rows.Count == 0) throw Error("结构化文件中没有找到题目。", "structured_import_questions_empty"); - return rows; - } - - private static void WalkStructured(JsonElement current, List path, List rows, - Guid? rootNodeId) - { - if (current.ValueKind != JsonValueKind.Object) return; - if (current.TryGetProperty("questions", out var questions) && questions.ValueKind == JsonValueKind.Array) - foreach (var question in questions.EnumerateArray()) - rows.Add(new ImportRow(rows.Count + 1, question.Clone(), path.ToArray(), rootNodeId)); - string[] childProperties = ["categories", "children", "subjects", "chapters", "papers", "nodes"]; - foreach (var property in childProperties) - { - if (!current.TryGetProperty(property, out var children) || - children.ValueKind != JsonValueKind.Array) continue; - foreach (var child in children.EnumerateArray()) - { - if (child.ValueKind != JsonValueKind.Object) continue; - var name = GetString(child, "name") ?? GetString(child, "title") ?? "未命名节点"; - var key = GetString(child, "code") ?? GetString(child, "key") ?? - GetString(child, "id") ?? $"{property}-{Hash(name)[..12]}"; - var type = property switch - { - "subjects" => ContentNodeType.Subject, - "chapters" => ContentNodeType.Chapter, - "papers" => ContentNodeType.Paper, - _ => ParseNodeType(GetString(child, "type"), ContentNodeType.Category) - }; - var next = new List(path) - { new(key, name, type, GetInt(child, "order") ?? path.Count) }; - WalkStructured(child, next, rows, rootNodeId); - } - } - } - - private static (string Code, string Field, string Message)? ValidateImportRow(JsonElement payload) - { - if (payload.ValueKind != JsonValueKind.Object) return ("question_payload_invalid", "$", "题目必须是 JSON 对象。"); - if (string.IsNullOrWhiteSpace(GetString(payload, "content")) && - string.IsNullOrWhiteSpace(GetString(payload, "title"))) - return ("question_content_required", "content", "题干不能为空。"); - var type = GetString(payload, "type") ?? "choice"; - if (!SupportedQuestionTypes.Contains(type)) return ("question_type_invalid", "type", $"不支持的题型:{type}。"); - return null; - } - - private static UpsertPlatformQuestionCommand FromImportPayload(Guid bankId, Guid nodeId, JsonElement payload, - string? legacyId, string sourceHash) - { - return new UpsertPlatformQuestionCommand( - null, bankId, nodeId, legacyId, GetString(payload, "type") ?? "choice", GetString(payload, "typeLabel"), - GetInt(payload, "difficulty"), - GetElement(payload, "tags", JsonDefaults.Array()), - GetString(payload, "content") ?? GetString(payload, "title"), - GetElement(payload, "options", JsonDefaults.Array()), - GetInt(payload, "correctOptionIndex"), GetElement(payload, "correctOptionIndices", JsonDefaults.Array()), - GetString(payload, "answerText") ?? GetString(payload, "answer"), - GetString(payload, "explanation"), GetElement(payload, "subQuestions", JsonDefaults.Array()), - GetString(payload, "codeLang"), GetString(payload, "codeTemplate"), - GetString(payload, "mediaUrl"), GetString(payload, "status") ?? "published", - GetElement(payload, "examMarkers", JsonDefaults.Object()), sourceHash); - } - -} diff --git a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Imports/PlatformQuestionImportService.cs b/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Imports/PlatformQuestionImportService.cs deleted file mode 100644 index 4b0c4b4..0000000 --- a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Imports/PlatformQuestionImportService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Tiku.Application.Assets; -using Tiku.Application.PlatformAdmin; -using Tiku.Application.Security; -using Tiku.Application.Tenancy; - -namespace Tiku.Infrastructure.PlatformAdmin; - -internal sealed class PlatformQuestionImportService( - ICurrentAccessContext currentAccessContext, - ITenantExecutionScope tenantExecutionScope) - : PlatformQuestionBankServiceBase(currentAccessContext, tenantExecutionScope), IPlatformQuestionImportService -{ - public Task PreviewImportAsync( - PlatformAdminActor actor, - PlatformQuestionImportCommand command, - CancellationToken cancellationToken = default) => - PreviewImportCoreAsync(actor, command, cancellationToken); - - public Task ExecuteImportAsync( - PlatformAdminActor actor, - PlatformQuestionImportCommand command, - CancellationToken cancellationToken = default) => - ExecuteImportCoreAsync(actor, command, cancellationToken); - - public Task GetImportAsync( - PlatformAdminActor actor, - Guid jobId, - CancellationToken cancellationToken = default) => - GetImportCoreAsync(actor, jobId, cancellationToken); -} diff --git a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Nodes/PlatformQuestionBankNodeFoundation.cs b/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Nodes/PlatformQuestionBankNodeFoundation.cs deleted file mode 100644 index 3298d74..0000000 --- a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Nodes/PlatformQuestionBankNodeFoundation.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -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; -using Tiku.Domain.Content; -using Tiku.Domain.Operations; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.Infrastructure.PlatformAdmin; - -internal abstract partial class PlatformQuestionBankServiceBase -{ - protected Task> GetNodesCoreAsync( - PlatformAdminActor actor, - Guid bankId, - CancellationToken cancellationToken = default) - { - return ExecuteAsync>(actor, "查询公共题库内容结构", - async (_, dbContext, tenantId, token) => - { - var bank = await RequireBankWithEntryAsync(dbContext, tenantId, bankId, token); - var nodes = await dbContext.ContentNodes.AsNoTracking() - .Where(item => item.TenantId == tenantId && item.EntryId == bank.ContentEntryId) - .OrderBy(item => item.Depth).ThenBy(item => item.SortOrder).ThenBy(item => item.Name) - .ToArrayAsync(token); - var counts = await dbContext.Questions.AsNoTracking() - .Where(item => - item.TenantId == tenantId && item.QuestionBankId == bankId && item.ContentNodeId.HasValue && - item.Status != QuestionStatus.Archived) - .GroupBy(item => item.ContentNodeId!.Value) - .Select(group => new { NodeId = group.Key, Count = group.Count() }) - .ToDictionaryAsync(item => item.NodeId, item => item.Count, token); - return nodes.Select(item => - ToNodeItem(item, bankId, counts.TryGetValue(item.Id, out var count) ? count : 0)).ToArray(); - }, cancellationToken); - } - - protected Task UpsertNodeCoreServiceAsync( - PlatformAdminActor actor, - UpsertPlatformQuestionBankNodeCommand command, - CancellationToken cancellationToken = default) - { - return ExecuteAsync(actor, "保存公共题库内容节点", async (_, dbContext, tenantId, token) => - { - var bank = await RequireBankWithEntryAsync(dbContext, tenantId, command.QuestionBankId, token); - return await UpsertNodeCoreAsync(dbContext, actor, tenantId, bank, command, token); - }, cancellationToken); - } - - protected Task> BatchCreateNodesCoreAsync( - PlatformAdminActor actor, - BatchCreatePlatformQuestionBankNodesCommand command, - CancellationToken cancellationToken = default) - { - return ExecuteAsync>(actor, "批量创建章节或试卷", - async (_, dbContext, tenantId, token) => - { - if (command.NodeType is not ContentNodeType.Chapter and not ContentNodeType.Paper) - throw Error("批量创建仅支持章节或试卷。", "node_batch_type_invalid"); - - var bank = await RequireBankWithEntryAsync(dbContext, tenantId, command.QuestionBankId, token); - var names = command.Names.Select(item => item.Trim()).Where(item => item.Length > 0) - .Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); - if (names.Length is 0 or > 100) throw Error("请提供 1 至 100 个不重复的名称。", "node_batch_names_invalid"); - - var result = new List(); - var order = 0; - foreach (var name in names) - result.Add(await UpsertNodeCoreAsync(dbContext, actor, tenantId, bank, - new UpsertPlatformQuestionBankNodeCommand( - null, command.QuestionBankId, command.ParentId, null, name, command.NodeType, order++, true, - JsonDefaults.Object()), token)); - - return result; - }, cancellationToken); - } - - protected Task ArchiveNodeCoreAsync( - PlatformAdminActor actor, - Guid nodeId, - CancellationToken cancellationToken = default) - { - return ExecuteAsync(actor, "归档公共题库内容节点", async (_, dbContext, tenantId, token) => - { - var node = await dbContext.ContentNodes.SingleOrDefaultAsync( - item => item.TenantId == tenantId && item.Id == nodeId, token) - ?? throw Error("内容节点不存在。", "question_bank_node_not_found"); - if (await dbContext.ContentNodes.AnyAsync( - item => item.TenantId == tenantId && item.ParentId == nodeId && item.IsActive, token)) - throw Error("该节点仍有启用的下级节点,不能归档。", "question_bank_node_has_children"); - - if (await dbContext.Questions.AnyAsync( - item => item.TenantId == tenantId && item.ContentNodeId == nodeId && - item.Status != QuestionStatus.Archived, token)) - throw Error("该节点仍有未归档题目,不能归档。", "question_bank_node_has_questions"); - - node.IsActive = false; - node.UpdatedBy = actor.UserId; - var bankId = await dbContext.QuestionBanks - .Where(item => item.TenantId == tenantId && item.ContentEntryId == node.EntryId).Select(item => item.Id) - .SingleAsync(token); - AddAudit(dbContext, actor, "platform.question_bank.node_archived", node.Id, new { node.Name }); - await dbContext.SaveChangesAsync(token); - return ToNodeItem(node, bankId, 0); - }, cancellationToken); - } - - private static async Task UpsertNodeCoreAsync(IPlatformQuestionBankAdministrationPersistence dbContext, - PlatformAdminActor actor, Guid tenantId, QuestionBank bank, UpsertPlatformQuestionBankNodeCommand command, - CancellationToken token) - { - if (string.IsNullOrWhiteSpace(command.Name)) throw Error("节点名称不能为空。", "question_bank_node_name_required"); - ContentNode? parent = null; - if (command.ParentId.HasValue) - parent = await RequireNodeAsync(dbContext, tenantId, bank, command.ParentId.Value, token); - var node = command.Id.HasValue - ? await dbContext.ContentNodes.SingleOrDefaultAsync( - item => item.TenantId == tenantId && item.Id == command.Id.Value && item.EntryId == bank.ContentEntryId, - token) - : null; - if (command.Id.HasValue && node is null) throw Error("内容节点不存在。", "question_bank_node_not_found"); - node ??= new ContentNode - { TenantId = tenantId, EntryId = bank.ContentEntryId!.Value, CreatedBy = actor.UserId }; - if (!command.Id.HasValue) dbContext.ContentNodes.Add(node); - node.ParentId = parent?.Id; - node.NodeKey = string.IsNullOrWhiteSpace(command.NodeKey) ? $"node-{node.Id:N}" : command.NodeKey.Trim(); - node.Name = command.Name.Trim(); - node.NodeType = command.NodeType; - node.Depth = parent is null ? 0 : parent.Depth + 1; - node.Path = parent is null ? $"n{node.Id:N}" : $"{parent.Path}.n{node.Id:N}"; - node.SortOrder = command.SortOrder; - node.IsActive = true; - node.IsSelectable = command.IsSelectable; - node.IsLeaf = command.NodeType is ContentNodeType.Chapter or ContentNodeType.Paper; - node.Metadata = ObjectOrDefault(command.Metadata); - node.UpdatedBy = actor.UserId; - AddAudit(dbContext, actor, "platform.question_bank.node_saved", node.Id, - new { bankId = bank.Id, node.Name, node.NodeType }); - await dbContext.SaveChangesAsync(token); - return ToNodeItem(node, bank.Id, 0); - } - -} diff --git a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Nodes/PlatformQuestionBankNodeService.cs b/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Nodes/PlatformQuestionBankNodeService.cs deleted file mode 100644 index 69152d3..0000000 --- a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Nodes/PlatformQuestionBankNodeService.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Tiku.Application.PlatformAdmin; -using Tiku.Application.Security; -using Tiku.Application.Tenancy; - -namespace Tiku.Infrastructure.PlatformAdmin; - -internal sealed class PlatformQuestionBankNodeService( - ICurrentAccessContext currentAccessContext, - ITenantExecutionScope tenantExecutionScope) - : PlatformQuestionBankServiceBase(currentAccessContext, tenantExecutionScope), IPlatformQuestionBankNodeService -{ - public Task> GetNodesAsync( - PlatformAdminActor actor, - Guid bankId, - CancellationToken cancellationToken = default) => - GetNodesCoreAsync(actor, bankId, cancellationToken); - - public Task UpsertNodeAsync( - PlatformAdminActor actor, - UpsertPlatformQuestionBankNodeCommand command, - CancellationToken cancellationToken = default) => - UpsertNodeCoreServiceAsync(actor, command, cancellationToken); - - public Task> BatchCreateNodesAsync( - PlatformAdminActor actor, - BatchCreatePlatformQuestionBankNodesCommand command, - CancellationToken cancellationToken = default) => - BatchCreateNodesCoreAsync(actor, command, cancellationToken); - - public Task ArchiveNodeAsync( - PlatformAdminActor actor, - Guid nodeId, - CancellationToken cancellationToken = default) => - ArchiveNodeCoreAsync(actor, nodeId, cancellationToken); -} diff --git a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Questions/PlatformQuestionAdministrationFoundation.cs b/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Questions/PlatformQuestionAdministrationFoundation.cs deleted file mode 100644 index 8ff0756..0000000 --- a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Questions/PlatformQuestionAdministrationFoundation.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -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; -using Tiku.Domain.Content; -using Tiku.Domain.Operations; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.Infrastructure.PlatformAdmin; - -internal abstract partial class PlatformQuestionBankServiceBase -{ - protected Task GetQuestionsCoreAsync( - PlatformAdminActor actor, - PlatformQuestionBankFilter filter, - CancellationToken cancellationToken = default) - { - return ExecuteAsync(actor, "查询公共题库题目", async (_, dbContext, tenantId, token) => - { - if (!filter.QuestionBankId.HasValue) throw Error("请选择公共题库。", "question_bank_id_required"); - - await RequireBankAsync(dbContext, tenantId, filter.QuestionBankId.Value, token); - var query = dbContext.Questions.AsNoTracking().Where(item => - item.TenantId == tenantId && item.QuestionBankId == filter.QuestionBankId); - if (filter.ContentNodeId.HasValue) query = query.Where(item => item.ContentNodeId == filter.ContentNodeId); - if (!string.IsNullOrWhiteSpace(filter.Type)) - { - var type = filter.Type.Trim(); - query = query.Where(item => item.Type == type); - } - - if (filter.Difficulty.HasValue) query = query.Where(item => item.Difficulty == filter.Difficulty); - if (!string.IsNullOrWhiteSpace(filter.Status) && - !filter.Status.Equals("all", StringComparison.OrdinalIgnoreCase)) - { - var status = ParseQuestionStatus(filter.Status); - query = query.Where(item => item.Status == status); - } - - if (!string.IsNullOrWhiteSpace(filter.Keyword)) - { - var keyword = filter.Keyword.Trim(); - query = query.Where(item => item.Type.Contains(keyword) || dbContext.QuestionVersions.Any(version => - version.TenantId == tenantId && version.QuestionId == item.Id && - version.Id == item.CurrentVersionId && version.Content != null && - version.Content.Contains(keyword))); - } - - var page = Math.Max(1, filter.Page); - var pageSize = Math.Clamp(filter.PageSize, 1, MaxPageSize); - var total = await query.CountAsync(token); - var questions = await query.OrderByDescending(item => item.UpdatedAt).Skip((page - 1) * pageSize) - .Take(pageSize).ToArrayAsync(token); - var versionIds = questions.Where(item => item.CurrentVersionId.HasValue) - .Select(item => item.CurrentVersionId!.Value).ToArray(); - var versions = await dbContext.QuestionVersions.AsNoTracking() - .Where(item => item.TenantId == tenantId && versionIds.Contains(item.Id)) - .ToDictionaryAsync(item => item.Id, token); - return new PlatformQuestionPage( - questions.Select(item => ToQuestionItem(item, - item.CurrentVersionId.HasValue && versions.TryGetValue(item.CurrentVersionId.Value, out var version) - ? version - : null)).ToArray(), total, page, pageSize); - }, cancellationToken); - } - - protected Task UpsertQuestionCoreAsync( - PlatformAdminActor actor, - UpsertPlatformQuestionCommand command, - CancellationToken cancellationToken = default) - { - return ExecuteAsync(actor, "保存公共题库题目", async (_, dbContext, tenantId, token) => - { - var bank = await RequireBankWithEntryAsync(dbContext, tenantId, command.QuestionBankId, token); - var node = await RequireNodeAsync(dbContext, tenantId, bank, command.ContentNodeId, token); - ValidateQuestion(command); - var question = command.Id.HasValue - ? await dbContext.Questions.SingleOrDefaultAsync( - item => item.TenantId == tenantId && item.Id == command.Id.Value && item.QuestionBankId == bank.Id, - token) - : null; - if (command.Id.HasValue && question is null) throw Error("题目不存在。", "question_not_found"); - question ??= new Question - { - TenantId = tenantId, - QuestionBankId = bank.Id, - EntryId = bank.ContentEntryId, - CreatedAt = DateTimeOffset.UtcNow - }; - if (!command.Id.HasValue) dbContext.Questions.Add(question); - ApplyQuestion(question, command, bank.ContentEntryId!.Value, node.Id); - await dbContext.SaveChangesAsync(token); - var nextVersion = await dbContext.QuestionVersions - .Where(item => item.TenantId == tenantId && item.QuestionId == question.Id) - .Select(item => (int?)item.VersionNo).MaxAsync(token) ?? 0; - var version = BuildVersion(actor, tenantId, question.Id, nextVersion + 1, command); - dbContext.QuestionVersions.Add(version); - question.CurrentVersionId = version.Id; - AddAudit(dbContext, actor, "platform.question_bank.question_saved", question.Id, - new { bankId = bank.Id, nodeId = node.Id, version = version.VersionNo }); - await dbContext.SaveChangesAsync(token); - return ToQuestionItem(question, version); - }, cancellationToken); - } - - protected Task ArchiveQuestionsCoreAsync( - PlatformAdminActor actor, - ArchivePlatformQuestionsCommand command, - CancellationToken cancellationToken = default) - { - return ExecuteAsync(actor, "归档公共题库题目", async (_, dbContext, tenantId, token) => - { - var ids = command.QuestionIds.Distinct().Take(500).ToArray(); - if (ids.Length == 0) throw Error("请选择需要归档的题目。", "question_ids_required"); - var questions = await dbContext.Questions.Where(item => item.TenantId == tenantId && ids.Contains(item.Id)) - .ToArrayAsync(token); - foreach (var question in questions) question.Status = QuestionStatus.Archived; - AddAudit(dbContext, actor, "platform.question_bank.questions_archived", Guid.NewGuid(), - new { questionIds = questions.Select(item => item.Id).ToArray() }); - await dbContext.SaveChangesAsync(token); - return questions.Length; - }, cancellationToken); - } - - private static void ValidateQuestion(UpsertPlatformQuestionCommand command) - { - if (string.IsNullOrWhiteSpace(command.Content)) throw Error("题干不能为空。", "question_content_required"); - 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) - { - question.QuestionBankId = command.QuestionBankId; - question.EntryId = entryId; - question.ContentNodeId = nodeId; - question.LegacyId = Normalize(command.LegacyId); - question.Type = Normalize(command.Type) ?? "choice"; - question.TypeLabel = Normalize(command.TypeLabel); - question.Difficulty = command.Difficulty; - question.Tags = ArrayOrDefault(command.Tags); - question.MediaUrl = Normalize(command.MediaUrl); - question.ExamMarkers = ObjectOrDefault(command.ExamMarkers); - question.Status = ParseQuestionStatus(command.Status); - } - - private static QuestionVersion BuildVersion(PlatformAdminActor actor, Guid tenantId, Guid questionId, int versionNo, - UpsertPlatformQuestionCommand command) - { - return new QuestionVersion - { - TenantId = tenantId, - QuestionId = questionId, - VersionNo = versionNo, - QuestionType = Normalize(command.Type) ?? "choice", - TypeLabel = Normalize(command.TypeLabel), - Difficulty = command.Difficulty, - Tags = ArrayOrDefault(command.Tags), - Content = Normalize(command.Content), - Options = ArrayOrDefault(command.Options), - CorrectOptionIndex = command.CorrectOptionIndex, - CorrectOptionIndices = ArrayOrDefault(command.CorrectOptionIndices), - AnswerText = Normalize(command.AnswerText), - Explanation = Normalize(command.Explanation), - SubQuestions = ArrayOrDefault(command.SubQuestions), - CodeLang = Normalize(command.CodeLang), - CodeTemplate = Normalize(command.CodeTemplate), - SourceHash = Normalize(command.SourceHash) ?? Hash(JsonSerializer.Serialize(command)), - CreatedBy = actor.UserId - }; - } - -} diff --git a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Questions/PlatformQuestionAdministrationService.cs b/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Questions/PlatformQuestionAdministrationService.cs deleted file mode 100644 index b698174..0000000 --- a/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Questions/PlatformQuestionAdministrationService.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Tiku.Application.PlatformAdmin; -using Tiku.Application.Security; -using Tiku.Application.Tenancy; - -namespace Tiku.Infrastructure.PlatformAdmin; - -internal sealed class PlatformQuestionAdministrationService( - ICurrentAccessContext currentAccessContext, - ITenantExecutionScope tenantExecutionScope) - : PlatformQuestionBankServiceBase(currentAccessContext, tenantExecutionScope), IPlatformQuestionAdministrationService -{ - public Task GetQuestionsAsync( - PlatformAdminActor actor, - PlatformQuestionBankFilter filter, - CancellationToken cancellationToken = default) => - GetQuestionsCoreAsync(actor, filter, cancellationToken); - - public Task UpsertQuestionAsync( - PlatformAdminActor actor, - UpsertPlatformQuestionCommand command, - CancellationToken cancellationToken = default) => - UpsertQuestionCoreAsync(actor, command, cancellationToken); - - public Task ArchiveQuestionsAsync( - PlatformAdminActor actor, - ArchivePlatformQuestionsCommand command, - CancellationToken cancellationToken = default) => - ArchiveQuestionsCoreAsync(actor, command, cancellationToken); -} diff --git a/Tiku.Infrastructure/QuestionBanks/QuestionBankQueryService.cs b/Tiku.Infrastructure/QuestionBanks/QuestionBankQueryService.cs deleted file mode 100644 index 77d7f48..0000000 --- a/Tiku.Infrastructure/QuestionBanks/QuestionBankQueryService.cs +++ /dev/null @@ -1,350 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Tiku.Application.Catalog; -using Tiku.Application.QuestionBanks; -using Tiku.Application.Security; -using Tiku.Domain.Common; -using Tiku.Domain.Content; -using Tiku.Domain.Learning; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.Infrastructure.QuestionBanks; - -public sealed class QuestionBankQueryService( - IQuestionBankPersistence questionBankPersistence, - ILearningPersistence learningPersistence, - IPublicQuestionAccessPolicy accessPolicy, - ITenantExecutionScope tenantExecutionScope) : IQuestionBankQueryService -{ - private const int DefaultQuestionLimit = 200; - private const int MaxQuestionLimit = 500; - private const int DefaultBankLimit = 100; - private const int MaxBankLimit = 500; - - public async Task> GetQuestionBanksAsync( - QuestionBankFilter filter, - CancellationToken cancellationToken = default) - { - var limit = ResolveLimit(filter.Limit, DefaultBankLimit, MaxBankLimit); - var allowedSliceIds = filter.AllowedContentSliceIds?.ToArray() ?? []; - var tenantItems = filter.Source == QuestionSource.Platform - ? [] - : await questionBankPersistence.QuestionBanks - .AsNoTracking() - .Where(bank => - bank.TenantId == filter.TenantId && - bank.Status == QuestionBankStatus.Active && - learningPersistence.ContentSlices.Any(slice => - slice.TenantId == filter.TenantId && - allowedSliceIds.Contains(slice.Id) && - slice.Status == ContentSliceStatus.Active && - slice.ResourceType == LearningContentResourceType.QuestionBank && - slice.ResourceId == bank.Id) && - (!filter.RegionId.HasValue || bank.RegionId == filter.RegionId.Value || bank.RegionId == null) && - (string.IsNullOrWhiteSpace(filter.Keyword) || bank.Name.Contains(filter.Keyword.Trim()))) - .OrderBy(bank => bank.Name) - .ThenBy(bank => bank.CreatedAt) - .Take(limit) - .Select(bank => new QuestionBankCatalogItem( - bank.Id, - bank.RegionId, - bank.Name, - QuestionSource.Tenant, - bank.Status, - bank.Metadata)) - .ToArrayAsync(cancellationToken); - - var platformItems = filter.Source == QuestionSource.Tenant || - !await CanAccessPlatformAsync(filter.TenantId, cancellationToken) - ? [] - : await tenantExecutionScope.ExecuteAsync( - new SystemScopeRequest( - filter.TenantId, SystemScopeCallerType.PublicQuestionBank, nameof(QuestionBankQueryService), - "List platform question banks for an entitled tenant", Guid.NewGuid().ToString("N")), - async (provider, token) => - { - var systemQuestionBank = provider.GetRequiredService(); - var systemLearning = provider.GetRequiredService(); - var systemTenancy = provider.GetRequiredService(); - return await systemQuestionBank.QuestionBanks.AsNoTracking() - .Join( - systemTenancy.Tenants.AsNoTracking() - .Where(tenant => tenant.Mode == TenantMode.PlatformOwned), - bank => bank.TenantId, - tenant => tenant.Id, - (bank, tenant) => bank) - .Where(bank => - bank.Status == QuestionBankStatus.Active && - systemLearning.ContentSlices.Any(slice => - slice.TenantId == filter.TenantId && - allowedSliceIds.Contains(slice.Id) && - slice.Status == ContentSliceStatus.Active && - slice.ResourceType == LearningContentResourceType.QuestionBank && - slice.ResourceId == bank.Id) && - (!filter.RegionId.HasValue || bank.RegionId == filter.RegionId.Value || - bank.RegionId == null) && - (string.IsNullOrWhiteSpace(filter.Keyword) || bank.Name.Contains(filter.Keyword.Trim()))) - .OrderBy(bank => bank.Name) - .Take(limit) - .Select(bank => new QuestionBankCatalogItem( - bank.Id, - bank.RegionId, - bank.Name, - QuestionSource.Platform, - bank.Status, - bank.Metadata)) - .ToArrayAsync(token); - }, - cancellationToken); - - return new CatalogList(tenantItems - .Concat(platformItems) - .OrderBy(item => item.Name) - .Take(limit) - .ToArray()); - } - - public async Task> GetQuestionsAsync( - QuestionBankFilter filter, - CancellationToken cancellationToken = default) - { - var limit = ResolveLimit(filter.Limit, DefaultQuestionLimit, MaxQuestionLimit); - var tenantItems = filter.Source == QuestionSource.Platform - ? [] - : await ProjectQuestions( - questionBankPersistence, - ApplyQuestionFilters(BaseQuestionQuery(), filter) - .OrderByDescending(question => question.CreatedAt) - .Take(limit), - QuestionSource.Tenant) - .ToArrayAsync(cancellationToken); - var platformItems = filter.Source == QuestionSource.Tenant || - !await CanAccessPlatformAsync(filter.TenantId, cancellationToken) - ? [] - : await GetPlatformQuestionsAsync(filter, limit, cancellationToken); - return new CatalogList(tenantItems.Concat(platformItems).Take(limit).ToArray()); - } - - public async Task GetQuestionAsync( - QuestionBankFilter filter, - CancellationToken cancellationToken = default) - { - if (!filter.QuestionId.HasValue) throw new QuestionBankRequiredFieldException("questionId is required."); - - var result = await GetQuestionsAsync(filter with { Limit = 2 }, cancellationToken); - return result.Items.SingleOrDefault() - ?? throw new QuestionBankNotFoundException("Question was not found."); - } - - private IQueryable BaseQuestionQuery() - { - return questionBankPersistence.Questions - .AsNoTracking() - .Where(question => question.Status == QuestionStatus.Published); - } - - private IQueryable ApplyQuestionFilters( - IQueryable query, - QuestionBankFilter filter) - { - query = query.Where(question => question.TenantId == filter.TenantId); - var allowedSliceIds = filter.AllowedContentSliceIds?.ToArray() ?? []; - query = query.Where(question => - (question.QuestionBankId.HasValue && learningPersistence.ContentSlices.Any(slice => - slice.TenantId == filter.TenantId && - allowedSliceIds.Contains(slice.Id) && - slice.Status == ContentSliceStatus.Active && - slice.ResourceType == LearningContentResourceType.QuestionBank && - slice.ResourceId == question.QuestionBankId.Value)) || - questionBankPersistence.QuestionCollectionItems.Any(collectionItem => - collectionItem.TenantId == filter.TenantId && - collectionItem.QuestionId == question.Id && - learningPersistence.ContentSlices.Any(slice => - slice.TenantId == filter.TenantId && - allowedSliceIds.Contains(slice.Id) && - slice.Status == ContentSliceStatus.Active && - slice.ResourceType == LearningContentResourceType.Collection && - slice.ResourceId == collectionItem.CollectionId))); - - if (filter.QuestionBankId.HasValue) - query = query.Where(question => question.QuestionBankId == filter.QuestionBankId.Value); - - if (filter.SubjectId.HasValue) query = query.Where(question => question.SubjectId == filter.SubjectId.Value); - - if (filter.CategoryId.HasValue) query = query.Where(question => question.CategoryId == filter.CategoryId.Value); - - if (filter.NodeId.HasValue) query = query.Where(question => question.NodeId == filter.NodeId.Value); - - if (filter.EntryId.HasValue) query = query.Where(question => question.EntryId == filter.EntryId.Value); - - if (filter.ContentNodeId.HasValue) - query = query.Where(question => question.ContentNodeId == filter.ContentNodeId.Value); - - if (filter.CollectionId.HasValue) - query = query.Where(question => - question.PrimaryCollectionId == filter.CollectionId.Value || - questionBankPersistence.QuestionCollectionItems.Any(item => - item.TenantId == question.TenantId && - item.QuestionId == question.Id && - item.CollectionId == filter.CollectionId.Value)); - - if (filter.QuestionIds?.Count > 0) query = query.Where(question => filter.QuestionIds.Contains(question.Id)); - - if (!string.IsNullOrWhiteSpace(filter.Type)) - { - var type = filter.Type.Trim(); - query = query.Where(question => question.Type == type); - } - - if (!string.IsNullOrWhiteSpace(filter.Keyword)) - { - var keyword = filter.Keyword.Trim(); - query = query.Where(question => - question.Type.Contains(keyword) || - (question.TypeLabel != null && question.TypeLabel.Contains(keyword)) || - questionBankPersistence.QuestionVersions.Any(version => - version.TenantId == question.TenantId && - version.QuestionId == question.Id && - version.Id == question.CurrentVersionId && - version.Content != null && - version.Content.Contains(keyword))); - } - - return query; - } - - private static IQueryable ApplyKeyword(IQueryable query, string? keyword) - where T : class - { - if (string.IsNullOrWhiteSpace(keyword)) return query; - - var trimmed = keyword.Trim(); - return query.Where(entity => EF.Property(entity, nameof(QuestionBank.Name)).Contains(trimmed)); - } - - private static IQueryable ProjectQuestions( - IQuestionBankPersistence context, - IQueryable questions, - QuestionSource source) - { - var emptyOptions = JsonDefaults.Array(); - return - from question in questions - join version in context.QuestionVersions.AsNoTracking() - on new { question.TenantId, QuestionId = question.Id, VersionId = question.CurrentVersionId } - equals new { version.TenantId, version.QuestionId, VersionId = (Guid?)version.Id } - into versions - from version in versions.DefaultIfEmpty() - select new QuestionCatalogItem( - question.Id, - question.QuestionBankId, - question.SubjectId, - question.CategoryId, - question.NodeId, - question.EntryId, - question.ContentNodeId, - question.PrimaryCollectionId, - question.LegacyId, - question.LegacySubjectId, - question.LegacyCategoryId, - question.LegacyNodeId, - question.Type, - question.TypeLabel, - question.Difficulty, - question.Tags, - question.ExamMarkers, - question.MediaUrl, - question.HasVideoExplanation, - question.Status, - version == null ? null : version.Id, - version == null ? null : version.VersionNo, - version == null ? null : version.Content, - version == null ? emptyOptions : version.Options, - version == null ? null : version.CodeLang, - version == null ? null : version.CodeTemplate, - new QuestionLocator(source, question.Id)); - } - - private async Task CanAccessPlatformAsync(Guid tenantId, CancellationToken cancellationToken) - { - try - { - await accessPolicy.EnsureCanStartAsync(tenantId, cancellationToken); - return true; - } - catch (PublicQuestionAccessDeniedException) - { - return false; - } - } - - private Task GetPlatformQuestionsAsync( - QuestionBankFilter filter, - int limit, - CancellationToken cancellationToken) - { - return tenantExecutionScope.ExecuteAsync( - new SystemScopeRequest( - filter.TenantId, SystemScopeCallerType.PublicQuestionBank, nameof(QuestionBankQueryService), - "List platform questions for an entitled tenant", Guid.NewGuid().ToString("N")), - async (provider, token) => - { - var systemQuestionBank = provider.GetRequiredService(); - var systemLearning = provider.GetRequiredService(); - var systemTenancy = provider.GetRequiredService(); - var platformTenantId = await systemTenancy.Tenants.AsNoTracking() - .Where(tenant => tenant.Mode == TenantMode.PlatformOwned) - .Select(tenant => tenant.Id) - .SingleAsync(token); - var query = systemQuestionBank.Questions.AsNoTracking().Where(question => - question.TenantId == platformTenantId && - question.Status == QuestionStatus.Published && - ((question.QuestionBankId.HasValue && systemLearning.ContentSlices.Any(slice => - slice.TenantId == filter.TenantId && - (filter.AllowedContentSliceIds ?? Array.Empty()).Contains(slice.Id) && - slice.Status == ContentSliceStatus.Active && - slice.ResourceType == LearningContentResourceType.QuestionBank && - slice.ResourceId == question.QuestionBankId.Value)) || - systemQuestionBank.QuestionCollectionItems.Any(collectionItem => - collectionItem.TenantId == filter.TenantId && - collectionItem.QuestionId == question.Id && - systemLearning.ContentSlices.Any(slice => - slice.TenantId == filter.TenantId && - (filter.AllowedContentSliceIds ?? Array.Empty()).Contains(slice.Id) && - slice.Status == ContentSliceStatus.Active && - slice.ResourceType == LearningContentResourceType.Collection && - slice.ResourceId == collectionItem.CollectionId))) && - (!filter.QuestionId.HasValue || question.Id == filter.QuestionId.Value) && - (!filter.QuestionBankId.HasValue || question.QuestionBankId == filter.QuestionBankId.Value) && - (!filter.SubjectId.HasValue || question.SubjectId == filter.SubjectId.Value) && - (!filter.CategoryId.HasValue || question.CategoryId == filter.CategoryId.Value) && - (!filter.NodeId.HasValue || question.NodeId == filter.NodeId.Value) && - (!filter.EntryId.HasValue || question.EntryId == filter.EntryId.Value) && - (!filter.ContentNodeId.HasValue || question.ContentNodeId == filter.ContentNodeId.Value) && - (filter.QuestionIds == null || filter.QuestionIds.Count == 0 || - filter.QuestionIds.Contains(question.Id)) && - (string.IsNullOrWhiteSpace(filter.Type) || question.Type == filter.Type.Trim())); - if (!string.IsNullOrWhiteSpace(filter.Keyword)) - { - var keyword = filter.Keyword.Trim(); - query = query.Where(question => - question.Type.Contains(keyword) || - (question.TypeLabel != null && question.TypeLabel.Contains(keyword))); - } - - return await ProjectQuestions( - systemQuestionBank, - query.OrderByDescending(question => question.CreatedAt).Take(limit), - QuestionSource.Platform) - .ToArrayAsync(token); - }, - cancellationToken); - } - - private static int ResolveLimit(int? limit, int defaultLimit, int maxLimit) - { - return Math.Clamp(limit ?? defaultLimit, 1, maxLimit); - } -} diff --git a/Tiku.IntegrationTests/Api/AssetEndpointTests.cs b/Tiku.IntegrationTests/Api/AssetEndpointTests.cs deleted file mode 100644 index 352597d..0000000 --- a/Tiku.IntegrationTests/Api/AssetEndpointTests.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System.Net; -using System.Text.Json; -using Tiku.Domain.Catalog; -using Tiku.Domain.Common; -using Tiku.Domain.Content; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; - -namespace Tiku.IntegrationTests.Api; - -public sealed class AssetEndpointTests -{ - [Fact] - public async Task Content_assets_return_public_active_assets_by_default() - { - var tenantId = Guid.NewGuid(); - var regionId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new Region { Id = regionId, TenantId = tenantId, Name = "测试地区" }, - new ContentAsset - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Title = "公开讲义", - FileName = "handbook.pdf", - AssetType = ContentAssetType.Pdf, - StorageProvider = AssetStorageProvider.AliyunOss, - Bucket = "tiku-assets", - ObjectKey = $"{tenantId:N}/handbook.pdf", - MimeType = "application/pdf", - Visibility = ContentVisibility.Public, - Status = ContentStatus.Active, - SortOrder = 2 - }, - new ContentAsset - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Title = "会员讲义", - Visibility = ContentVisibility.Members, - Status = ContentStatus.Active, - SortOrder = 1 - }, - new ContentAsset - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Title = "归档讲义", - Visibility = ContentVisibility.Public, - Status = ContentStatus.Archived - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = - await client.GetAsync( - $"/api/student/catalog/content-assets?tenantCode=master®ionId={regionId}&assetType=pdf"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var item = Assert.Single(items); - Assert.Equal("公开讲义", item.GetProperty("title").GetString()); - Assert.Equal("AliyunOss", item.GetProperty("storageProvider").GetString()); - } - - [Fact] - public async Task Content_assets_can_include_locked_catalog_items_when_requested() - { - var tenantId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new ContentAsset - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Title = "会员讲义", - Visibility = ContentVisibility.Members, - Status = ContentStatus.Active - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = - await client.GetAsync("/api/student/catalog/content-assets?tenantCode=master&includeLocked=true"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("会员讲义", Assert.Single(items).GetProperty("title").GetString()); - } - - [Fact] - public async Task Images_filter_private_items_until_include_locked() - { - var tenantId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new ImageAsset - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Title = "公开图", - Category = "banner", - IsPublic = true - }, - new ImageAsset - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Title = "内部图", - Category = "banner", - IsPublic = false - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var publicResponse = - await client.GetAsync("/api/student/catalog/images?tenantCode=master&category=banner"); - using var lockedResponse = - await client.GetAsync("/api/student/catalog/images?tenantCode=master&category=banner&includeLocked=true"); - - Assert.Equal(["公开图"], - (await ReadItemsAsync(publicResponse)).Select(item => item.GetProperty("title").GetString()!).ToArray()); - var lockedTitles = (await ReadItemsAsync(lockedResponse)) - .Select(item => item.GetProperty("title").GetString()!) - .ToArray(); - Assert.Equal(2, lockedTitles.Length); - Assert.Contains("内部图", lockedTitles); - Assert.Contains("公开图", lockedTitles); - } - - [Fact] - public async Task App_assets_support_asset_key_lookup() - { - var tenantId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new AppAsset - { - Id = Guid.NewGuid(), - TenantId = tenantId, - AssetKey = "logo", - FileName = "logo.png" - }, - new AppAsset - { - Id = Guid.NewGuid(), - TenantId = tenantId, - AssetKey = "icon", - FileName = "icon.png" - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = await client.GetAsync("/api/student/catalog/app-assets?tenantCode=master&assetKey=logo"); - var item = Assert.Single(await ReadItemsAsync(response)); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("logo", item.GetProperty("assetKey").GetString()); - } - - [Fact] - public async Task Question_videos_include_video_explanation_summary() - { - var tenantId = Guid.NewGuid(); - var questionId = Guid.NewGuid(); - var videoId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new Question - { - Id = questionId, - TenantId = tenantId, - Type = "choice", - Status = QuestionStatus.Published - }, - new VideoExplanation - { - Id = videoId, - TenantId = tenantId, - Title = "解题视频", - IsActive = true, - SortOrder = 1 - }, - new QuestionVideo - { - Id = Guid.NewGuid(), - TenantId = tenantId, - QuestionId = questionId, - VideoId = videoId, - VideoType = QuestionVideoType.Specific - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = - await client.GetAsync($"/api/student/catalog/question-videos?tenantCode=master&questionId={questionId}"); - var item = Assert.Single(await ReadItemsAsync(response)); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(questionId, item.GetProperty("questionId").GetGuid()); - Assert.Equal("解题视频", item.GetProperty("video").GetProperty("title").GetString()); - } - - private static Tenant Tenant(Guid id, string slug) - { - return new Tenant - { - Id = id, - Slug = slug, - Name = slug, - Status = TenantStatus.Active, - Metadata = JsonDefaults.Object() - }; - } - - private static async Task ReadItemsAsync(HttpResponseMessage response) - { - var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); - return body.RootElement - .GetProperty("items") - .EnumerateArray() - .Select(item => item.Clone()) - .ToArray(); - } -} diff --git a/Tiku.IntegrationTests/Api/AuthorizationManifestTests.cs b/Tiku.IntegrationTests/Api/AuthorizationManifestTests.cs index 6994587..7e08737 100644 --- a/Tiku.IntegrationTests/Api/AuthorizationManifestTests.cs +++ b/Tiku.IntegrationTests/Api/AuthorizationManifestTests.cs @@ -16,8 +16,8 @@ namespace Tiku.IntegrationTests.Api; public sealed class AuthorizationManifestTests { // Controller-agnostic contract: HTTP method, route, action, anonymous access and policies. - private const int ExpectedActionCount = 481; - private const string ExpectedSha256 = "0f5c7597431d23a50f57f57f5074b3d2ab551bcbc534dc85c98543842bfff723"; + private const int ExpectedActionCount = 435; + private const string ExpectedSha256 = "dbafd4eddc7a607ca03869dbce82d23a2aa3335c9b2ea65495c8c3fb06f00d79"; [Fact] public void Controller_authorization_surface_matches_reviewed_manifest() diff --git a/Tiku.IntegrationTests/Api/CatalogEndpointTests.cs b/Tiku.IntegrationTests/Api/CatalogEndpointTests.cs deleted file mode 100644 index b57bf54..0000000 --- a/Tiku.IntegrationTests/Api/CatalogEndpointTests.cs +++ /dev/null @@ -1,510 +0,0 @@ -using System.Net; -using System.Text.Json; -using Tiku.Domain.Catalog; -using Tiku.Domain.Commerce; -using Tiku.Domain.Learning; -using Tiku.Domain.Operations; -using Tiku.Domain.Tenancy; - -namespace Tiku.IntegrationTests.Api; - -public sealed class CatalogEndpointTests -{ - [Fact] - public async Task Regions_are_filtered_by_active_tenant() - { - var tenantId = Guid.NewGuid(); - var otherTenantId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - Tenant(otherTenantId, "other"), - new Region - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Name = "浙江", - SortOrder = 2, - IsActive = true - }, - new Region - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Name = "北京", - SortOrder = 1, - IsActive = true - }, - new Region - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Name = "停用地区", - SortOrder = 0, - IsActive = false - }, - new Region - { - Id = Guid.NewGuid(), - TenantId = otherTenantId, - Name = "其他租户地区", - IsActive = true - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = await client.GetAsync("/api/student/catalog/regions?tenantCode=master"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(["北京", "浙江"], items.Select(item => item.GetProperty("name").GetString()!).ToArray()); - } - - [Fact] - public async Task Subjects_support_common_filters() - { - var tenantId = Guid.NewGuid(); - var regionId = Guid.NewGuid(); - var schoolId = Guid.NewGuid(); - var majorId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new Region { Id = regionId, TenantId = tenantId, Name = "浙江" }, - new School { Id = schoolId, TenantId = tenantId, RegionId = regionId, Name = "测试院校" }, - new Major { Id = majorId, TenantId = tenantId, RegionId = regionId, SchoolId = schoolId, Name = "视觉传达" }, - new Subject - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - SchoolId = schoolId, - MajorId = majorId, - Name = "专业理论", - Type = SubjectType.Professional, - SortOrder = 1, - IsActive = true - }, - new Subject - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - SchoolId = schoolId, - MajorId = majorId, - Name = "公共英语", - Type = SubjectType.Cultural, - SortOrder = 2, - IsActive = true - }, - new Subject - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Name = "停用科目", - Type = SubjectType.Professional, - IsActive = false - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = await client.GetAsync( - $"/api/student/catalog/subjects?tenantCode=master®ionId={regionId}&schoolId={schoolId}&majorId={majorId}&type=professional&keyword=理论"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var item = Assert.Single(items); - Assert.Equal("专业理论", item.GetProperty("name").GetString()); - } - - [Fact] - public async Task Module_nodes_support_root_filter() - { - var tenantId = Guid.NewGuid(); - var parentId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new ModuleNode - { - Id = parentId, - TenantId = tenantId, - Name = "根节点", - Type = ModuleNodeType.Category, - IsActive = true - }, - new ModuleNode - { - Id = Guid.NewGuid(), - TenantId = tenantId, - ParentId = parentId, - Name = "子节点", - Type = ModuleNodeType.Chapter, - IsActive = true - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = await client.GetAsync("/api/student/catalog/module-nodes?tenantCode=master&parentId=root"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var item = Assert.Single(items); - Assert.Equal("根节点", item.GetProperty("name").GetString()); - } - - [Fact] - public async Task Student_catalog_rejects_anonymous_requests() - { - await using var factory = new ApiTestFactory(); - using var client = factory.CreateClient(); - - using var response = await client.GetAsync("/api/student/catalog/regions"); - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task Banners_are_filtered_by_tenant_region_and_active_status() - { - var tenantId = Guid.NewGuid(); - var otherTenantId = Guid.NewGuid(); - var regionId = Guid.NewGuid(); - var otherRegionId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - Tenant(otherTenantId, "other"), - new Region { Id = regionId, TenantId = tenantId, Name = "浙江" }, - new Region { Id = otherRegionId, TenantId = tenantId, Name = "江苏" }, - new Banner - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Title = "第二张", - SortOrder = 2, - IsActive = true - }, - new Banner - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Title = "第一张", - SortOrder = 1, - IsActive = true - }, - new Banner - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Title = "停用横幅", - IsActive = false - }, - new Banner - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = otherRegionId, - Title = "其他地区横幅", - IsActive = true - }, - new Banner - { - Id = Guid.NewGuid(), - TenantId = otherTenantId, - Title = "其他租户横幅", - IsActive = true - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = - await client.GetAsync($"/api/student/catalog/banners?tenantCode=master®ionId={regionId}"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(["第一张", "第二张"], items.Select(item => item.GetProperty("title").GetString()!).ToArray()); - } - - [Fact] - public async Task Faqs_and_announcements_only_return_active_current_tenant_items() - { - var tenantId = Guid.NewGuid(); - var otherTenantId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - Tenant(otherTenantId, "other"), - new Faq - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Question = "如何购买?", - Answer = "在线购买", - IsActive = true - }, - new Faq - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Question = "停用 FAQ", - IsActive = false - }, - new Faq - { - Id = Guid.NewGuid(), - TenantId = otherTenantId, - Question = "其他租户 FAQ", - IsActive = true - }, - new Announcement - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Content = "暑期活动", - IsActive = true - }, - new Announcement - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Content = "停用公告", - IsActive = false - }, - new Announcement - { - Id = Guid.NewGuid(), - TenantId = otherTenantId, - Content = "其他租户公告", - IsActive = true - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var faqResponse = await client.GetAsync("/api/student/catalog/faqs?tenantCode=master"); - using var announcementResponse = await client.GetAsync("/api/student/catalog/announcements?tenantCode=master"); - var faqs = await ReadItemsAsync(faqResponse); - var announcements = await ReadItemsAsync(announcementResponse); - - Assert.Equal(HttpStatusCode.OK, faqResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, announcementResponse.StatusCode); - Assert.Equal("如何购买?", Assert.Single(faqs).GetProperty("question").GetString()); - Assert.Equal("暑期活动", Assert.Single(announcements).GetProperty("content").GetString()); - } - - [Fact] - public async Task Exam_dates_support_region_school_filters_and_days_left() - { - var tenantId = Guid.NewGuid(); - var regionId = Guid.NewGuid(); - var schoolId = Guid.NewGuid(); - var otherRegionId = Guid.NewGuid(); - var otherSchoolId = Guid.NewGuid(); - var futureExamAt = new DateTimeOffset(DateTimeOffset.UtcNow.Date.AddDays(10), TimeSpan.Zero); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new Region { Id = regionId, TenantId = tenantId, Name = "浙江" }, - new Region { Id = otherRegionId, TenantId = tenantId, Name = "其他地区" }, - new School { Id = schoolId, TenantId = tenantId, RegionId = regionId, Name = "测试院校" }, - new School { Id = otherSchoolId, TenantId = tenantId, RegionId = otherRegionId, Name = "其他院校" }, - new ExamDate - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - SchoolId = schoolId, - ExamName = "校考", - ExamAt = futureExamAt, - IsActive = true - }, - new ExamDate - { - Id = Guid.NewGuid(), - TenantId = tenantId, - ExamName = "通用考试", - ExamAt = futureExamAt.AddDays(1), - IsActive = true - }, - new ExamDate - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = otherRegionId, - SchoolId = otherSchoolId, - ExamName = "其他地区学校考试", - ExamAt = futureExamAt.AddDays(2), - IsActive = true - }, - new ExamDate - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - SchoolId = schoolId, - ExamName = "停用考试", - IsActive = false - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = await client.GetAsync( - $"/api/student/catalog/exam-dates?tenantCode=master®ionId={regionId}&schoolId={schoolId}"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(["校考", "通用考试"], items.Select(item => item.GetProperty("examName").GetString()!).ToArray()); - Assert.Equal(10, items[0].GetProperty("daysLeft").GetInt32()); - } - - [Fact] - public async Task Products_filter_active_region_type_keyword_and_limit() - { - var tenantId = Guid.NewGuid(); - var regionId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new Region { Id = regionId, TenantId = tenantId, Name = "浙江" }, - new Product - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Title = "联考资料 B", - Type = ProductType.Material, - SortOrder = 2, - LegacyPriceText = "¥20", - IsActive = true - }, - new Product - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Title = "联考资料 A", - Type = ProductType.Material, - SortOrder = 1, - LegacyPriceText = "¥10", - IsActive = true - }, - new Product - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Title = "联考课程", - Type = ProductType.Course, - IsActive = true - }, - new Product - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Title = "停用资料", - Type = ProductType.Material, - IsActive = false - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = await client.GetAsync( - $"/api/student/catalog/products?tenantCode=master®ionId={regionId}&type=material&keyword=联考&limit=1"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var item = Assert.Single(items); - Assert.Equal("联考资料 A", item.GetProperty("title").GetString()); - Assert.Equal("¥10", item.GetProperty("priceLabel").GetString()); - Assert.True(item.GetProperty("isActive").GetBoolean()); - } - - [Fact] - public async Task Svip_plans_filter_active_region_and_include_global_plans() - { - var tenantId = Guid.NewGuid(); - var regionId = Guid.NewGuid(); - var otherRegionId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new Region { Id = regionId, TenantId = tenantId, Name = "浙江" }, - new Region { Id = otherRegionId, TenantId = tenantId, Name = "其他地区" }, - new SvipPlan - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Name = "地区月卡", - PriceCents = 3000, - Days = 30, - SortOrder = 2, - IsActive = true - }, - new SvipPlan - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Name = "通用周卡", - PriceCents = 900, - Days = 7, - SortOrder = 1, - IsActive = true - }, - new SvipPlan - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = otherRegionId, - Name = "其他地区卡", - PriceCents = 1000, - Days = 7, - IsActive = true - }, - new SvipPlan - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Name = "停用卡", - PriceCents = 1, - Days = 1, - IsActive = false - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = - await client.GetAsync($"/api/student/catalog/svip-plans?tenantCode=master®ionId={regionId}"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(["通用周卡", "地区月卡"], items.Select(item => item.GetProperty("name").GetString()!).ToArray()); - Assert.Equal(9m, items[0].GetProperty("price").GetDecimal()); - } - - private static Tenant Tenant(Guid id, string slug) - { - return new Tenant - { - Id = id, - Slug = slug, - Name = slug, - Status = TenantStatus.Active - }; - } - - private static async Task ReadItemsAsync(HttpResponseMessage response) - { - var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); - return body.RootElement - .GetProperty("items") - .EnumerateArray() - .Select(item => item.Clone()) - .ToArray(); - } -} diff --git a/Tiku.IntegrationTests/Api/ContentManagementEndpointTests.cs b/Tiku.IntegrationTests/Api/ContentManagementEndpointTests.cs deleted file mode 100644 index 249acd9..0000000 --- a/Tiku.IntegrationTests/Api/ContentManagementEndpointTests.cs +++ /dev/null @@ -1,319 +0,0 @@ -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; -using Microsoft.Extensions.DependencyInjection; -using Tiku.Api.Contracts; -using Tiku.Application.Auth; -using Tiku.Application.Security; -using Tiku.Domain.Catalog; -using Tiku.Domain.Common; -using Tiku.Domain.Content; -using Tiku.Domain.Identity; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.IntegrationTests.Api; - -public sealed class ContentManagementEndpointTests -{ - [Fact] - public async Task Tenant_content_management_requires_admin_authentication() - { - await using var factory = new ApiTestFactory(); - using var client = factory.CreateClient(); - - using var response = await client.GetAsync("/api/tenant/content/entries"); - - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task Tenant_admin_can_upsert_entries_and_nodes() - { - await using var factory = new ApiTestFactory(); - var seed = await SeedAdminAsync(factory); - using var client = factory.CreateClient(); - await LoginAsync(client, seed); - - using var entryResponse = await client.PostAsJsonAsync( - "/api/tenant/content/entries", - new UpsertContentEntryDto - { - EntryKey = "exam-practice", - Name = "刷题入口", - EntryType = "questionPractice", - Visibility = "members", - Order = 10 - }); - var entryJson = await ReadJsonAsync(entryResponse); - var entryId = entryJson.RootElement.GetProperty("item").GetProperty("id").GetGuid(); - - using var nodeResponse = await client.PostAsJsonAsync( - "/api/tenant/content/nodes", - new UpsertContentNodeDto - { - EntryId = entryId, - NodeKey = "chapter-1", - Name = "第一章", - NodeType = "chapter", - IsLeaf = true - }); - var nodeJson = await ReadJsonAsync(nodeResponse); - using var listResponse = await client.GetAsync($"/api/tenant/content/nodes?entryId={entryId}&parentId=root"); - var listJson = await ReadJsonAsync(listResponse); - - Assert.Equal(HttpStatusCode.OK, entryResponse.StatusCode); - Assert.Equal("exam-practice", entryJson.RootElement.GetProperty("item").GetProperty("entryKey").GetString()); - Assert.Equal(HttpStatusCode.OK, nodeResponse.StatusCode); - var node = nodeJson.RootElement.GetProperty("item"); - Assert.Equal(0, node.GetProperty("depth").GetInt32()); - Assert.StartsWith("n_", node.GetProperty("path").GetString(), StringComparison.Ordinal); - Assert.Single(listJson.RootElement.GetProperty("items").EnumerateArray()); - } - - [Fact] - public async Task Tenant_admin_can_upsert_collection_replace_items_and_create_blueprint() - { - await using var factory = new ApiTestFactory(); - var seed = await SeedAdminAsync(factory); - var questionId = Guid.NewGuid(); - await factory.SeedAsync(new Question - { - Id = questionId, - TenantId = seed.TenantId, - Type = "choice", - Status = QuestionStatus.Published - }); - using var client = factory.CreateClient(); - await LoginAsync(client, seed); - - var entryId = await CreateEntryAsync(client); - var collectionResponse = await client.PostAsJsonAsync( - "/api/tenant/content/question-collections", - new UpsertQuestionCollectionDto - { - EntryId = entryId, - Name = "基础题集", - CollectionType = "manual", - SourceType = "manualQuestions", - TotalScore = 100, - DurationMinutes = 45 - }); - var collectionJson = await ReadJsonAsync(collectionResponse); - var collectionId = collectionJson.RootElement.GetProperty("item").GetProperty("id").GetGuid(); - - var replaceResponse = await client.PostAsJsonAsync( - "/api/tenant/content/question-collections/items/replace", - new ReplaceCollectionItemsDto - { - CollectionId = collectionId, - Questions = - [ - new CollectionQuestionDto - { - QuestionId = questionId, - SectionKey = "choice", - Order = 1, - Score = 5, - Required = true - } - ] - }); - var replaceJson = await ReadJsonAsync(replaceResponse); - - var blueprintResponse = await client.PostAsJsonAsync( - "/api/tenant/content/practice-blueprints", - new UpsertPracticeBlueprintDto - { - EntryId = entryId, - CollectionId = collectionId, - Name = "章节练习", - Mode = "sequential", - AssemblyType = "collection", - QuestionLimit = 10, - TotalScore = 100 - }); - var blueprintJson = await ReadJsonAsync(blueprintResponse); - - Assert.Equal(HttpStatusCode.OK, collectionResponse.StatusCode); - Assert.Equal("Manual", - collectionJson.RootElement.GetProperty("item").GetProperty("collectionType").GetString()); - Assert.Equal(HttpStatusCode.OK, replaceResponse.StatusCode); - Assert.Equal(1, replaceJson.RootElement.GetProperty("questionCount").GetInt32()); - Assert.Equal(HttpStatusCode.OK, blueprintResponse.StatusCode); - Assert.Equal(collectionId, blueprintJson.RootElement.GetProperty("item").GetProperty("collectionId").GetGuid()); - - using var scope = factory.CreateSystemScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - Assert.Equal(1, dbContext.QuestionCollections.Single(item => item.Id == collectionId).QuestionCount); - Assert.Contains(dbContext.QuestionCollectionItems, item => item.QuestionId == questionId); - } - - [Fact] - public async Task Tenant_admin_can_get_import_field_mapping_and_templates() - { - await using var factory = new ApiTestFactory(); - var seed = await SeedAdminAsync(factory); - using var client = factory.CreateClient(); - await LoginAsync(client, seed); - - using var mappingResponse = - await client.GetAsync("/api/tenant/content/imports/field-mapping?importType=questions"); - using var templateResponse = - await client.GetAsync("/api/tenant/content/imports/templates?importType=questions&format=csv"); - var mapping = await ReadJsonAsync(mappingResponse); - var template = await ReadJsonAsync(templateResponse); - - Assert.Equal(HttpStatusCode.OK, mappingResponse.StatusCode); - Assert.Equal("questions", mapping.RootElement.GetProperty("importType").GetString()); - Assert.Contains(mapping.RootElement.GetProperty("requiredFields").EnumerateArray(), - item => item.GetString() == "type"); - Assert.Equal(HttpStatusCode.OK, templateResponse.StatusCode); - Assert.Equal("csv", template.RootElement.GetProperty("format").GetString()); - Assert.NotEmpty(template.RootElement.GetProperty("contentBase64").GetString() ?? string.Empty); - } - - [Fact] - public async Task ContentEntries_ApplySelfAndRestrictedScopesAndHideUnauthorizedUpdates() - { - await using var factory = new ApiTestFactory(); - var seed = await SeedAdminAsync(factory); - var allowedRegionId = Guid.NewGuid(); - var outsideRegionId = Guid.NewGuid(); - var regionalCreatorId = Guid.NewGuid(); - var outsideCreatorId = Guid.NewGuid(); - var ownEntry = new ContentEntry - { - TenantId = seed.TenantId, - RegionId = outsideRegionId, - EntryKey = "own-entry", - Name = "Own Entry", - CreatedBy = seed.UserId - }; - var regionalEntry = new ContentEntry - { - TenantId = seed.TenantId, - RegionId = allowedRegionId, - EntryKey = "regional-entry", - Name = "Regional Entry", - CreatedBy = regionalCreatorId - }; - var outsideEntry = new ContentEntry - { - TenantId = seed.TenantId, - RegionId = outsideRegionId, - EntryKey = "outside-entry", - Name = "Outside Entry", - CreatedBy = outsideCreatorId - }; - await factory.SeedAsync( - new Region { Id = allowedRegionId, TenantId = seed.TenantId, Name = "Allowed Region" }, - new Region { Id = outsideRegionId, TenantId = seed.TenantId, Name = "Outside Region" }, - new User { Id = regionalCreatorId, Name = "Regional Creator" }, - new User { Id = outsideCreatorId, Name = "Outside Creator" }, - ownEntry, - regionalEntry, - outsideEntry); - await SetDataScopeAsync(factory, seed.TenantId, new { mode = "self" }); - - using var client = factory.CreateClient(); - await LoginAsync(client, seed); - using var selfResponse = await client.GetAsync("/api/tenant/content/entries?includeInactive=true"); - using var selfJson = await ReadJsonAsync(selfResponse); - - await SetDataScopeAsync(factory, seed.TenantId, new - { - mode = "restricted", - regionIds = new[] { allowedRegionId }, - includesSelf = false - }); - using var restrictedResponse = await client.GetAsync("/api/tenant/content/entries?includeInactive=true"); - using var restrictedJson = await ReadJsonAsync(restrictedResponse); - using var deniedUpdate = await client.PostAsJsonAsync( - "/api/tenant/content/entries", - new UpsertContentEntryDto - { - Id = outsideEntry.Id, - RegionId = outsideRegionId, - EntryKey = outsideEntry.EntryKey, - Name = "Must stay hidden" - }); - - Assert.Equal([ownEntry.Id], selfJson.RootElement.GetProperty("items").EnumerateArray() - .Select(item => item.GetProperty("id").GetGuid())); - Assert.Equal([regionalEntry.Id], restrictedJson.RootElement.GetProperty("items").EnumerateArray() - .Select(item => item.GetProperty("id").GetGuid())); - Assert.Equal(HttpStatusCode.NotFound, deniedUpdate.StatusCode); - } - - private static async Task CreateEntryAsync(HttpClient client) - { - using var response = await client.PostAsJsonAsync( - "/api/tenant/content/entries", - new UpsertContentEntryDto - { - EntryKey = Guid.NewGuid().ToString("N"), - Name = "刷题入口", - EntryType = "questionPractice" - }); - var json = await ReadJsonAsync(response); - return json.RootElement.GetProperty("item").GetProperty("id").GetGuid(); - } - - private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedAdminAsync(ApiTestFactory factory) - { - var tenantId = Guid.NewGuid(); - var userId = Guid.NewGuid(); - var phone = "13700000000"; - await factory.SeedAsync( - new Tenant - { - Id = tenantId, - Slug = tenantId.ToString("N"), - Name = "Test Tenant", - Status = TenantStatus.Active, - Metadata = JsonDefaults.Object() - }, - new User - { - Id = userId, - Phone = phone, - Name = "Tenant Admin" - }.WithTestPassword(), - new TenantMembership - { - TenantId = tenantId, - UserId = userId, - Role = TenantRole.TenantAdmin, - Status = MembershipStatus.Active - }); - - return (tenantId, userId, phone); - } - - private static async Task LoginAsync( - HttpClient client, - (Guid TenantId, Guid UserId, string Phone) seed) - { - client.UseAccessToken(await client.LoginAsTenantAsync(seed.TenantId, seed.Phone)); - } - - private static async Task SetDataScopeAsync(ApiTestFactory factory, Guid tenantId, object value) - { - using var scope = factory.CreateSystemScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var role = dbContext.TenantBackendRoles.Single(item => - item.TenantId == tenantId && item.Code == "integration_test_admin"); - role.DataScope = JsonSerializer.SerializeToElement(value); - await dbContext.SaveChangesAsync(); - await scope.ServiceProvider.GetRequiredService() - .BumpScopeAsync(AuthRealm.Tenant, tenantId); - } - - private static async Task ReadJsonAsync(HttpResponseMessage response) - { - var stream = await response.Content.ReadAsStreamAsync(); - return await JsonDocument.ParseAsync(stream); - } -} diff --git a/Tiku.IntegrationTests/Api/ContentNavigationEndpointTests.cs b/Tiku.IntegrationTests/Api/ContentNavigationEndpointTests.cs deleted file mode 100644 index 3362c83..0000000 --- a/Tiku.IntegrationTests/Api/ContentNavigationEndpointTests.cs +++ /dev/null @@ -1,292 +0,0 @@ -using System.Net; -using System.Text.Json; -using Tiku.Domain.Catalog; -using Tiku.Domain.Common; -using Tiku.Domain.Content; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; - -namespace Tiku.IntegrationTests.Api; - -public sealed class ContentNavigationEndpointTests -{ - [Fact] - public async Task Content_entries_hide_hidden_items_by_default() - { - var tenantId = Guid.NewGuid(); - var regionId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new Region { Id = regionId, TenantId = tenantId, Name = "测试地区" }, - new ContentEntry - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - EntryKey = "practice", - Name = "公开练习", - EntryType = ContentEntryType.QuestionPractice, - Visibility = ContentVisibility.Public, - SortOrder = 2 - }, - new ContentEntry - { - Id = Guid.NewGuid(), - TenantId = tenantId, - EntryKey = "global", - Name = "全局入口", - EntryType = ContentEntryType.QuestionPractice, - Visibility = ContentVisibility.Public, - SortOrder = 1 - }, - new ContentEntry - { - Id = Guid.NewGuid(), - TenantId = tenantId, - EntryKey = "hidden", - Name = "隐藏入口", - EntryType = ContentEntryType.QuestionPractice, - Visibility = ContentVisibility.Hidden - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = - await client.GetAsync($"/api/student/catalog/content-entries?tenantCode=master®ionId={regionId}"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(["全局入口", "公开练习"], items.Select(item => item.GetProperty("name").GetString()!).ToArray()); - } - - [Fact] - public async Task Content_nodes_support_required_entry_and_root_filter() - { - var tenantId = Guid.NewGuid(); - var entryId = Guid.NewGuid(); - var rootNodeId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new ContentEntry - { - Id = entryId, - TenantId = tenantId, - EntryKey = "practice", - Name = "练习入口" - }, - new ContentNode - { - Id = rootNodeId, - TenantId = tenantId, - EntryId = entryId, - Name = "根内容节点", - NodeKey = "root", - SortOrder = 1, - IsLeaf = false - }, - new ContentNode - { - Id = Guid.NewGuid(), - TenantId = tenantId, - EntryId = entryId, - ParentId = rootNodeId, - Name = "子内容节点", - NodeKey = "child", - SortOrder = 2, - IsLeaf = true - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var missingEntryResponse = await client.GetAsync("/api/student/catalog/content-nodes?tenantCode=master"); - using var response = - await client.GetAsync( - $"/api/student/catalog/content-nodes?tenantCode=master&entryId={entryId}&parentId=root"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.BadRequest, missingEntryResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var item = Assert.Single(items); - Assert.Equal("根内容节点", item.GetProperty("name").GetString()); - } - - [Fact] - public async Task Collections_and_blueprints_return_active_items_only() - { - var tenantId = Guid.NewGuid(); - var entryId = Guid.NewGuid(); - var nodeId = Guid.NewGuid(); - var collectionId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new ContentEntry { Id = entryId, TenantId = tenantId, EntryKey = "practice", Name = "练习入口" }, - new ContentNode { Id = nodeId, TenantId = tenantId, EntryId = entryId, Name = "章节" }, - new QuestionCollection - { - Id = collectionId, - TenantId = tenantId, - EntryId = entryId, - NodeId = nodeId, - Name = "章节题集", - CollectionType = QuestionCollectionType.Chapter, - Status = ContentStatus.Active, - SortOrder = 1 - }, - new QuestionCollection - { - Id = Guid.NewGuid(), - TenantId = tenantId, - EntryId = entryId, - NodeId = nodeId, - Name = "草稿题集", - Status = ContentStatus.Draft - }, - new PracticeBlueprint - { - Id = Guid.NewGuid(), - TenantId = tenantId, - EntryId = entryId, - NodeId = nodeId, - CollectionId = collectionId, - Name = "章节练习", - Mode = PracticeMode.Sequential, - Status = ContentStatus.Active - }, - new PracticeBlueprint - { - Id = Guid.NewGuid(), - TenantId = tenantId, - EntryId = entryId, - NodeId = nodeId, - CollectionId = collectionId, - Name = "归档练习", - Mode = PracticeMode.Sequential, - Status = ContentStatus.Archived - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var collectionsResponse = await client.GetAsync( - $"/api/student/catalog/question-collections?tenantCode=master&entryId={entryId}&nodeId={nodeId}&collectionType=chapter"); - using var blueprintsResponse = await client.GetAsync( - $"/api/student/catalog/practice-blueprints?tenantCode=master&entryId={entryId}&nodeId={nodeId}&collectionId={collectionId}&mode=sequential"); - var collections = await ReadItemsAsync(collectionsResponse); - var blueprints = await ReadItemsAsync(blueprintsResponse); - - Assert.Equal(HttpStatusCode.OK, collectionsResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, blueprintsResponse.StatusCode); - Assert.Equal("章节题集", Assert.Single(collections).GetProperty("name").GetString()); - Assert.Equal("章节练习", Assert.Single(blueprints).GetProperty("name").GetString()); - } - - [Fact] - public async Task Collection_questions_return_published_questions_only() - { - var tenantId = Guid.NewGuid(); - var collectionId = Guid.NewGuid(); - var publishedQuestionId = Guid.NewGuid(); - var archivedQuestionId = Guid.NewGuid(); - var versionId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new QuestionCollection - { - Id = collectionId, - TenantId = tenantId, - Name = "真题套卷", - Status = ContentStatus.Active - }, - new Question - { - Id = archivedQuestionId, - TenantId = tenantId, - PrimaryCollectionId = collectionId, - Type = "choice", - Status = QuestionStatus.Archived - }); - var archivedReference = await factory.SeedQuestionReferenceAsync( - tenantId, - tenantId, - archivedQuestionId); - await factory.SeedAsync(new QuestionCollectionItem - { - Id = Guid.NewGuid(), - TenantId = tenantId, - CollectionId = collectionId, - QuestionReferenceId = archivedReference.Id, - QuestionOwnerTenantId = tenantId, - QuestionId = archivedQuestionId, - SortOrder = 2 - }); - await factory.SeedQuestionWithVersionAsync( - new Question - { - Id = publishedQuestionId, - TenantId = tenantId, - PrimaryCollectionId = collectionId, - Type = "choice", - TypeLabel = "单选题", - Status = QuestionStatus.Published - }, - new QuestionVersion - { - Id = versionId, - TenantId = tenantId, - QuestionId = publishedQuestionId, - Content = "题干" - }); - var publishedReference = await factory.SeedQuestionReferenceAsync( - tenantId, - tenantId, - publishedQuestionId); - await factory.SeedAsync(new QuestionCollectionItem - { - Id = Guid.NewGuid(), - TenantId = tenantId, - CollectionId = collectionId, - QuestionReferenceId = publishedReference.Id, - QuestionOwnerTenantId = tenantId, - QuestionId = publishedQuestionId, - SortOrder = 1, - Score = 2 - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = - await client.GetAsync( - $"/api/student/catalog/question-collections/questions?tenantCode=master&collectionId={collectionId}"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var item = Assert.Single(items); - Assert.Equal(publishedQuestionId, item.GetProperty("id").GetGuid()); - Assert.Equal("题干", item.GetProperty("content").GetString()); - } - - private static Tenant Tenant(Guid id, string slug) - { - return new Tenant - { - Id = id, - Slug = slug, - Name = slug, - Status = TenantStatus.Active, - Metadata = JsonDefaults.Object() - }; - } - - private static async Task ReadItemsAsync(HttpResponseMessage response) - { - var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); - return body.RootElement - .GetProperty("items") - .EnumerateArray() - .Select(item => item.Clone()) - .ToArray(); - } -} diff --git a/Tiku.IntegrationTests/Api/LearningAccessPolicyTests.cs b/Tiku.IntegrationTests/Api/LearningAccessPolicyTests.cs deleted file mode 100644 index d96630e..0000000 --- a/Tiku.IntegrationTests/Api/LearningAccessPolicyTests.cs +++ /dev/null @@ -1,225 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Tiku.Application.Learning; -using Tiku.Domain.Commerce; -using Tiku.Domain.Identity; -using Tiku.Domain.Learning; -using Tiku.Domain.Tenancy; - -namespace Tiku.IntegrationTests.Api; - -public sealed class LearningAccessPolicyTests -{ - [Fact] - public async Task Target_region_history_is_isolated_per_business_line() - { - await using var factory = new ApiTestFactory(); - var tenantId = Guid.NewGuid(); - var userId = Guid.NewGuid(); - var primaryBusinessLineId = Guid.NewGuid(); - var futureBusinessLineId = Guid.NewGuid(); - var primaryRegionId = Guid.NewGuid(); - var futureRegionId = Guid.NewGuid(); - await factory.SeedAsync( - new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Multi-business Tenant" }, - new User { Id = userId, Phone = $"137{Random.Shared.Next(10_000_000, 99_999_999)}" }, - new BusinessLine - { - Id = primaryBusinessLineId, - Code = $"primary-{primaryBusinessLineId:N}", - Name = "当前主业务", - RegionAccessStrategy = LearningRegionAccessStrategy.NationalWithStudentTargetRegion - }, - new BusinessLine - { - Id = futureBusinessLineId, - Code = $"future-{futureBusinessLineId:N}", - Name = "未来新增业务", - RegionAccessStrategy = LearningRegionAccessStrategy.NationalWithStudentTargetRegion - }, - new MarketRegion { Id = primaryRegionId, Code = $"p-{primaryRegionId:N}"[..18], Name = "主业务地区" }, - new MarketRegion { Id = futureRegionId, Code = $"f-{futureRegionId:N}"[..18], Name = "新增业务地区" }, - new TenantLearningLicense - { - TenantId = tenantId, - BusinessLineId = primaryBusinessLineId, - IsPrimary = true, - IncludesNational = true - }, - new TenantLearningLicense - { - TenantId = tenantId, - BusinessLineId = futureBusinessLineId, - IsPrimary = false, - IncludesNational = true - }, - new StudentTargetRegionHistory - { - TenantId = tenantId, - UserId = userId, - BusinessLineId = primaryBusinessLineId, - MarketRegionId = primaryRegionId - }, - new StudentTargetRegionHistory - { - TenantId = tenantId, - UserId = userId, - BusinessLineId = futureBusinessLineId, - MarketRegionId = futureRegionId - }); - - using var scope = factory.CreateTenantScope(tenantId); - var target = await scope.ServiceProvider.GetRequiredService() - .GetTargetRegionAsync(new LearningActor(tenantId, userId)); - - Assert.NotNull(target); - Assert.Equal(primaryRegionId, target.MarketRegionId); - } - - [Theory] - [InlineData(LearningRegionAccessStrategy.NationalOnly, true, 1, 0)] - [InlineData(LearningRegionAccessStrategy.LicensedRegions, false, 2, 2)] - [InlineData(LearningRegionAccessStrategy.NationalWithStudentTargetRegion, true, 2, 1)] - [InlineData(LearningRegionAccessStrategy.NationalWithLicensedRegions, true, 3, 2)] - public async Task Configured_region_strategy_compiles_expected_slice_and_region_grants( - LearningRegionAccessStrategy strategy, - bool includesNational, - int expectedSliceCount, - int expectedRegionCount) - { - await using var factory = new ApiTestFactory(); - var tenantId = Guid.NewGuid(); - var userId = Guid.NewGuid(); - var businessLineId = Guid.NewGuid(); - var licenseId = Guid.NewGuid(); - var productId = Guid.NewGuid(); - var regionA = Guid.NewGuid(); - var regionB = Guid.NewGuid(); - var nationalSlice = Slice(tenantId, businessLineId, LearningRegionScopeKind.National, null); - var regionASlice = Slice(tenantId, businessLineId, LearningRegionScopeKind.Region, regionA); - var regionBSlice = Slice(tenantId, businessLineId, LearningRegionScopeKind.Region, regionB); - var slices = new[] { nationalSlice, regionASlice, regionBSlice }; - - var entities = new List - { - new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Policy Tenant" }, - new User { Id = userId, Phone = $"136{Random.Shared.Next(10_000_000, 99_999_999)}" }, - new BusinessLine - { - Id = businessLineId, - Code = $"p-{(int)strategy}-{tenantId:N}", - Name = "配置化业务线", - RegionAccessStrategy = strategy - }, - new MarketRegion { Id = regionA, Code = $"a-{regionA:N}"[..18], Name = "地区 A" }, - new MarketRegion { Id = regionB, Code = $"b-{regionB:N}"[..18], Name = "地区 B" }, - new TenantLearningLicense - { - Id = licenseId, - TenantId = tenantId, - BusinessLineId = businessLineId, - IncludesNational = includesNational, - StartsAt = DateTimeOffset.UtcNow.AddDays(-1) - }, - new TenantLearningLicenseRegion - { - TenantId = tenantId, - LicenseId = licenseId, - MarketRegionId = regionA, - IsBaseRegion = true - }, - new TenantLearningLicenseRegion - { - TenantId = tenantId, - LicenseId = licenseId, - MarketRegionId = regionB - }, - new LearningProduct - { - Id = productId, - TenantId = tenantId, - BusinessLineId = businessLineId, - Code = "all-slices", - Name = "全部测试切片" - }, - new Entitlement - { - TenantId = tenantId, - UserId = userId, - LearningProductId = productId, - StartsAt = DateTimeOffset.UtcNow.AddDays(-1), - ExpiresAt = DateTimeOffset.UtcNow.AddDays(1), - Status = EntitlementStatus.Active, - SourceType = "integration_test" - }, - new LearningAccessVersion { TenantId = tenantId, UserId = userId } - }; - entities.AddRange(slices); - entities.AddRange(slices.Select(slice => new LearningProductScope - { - TenantId = tenantId, - ProductId = productId, - ContentSliceOwnerTenantId = tenantId, - ContentSliceId = slice.Id - })); - if (strategy == LearningRegionAccessStrategy.NationalWithStudentTargetRegion) - entities.Add(new StudentTargetRegionHistory - { - TenantId = tenantId, - UserId = userId, - BusinessLineId = businessLineId, - MarketRegionId = regionA - }); - await factory.SeedAsync([.. entities]); - - using var scope = factory.CreateTenantScope(tenantId); - var snapshot = await scope.ServiceProvider.GetRequiredService() - .GetSnapshotAsync(new LearningActor(tenantId, userId)); - - Assert.Equal(expectedSliceCount, snapshot.ContentSliceIds.Count); - Assert.Equal(expectedRegionCount, snapshot.LicensedRegionIds.Count); - Assert.Equal(includesNational, snapshot.ContentSliceIds.Contains(nationalSlice.Id)); - Assert.Equal( - strategy == LearningRegionAccessStrategy.NationalWithStudentTargetRegion ? regionA : (Guid?)null, - snapshot.TargetRegionId); - } - - [Fact] - public async Task Snapshot_fails_closed_without_an_active_primary_license() - { - await using var factory = new ApiTestFactory(); - var tenantId = Guid.NewGuid(); - var userId = Guid.NewGuid(); - await factory.SeedAsync(new Tenant - { - Id = tenantId, - Slug = tenantId.ToString("N"), - Name = "Unlicensed Tenant" - }); - - using var scope = factory.CreateTenantScope(tenantId); - var exception = await Assert.ThrowsAsync(() => - scope.ServiceProvider.GetRequiredService() - .GetSnapshotAsync(new LearningActor(tenantId, userId))); - - Assert.Equal("learning_license_required", exception.Code); - } - - private static ContentSlice Slice( - Guid tenantId, - Guid businessLineId, - LearningRegionScopeKind regionScope, - Guid? regionId) - { - return new ContentSlice - { - Id = Guid.NewGuid(), - TenantId = tenantId, - BusinessLineId = businessLineId, - RegionScope = regionScope, - MarketRegionId = regionId, - ResourceType = LearningContentResourceType.Collection, - ResourceId = Guid.NewGuid(), - Status = ContentSliceStatus.Active - }; - } -} diff --git a/Tiku.IntegrationTests/Api/LearningEndpointTests.cs b/Tiku.IntegrationTests/Api/LearningEndpointTests.cs index a0b0251..32baee1 100644 --- a/Tiku.IntegrationTests/Api/LearningEndpointTests.cs +++ b/Tiku.IntegrationTests/Api/LearningEndpointTests.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Tiku.Api.Contracts; using Tiku.Application.Learning; +using Tiku.Domain.Common; using Tiku.Domain.Commerce; using Tiku.Domain.Content; using Tiku.Domain.Identity; @@ -17,6 +18,218 @@ namespace Tiku.IntegrationTests.Api; public sealed class LearningEndpointTests { + [Fact] + public async Task V2_session_creation_reserves_manifest_quota_atomically() + { + await using var factory = new ApiTestFactory(); + var seed = await SeedLearningUserAsync(factory); + var releaseId = await SeedV2ReleaseAccessAsync(factory, seed, dailyQuestionLimit: 1); + using var client = factory.CreateClient(); + await LoginAsync(client, seed); + + Task CreateAsync() => client.PostAsJsonAsync( + "/api/student/learning/practice-sessions", + new CreatePracticeSessionDto + { + ResourceType = AccessResourceType.ContentRelease, + ResourceId = releaseId + }); + + var responses = await Task.WhenAll(CreateAsync(), CreateAsync()); + + Assert.Single(responses, response => response.StatusCode == HttpStatusCode.OK); + var denied = Assert.Single(responses, response => response.StatusCode == HttpStatusCode.Forbidden); + Assert.Equal("practice_total_quota_exhausted", await ReadProblemCodeAsync(denied)); + using var scope = factory.CreateSystemScope("Verify atomic V2 practice quota"); + var db = scope.ServiceProvider.GetRequiredService(); + Assert.Equal(1, await db.PracticeSessions.CountAsync(item => + item.TenantId == seed.TenantId && item.UserId == seed.UserId && item.TargetId == releaseId)); + Assert.Equal(1, await db.StudentEntitlementsV2 + .Where(item => item.TenantId == seed.TenantId && item.UserId == seed.UserId) + .Select(item => item.UsedQuestionCount) + .SingleAsync()); + var usage = await db.PracticeDailyUsages.SingleAsync(item => + item.TenantId == seed.TenantId && + item.UserId == seed.UserId && + item.ScopeType == PracticeUsageScopeType.ProductManifest); + Assert.Equal(1, usage.UsedCount); + Assert.Equal(1, usage.FreeLimit); + } + + [Fact] + public async Task V2_session_delivers_prompt_grades_from_snapshot_and_reveals_solution() + { + await using var factory = new ApiTestFactory(); + var seed = await SeedLearningUserAsync(factory); + var releaseId = await SeedV2ReleaseAccessAsync(factory, seed, dailyQuestionLimit: 5); + using var client = factory.CreateClient(); + await LoginAsync(client, seed); + + using var catalogResponse = await client.GetAsync( + $"/api/student/catalog/resources?businessLineId={seed.BusinessLineId}"); + var catalog = await ReadJsonAsync(catalogResponse); + var catalogResource = Assert.Single(catalog.RootElement.GetProperty("resources").EnumerateArray().ToArray()); + Assert.Equal(HttpStatusCode.OK, catalogResponse.StatusCode); + Assert.Equal(releaseId, catalogResource.GetProperty("resourceId").GetGuid()); + Assert.Equal("V2 release practice", catalogResource.GetProperty("displayName").GetString()); + + using var createResponse = await client.PostAsJsonAsync( + "/api/student/learning/practice-sessions", + new CreatePracticeSessionDto + { + Mode = "chapter", + ResourceType = AccessResourceType.ContentRelease, + ResourceId = releaseId + }); + var created = await ReadJsonAsync(createResponse); + var sessionId = created.RootElement.GetProperty("id").GetGuid(); + using var detailResponse = await client.GetAsync( + $"/api/student/learning/practice-sessions/detail?practiceSessionId={sessionId}"); + var detail = await ReadJsonAsync(detailResponse); + var question = Assert.Single(detail.RootElement.GetProperty("questions").EnumerateArray().ToArray()); + var sessionQuestionId = question.GetProperty("sessionQuestionId").GetGuid(); + + Assert.Equal(HttpStatusCode.OK, createResponse.StatusCode); + Assert.Equal("1 + 1 = ?", question.GetProperty("content").GetString()); + Assert.NotEqual(Guid.Empty, question.GetProperty("questionAssetId").GetGuid()); + Assert.Empty(detail.RootElement.GetProperty("solutionsBySessionQuestion").EnumerateObject()); + + var answerRequest = new SubmitAnswerDto + { + SessionQuestionId = sessionQuestionId, + ClientSequence = 1, + IdempotencyKey = "v2-answer-1", + SelectedOptionIndices = [0] + }; + using var answerResponse = await client.PostAsJsonAsync("/api/student/learning/answers", answerRequest); + using var replayResponse = await client.PostAsJsonAsync("/api/student/learning/answers", answerRequest); + var answer = await ReadJsonAsync(answerResponse); + var replay = await ReadJsonAsync(replayResponse); + + Assert.Equal(HttpStatusCode.OK, answerResponse.StatusCode); + Assert.Equal(answer.RootElement.GetProperty("id").GetGuid(), replay.RootElement.GetProperty("id").GetGuid()); + Assert.Equal(0, answer.RootElement.GetProperty("solution").GetProperty("correctOptionIndex").GetInt32()); + + using var submitResponse = await client.PostAsJsonAsync( + "/api/student/learning/practice-sessions/submit", + new SubmitPracticeSessionDto + { + PracticeSessionId = sessionId, + IdempotencyKey = "v2-submit-1" + }); + var report = await ReadJsonAsync(submitResponse); + Assert.Equal(HttpStatusCode.OK, submitResponse.StatusCode); + Assert.Equal(1, report.RootElement.GetProperty("correctCount").GetInt32()); + Assert.Equal(1m, report.RootElement.GetProperty("score").GetDecimal()); + + using var scope = factory.CreateSystemScope("Verify V2 immutable answer replay"); + var db = scope.ServiceProvider.GetRequiredService(); + Assert.Equal(1, await db.AnswerRecords.CountAsync(item => item.PracticeSessionId == sessionId)); + var locked = await db.PracticeSessionQuestions.SingleAsync(item => item.PracticeSessionId == sessionId); + Assert.Equal(2, locked.SnapshotVersion); + Assert.NotNull(locked.QuestionRevisionId); + Assert.NotNull(locked.QuestionPlacementId); + Assert.NotNull(locked.AssessmentPolicyVersionId); + await Assert.ThrowsAnyAsync(() => db.QuestionRevisions + .Where(item => item.Id == locked.QuestionRevisionId) + .ExecuteUpdateAsync(setters => setters.SetProperty(item => item.Explanation, "tampered"))); + await Assert.ThrowsAnyAsync(() => db.ContentReleaseQuestions + .Where(item => item.Id == locked.ContentReleaseQuestionId) + .ExecuteUpdateAsync(setters => setters.SetProperty( + item => item.Status, ContentReleaseQuestionStatus.Retired))); + var manifestVersionId = await db.StudentEntitlementsV2 + .Where(item => item.UserId == seed.UserId) + .Select(item => item.ProductAccessManifestVersionId) + .SingleAsync(); + db.ChangeTracker.Clear(); + db.ProductManifestResources.Add(new ProductManifestResource + { + TenantId = seed.TenantId, + ProductAccessManifestVersionId = manifestVersionId, + ResourceType = AccessResourceType.ContentRelease, + ResourceId = Guid.NewGuid(), + DisplayName = "forbidden late child" + }); + await Assert.ThrowsAnyAsync(() => db.SaveChangesAsync()); + } + + [Fact] + public async Task V2_class_assignment_is_license_bounded_versioned_and_auditable() + { + await using var factory = new ApiTestFactory(); + var seed = await SeedLearningUserAsync(factory); + var releaseId = await SeedV2ReleaseAccessAsync(factory, seed, dailyQuestionLimit: 5); + var classId = Guid.NewGuid(); + await factory.SeedAsync( + new TenantClass + { + Id = classId, + TenantId = seed.TenantId, + Code = "v2-class", + Name = "V2 class" + }, + new TenantClassMember + { + TenantId = seed.TenantId, + ClassId = classId, + UserId = seed.UserId, + MemberType = TenantClassMemberType.Student, + Status = TenantClassMemberStatus.Active + }); + + Guid manifestVersionId; + using (var verification = factory.CreateSystemScope("Resolve V2 manifest fixture")) + { + var db = verification.ServiceProvider.GetRequiredService(); + manifestVersionId = await db.ProductManifestResources.AsNoTracking() + .Where(item => item.TenantId == seed.TenantId && + item.ResourceType == AccessResourceType.ContentRelease && + item.ResourceId == releaseId) + .Select(item => item.ProductAccessManifestVersionId) + .SingleAsync(); + } + + ClassAssignmentGrantItem created; + using (var scope = factory.CreateTenantScope(seed.TenantId, seed.TenantId.ToString("N"))) + { + var service = scope.ServiceProvider.GetRequiredService(); + created = await service.UpsertClassGrantAsync( + seed.TenantId, + seed.UserId, + new UpsertClassAssignmentGrantCommand( + null, + classId, + manifestVersionId, + AccessResourceType.ContentRelease, + releaseId, + DateTimeOffset.UtcNow.AddMinutes(-1), + DateTimeOffset.UtcNow.AddDays(7))); + } + + Assert.Equal(ClassAssignmentGrantStatus.Active, created.Status); + Assert.Equal(seed.UserId, created.CreatedBy); + ClassAssignmentGrantItem revoked; + using (var scope = factory.CreateTenantScope(seed.TenantId, seed.TenantId.ToString("N"))) + { + revoked = await scope.ServiceProvider.GetRequiredService() + .RevokeClassGrantAsync( + seed.TenantId, + seed.UserId, + classId, + created.Id, + "课程调整"); + } + + Assert.Equal(ClassAssignmentGrantStatus.Revoked, revoked.Status); + Assert.Equal(seed.UserId, revoked.RevokedBy); + Assert.Equal("课程调整", revoked.RevokedReason); + using var finalVerification = factory.CreateSystemScope("Verify V2 class grant version bump"); + var finalDb = finalVerification.ServiceProvider.GetRequiredService(); + var accessVersion = await finalDb.LearningAccessVersions.AsNoTracking().SingleAsync(item => + item.TenantId == seed.TenantId && item.UserId == seed.UserId); + Assert.True(accessVersion.GrantVersion >= 3); + } + [Fact] public async Task Learning_endpoints_require_current_tenant_member() { @@ -52,7 +265,6 @@ public sealed class LearningEndpointTests new SubmitPracticeSessionDto { PracticeSessionId = answerable.SessionId, - ExpectedSessionVersion = 1, IdempotencyKey = "wrong-submit-1" }); using var beforeProjectionResponse = await client.GetAsync("/api/student/learning/wrong-questions"); @@ -166,7 +378,7 @@ public sealed class LearningEndpointTests Assert.True(stored.AvailableAt > stored.OccurredAt); } - [Fact] + [Fact(Skip = "V1 question collections were retired by the Content Domain V2 cutover.")] public async Task Practice_session_can_be_created_detailed_submitted_and_listed() { await using var factory = new ApiTestFactory(); @@ -265,9 +477,8 @@ public sealed class LearningEndpointTests new CreatePracticeSessionDto { Mode = "chapter", - CollectionId = collectionId, - QuestionLimit = 2, - TotalScore = 100 + ResourceType = AccessResourceType.CollectionRelease, + ResourceId = collectionId }); var created = await ReadJsonAsync(createResponse); var practiceSessionId = created.RootElement.GetProperty("id").GetGuid(); @@ -304,7 +515,6 @@ public sealed class LearningEndpointTests new SubmitPracticeSessionDto { PracticeSessionId = practiceSessionId, - ExpectedSessionVersion = 1, IdempotencyKey = "practice-submit" }); var report = await ReadJsonAsync(submitResponse); @@ -497,7 +707,6 @@ public sealed class LearningEndpointTests new SubmitPracticeSessionDto { PracticeSessionId = answerable.SessionId, - ExpectedSessionVersion = 1, IdempotencyKey = "stats-wrong-submit" }); Assert.Equal(1, await ProcessLearningOutboxAsync(factory, "learning-stats-worker")); @@ -612,8 +821,7 @@ public sealed class LearningEndpointTests .OrderBy(item => item.Revision) .ToArrayAsync(); Assert.Equal(2, records.Length); - Assert.Equal(2, await db.AnswerSubmissionIdempotencies.CountAsync(item => - item.PracticeSessionId == answerable.SessionId)); + Assert.Equal(2, records.Select(item => item.IdempotencyKey).Distinct().Count()); var current = await db.CurrentAnswers.SingleAsync(item => item.PracticeSessionId == answerable.SessionId && item.SessionQuestionId == answerable.SessionQuestionId); @@ -689,7 +897,6 @@ public sealed class LearningEndpointTests new SubmitPracticeSessionDto { PracticeSessionId = answerable.SessionId, - ExpectedSessionVersion = 1, IdempotencyKey = "subjective-submit" }); var answerBody = await ReadJsonAsync(answer); @@ -730,7 +937,6 @@ public sealed class LearningEndpointTests new SubmitPracticeSessionDto { PracticeSessionId = answerable.SessionId, - ExpectedSessionVersion = 1, IdempotencyKey = "concurrent-submit-a" }), client.PostAsJsonAsync( @@ -738,7 +944,6 @@ public sealed class LearningEndpointTests new SubmitPracticeSessionDto { PracticeSessionId = answerable.SessionId, - ExpectedSessionVersion = 1, IdempotencyKey = "concurrent-submit-b" })); @@ -842,6 +1047,265 @@ public sealed class LearningEndpointTests return (tenantId, userId, phone, businessLineId); } + private static async Task SeedV2ReleaseAccessAsync( + ApiTestFactory factory, + (Guid TenantId, Guid UserId, string Phone, Guid BusinessLineId) seed, + int dailyQuestionLimit) + { + var now = DateTimeOffset.UtcNow; + var licenseId = Guid.NewGuid(); + var profileId = Guid.NewGuid(); + var profileVersionId = Guid.NewGuid(); + var curriculumId = Guid.NewGuid(); + var curriculumVersionId = Guid.NewGuid(); + var curriculumNodeId = Guid.NewGuid(); + var assessmentPolicyId = Guid.NewGuid(); + var assessmentPolicyVersionId = Guid.NewGuid(); + var questionAssetId = Guid.NewGuid(); + var questionRevisionId = Guid.NewGuid(); + var placementId = Guid.NewGuid(); + var releaseId = Guid.NewGuid(); + var segmentId = Guid.NewGuid(); + var releaseQuestionId = Guid.NewGuid(); + var productId = Guid.NewGuid(); + var manifestId = Guid.NewGuid(); + var manifestVersionId = Guid.NewGuid(); + var entitlementId = Guid.NewGuid(); + var fingerprint = new string('a', 64); + + await factory.SeedAsync( + new TenantBusinessLicense + { + Id = licenseId, + TenantId = seed.TenantId, + BusinessLineId = seed.BusinessLineId, + StartsAt = now.AddDays(-1) + }, + new ExamTargetProfile + { + Id = profileId, + BusinessLineId = seed.BusinessLineId, + Code = $"profile-{profileId:N}", + Name = "V2 target" + }, + new ExamTargetProfileVersion + { + Id = profileVersionId, + ExamTargetProfileId = profileId, + DisplayName = "V2 target 2027", + Status = ContentDefinitionStatus.Published, + PublishedAt = now + }, + new Curriculum + { + Id = curriculumId, + TenantId = seed.TenantId, + BusinessLineId = seed.BusinessLineId, + Code = $"curriculum-{curriculumId:N}", + Name = "V2 curriculum" + }, + new CurriculumVersion + { + Id = curriculumVersionId, + TenantId = seed.TenantId, + CurriculumId = curriculumId, + Name = "V2 curriculum 2027", + Status = ContentDefinitionStatus.Published, + PublishedAt = now + }, + new CurriculumNode + { + Id = curriculumNodeId, + TenantId = seed.TenantId, + CurriculumVersionId = curriculumVersionId, + Code = "math", + Name = "数学", + Kind = CurriculumNodeKind.Subject + }, + new AssessmentPolicy + { + Id = assessmentPolicyId, + TenantId = seed.TenantId, + BusinessLineId = seed.BusinessLineId, + Code = $"policy-{assessmentPolicyId:N}", + Name = "Objective" + }, + new AssessmentPolicyVersion + { + Id = assessmentPolicyVersionId, + TenantId = seed.TenantId, + AssessmentPolicyId = assessmentPolicyId, + DefaultScore = 1, + Status = ContentDefinitionStatus.Published, + PublishedAt = now + }, + new QuestionAsset + { + Id = questionAssetId, + TenantId = seed.TenantId, + DeliveryFingerprint = fingerprint, + Status = QuestionAssetStatus.Published + }, + new LearningProduct + { + Id = productId, + TenantId = seed.TenantId, + BusinessLineId = seed.BusinessLineId, + Code = $"product-{productId:N}", + Name = "V2 product" + }, + new ProductAccessManifest + { + Id = manifestId, + TenantId = seed.TenantId, + LearningProductId = productId + }); + + await factory.SeedAsync( + new QuestionRevision + { + Id = questionRevisionId, + TenantId = seed.TenantId, + QuestionAssetId = questionAssetId, + NormalizedContent = "one plus one", + Content = "1 + 1 = ?", + Options = JsonSerializer.SerializeToElement(new[] { "2", "3" }), + CorrectOptionIndex = 0, + DeliveryFingerprint = fingerprint + }, + new QuestionPlacement + { + Id = placementId, + TenantId = seed.TenantId, + QuestionAssetOwnerTenantId = seed.TenantId, + QuestionAssetId = questionAssetId, + CurriculumVersionId = curriculumVersionId, + CurriculumNodeId = curriculumNodeId, + AssessmentPolicyVersionId = assessmentPolicyVersionId, + Status = QuestionPlacementStatus.Active + }, + new ContentRelease + { + Id = releaseId, + TenantId = seed.TenantId, + BusinessLineId = seed.BusinessLineId, + CurriculumVersionId = curriculumVersionId, + Name = "V2 release", + SourceFingerprint = fingerprint, + Status = ContentReleaseStatus.Compiling + }, + new ProductAccessManifestVersion + { + Id = manifestVersionId, + TenantId = seed.TenantId, + ProductAccessManifestId = manifestId, + Status = AccessManifestStatus.Draft, + MaxActiveTargets = 1, + DailyQuestionLimit = dailyQuestionLimit, + TotalQuestionLimit = dailyQuestionLimit + }); + + await factory.SeedAsync( + new AudienceSegment + { + Id = segmentId, + TenantId = seed.TenantId, + ContentReleaseId = releaseId, + RuleHash = fingerprint + }, + new TenantLicensedTarget + { + TenantId = seed.TenantId, + TenantBusinessLicenseId = licenseId, + ExamTargetProfileVersionId = profileVersionId, + IsBaseTarget = true, + StartsAt = now.AddDays(-1) + }, + new StudentTargetSelectionHistory + { + TenantId = seed.TenantId, + UserId = seed.UserId, + BusinessLineId = seed.BusinessLineId, + ExamTargetProfileVersionId = profileVersionId, + Role = TargetSelectionRole.Primary, + EntitlementId = entitlementId, + SourceType = "student_entitlement", + SourceId = entitlementId, + EffectiveAt = now.AddDays(-1) + }, + new ProductManifestRelease + { + TenantId = seed.TenantId, + ProductAccessManifestVersionId = manifestVersionId, + ContentOwnerTenantId = seed.TenantId, + ContentReleaseId = releaseId + }, + new ProductManifestTarget + { + TenantId = seed.TenantId, + ProductAccessManifestVersionId = manifestVersionId, + ExamTargetProfileVersionId = profileVersionId, + MayBePrimary = true + }, + new ProductManifestResource + { + TenantId = seed.TenantId, + ProductAccessManifestVersionId = manifestVersionId, + ResourceType = AccessResourceType.ContentRelease, + ResourceId = releaseId, + DisplayName = "V2 release practice", + SortOrder = 10 + }, + new StudentEntitlement + { + Id = entitlementId, + TenantId = seed.TenantId, + UserId = seed.UserId, + LearningProductId = productId, + ProductAccessManifestVersionId = manifestVersionId, + StartsAt = now.AddDays(-1), + EndsAt = now.AddDays(30), + SourceType = "integration_test" + }); + + await factory.SeedAsync( + new AudienceSegmentMember + { + TenantId = seed.TenantId, + ContentReleaseId = releaseId, + AudienceSegmentId = segmentId, + ExamTargetProfileVersionId = profileVersionId + }, + new ContentReleaseQuestion + { + Id = releaseQuestionId, + TenantId = seed.TenantId, + ContentReleaseId = releaseId, + AudienceSegmentId = segmentId, + CurriculumNodeId = curriculumNodeId, + QuestionPlacementId = placementId, + QuestionAssetOwnerTenantId = seed.TenantId, + QuestionAssetId = questionAssetId, + QuestionRevisionId = questionRevisionId, + AssessmentPolicyVersionId = assessmentPolicyVersionId, + QuestionType = "choice" + }); + + using var scope = factory.CreateSystemScope("Finalize V2 fixture current revisions"); + var db = scope.ServiceProvider.GetRequiredService(); + await db.QuestionAssets.Where(item => item.Id == questionAssetId) + .ExecuteUpdateAsync(setters => setters.SetProperty(item => item.CurrentRevisionId, questionRevisionId)); + await db.ContentReleases.Where(item => item.Id == releaseId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(item => item.Status, ContentReleaseStatus.Published) + .SetProperty(item => item.PublishedAt, now)); + await db.ProductAccessManifestVersions.Where(item => item.Id == manifestVersionId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(item => item.Status, AccessManifestStatus.Published) + .SetProperty(item => item.PublishedAt, now)); + return releaseId; + } + private static async Task<(Guid SessionQuestionId, Guid SessionId, Guid VersionId, Guid ReferenceId)> SeedAnswerableQuestionAsync( ApiTestFactory factory, @@ -966,7 +1430,6 @@ public sealed class LearningEndpointTests new SubmitPracticeSessionDto { PracticeSessionId = sessionId, - ExpectedSessionVersion = 1, IdempotencyKey = $"{keyPrefix}-submit" }); Assert.Equal(HttpStatusCode.OK, submit.StatusCode); diff --git a/Tiku.IntegrationTests/Api/PlatformQuestionBankEndpointTests.cs b/Tiku.IntegrationTests/Api/PlatformQuestionBankEndpointTests.cs deleted file mode 100644 index 504c221..0000000 --- a/Tiku.IntegrationTests/Api/PlatformQuestionBankEndpointTests.cs +++ /dev/null @@ -1,296 +0,0 @@ -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Tiku.Application.PlatformAdmin; -using Tiku.Application.Security; -using Tiku.Domain.Common; -using Tiku.Domain.Content; -using Tiku.Domain.Identity; -using Tiku.Domain.Operations; -using Tiku.Domain.Platform; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.IntegrationTests.Api; - -public sealed class PlatformQuestionBankEndpointTests -{ - private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) - { - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) } - }; - - [Fact] - public async Task Platform_question_bank_workflow_is_scoped_versioned_and_idempotent() - { - await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary - { - ["Tenancy:Resolution:PlatformHosts:0"] = "localhost" - }); - var platformTenantId = Guid.NewGuid(); - var ordinaryTenantId = Guid.NewGuid(); - var ordinaryBankId = Guid.NewGuid(); - var platformUser = await SeedPlatformQuestionBankUserAsync(factory); - await factory.SeedAsync( - new Tenant - { - Id = platformTenantId, - Slug = "platform-content-test", - Name = "平台公共内容", - Mode = TenantMode.PlatformOwned, - Status = TenantStatus.Active, - BillingStatus = BillingStatus.Active - }, - new Tenant - { - Id = ordinaryTenantId, - Slug = "ordinary-question-bank", - Name = "普通租户", - Status = TenantStatus.Active, - BillingStatus = BillingStatus.Active - }, - new QuestionBank { Id = ordinaryBankId, TenantId = ordinaryTenantId, Name = "普通租户私有题库" }); - - using var client = factory.CreateClient(); - client.UseAccessToken(await client.LoginAsPlatformAsync(platformUser.Email)); - - var bankResponse = await client.PutAsJsonAsync("/api/platform/question-banks", - new UpsertPlatformQuestionBankCommand(null, "平台高等数学公共题库", JsonDefaults.Object())); - Assert.True(bankResponse.StatusCode == HttpStatusCode.OK, await bankResponse.Content.ReadAsStringAsync()); - var bank = await bankResponse.Content.ReadFromJsonAsync(JsonOptions); - Assert.NotNull(bank); - Assert.NotNull(bank.ContentEntryId); - - var nodeResponse = await client.PutAsJsonAsync("/api/platform/question-banks/nodes", - new UpsertPlatformQuestionBankNodeCommand( - null, bank.Id, null, "math-chapter-1", "函数、极限与连续", ContentNodeType.Chapter, 10, true, - JsonDefaults.Object())); - Assert.Equal(HttpStatusCode.OK, nodeResponse.StatusCode); - var node = await nodeResponse.Content.ReadFromJsonAsync(JsonOptions); - Assert.NotNull(node); - - var createQuestion = QuestionCommand(null, bank.Id, node.Id, "platform-question-1", "函数极限的定义是什么?"); - var questionResponse = await client.PutAsJsonAsync("/api/platform/question-banks/questions", createQuestion); - Assert.Equal(HttpStatusCode.OK, questionResponse.StatusCode); - var question = await questionResponse.Content.ReadFromJsonAsync(JsonOptions); - Assert.NotNull(question); - Assert.Equal(1, question.VersionNo); - - var updatedResponse = await client.PutAsJsonAsync( - "/api/platform/question-banks/questions", - createQuestion with { Id = question.Id, Content = "请说明函数极限的严格定义。" }); - Assert.Equal(HttpStatusCode.OK, updatedResponse.StatusCode); - var updated = await updatedResponse.Content.ReadFromJsonAsync(JsonOptions); - Assert.NotNull(updated); - Assert.Equal(2, updated.VersionNo); - - var import = new PlatformQuestionImportCommand( - bank.Id, - node.Id, - "simple", - "平台普通 JSON 回归", - JsonSerializer.SerializeToElement(new object[] - { - new { legacyId = "import-stable-1", type = "true_false", content = "函数极限一定存在。", answerText = "错误" }, - new { legacyId = "invalid-1", type = "choice", content = "" } - })); - var jobsBeforePreview = await CountImportJobsAsync(factory, platformTenantId); - var previewResponse = await client.PostAsJsonAsync("/api/platform/question-banks/imports/preview", import); - Assert.Equal(HttpStatusCode.OK, previewResponse.StatusCode); - var preview = await previewResponse.Content.ReadFromJsonAsync(JsonOptions); - Assert.NotNull(preview); - Assert.Equal(2, preview.Detail.Job.TotalCount); - Assert.Equal(1, preview.Detail.Job.ValidCount); - Assert.Equal(1, preview.Detail.Job.ErrorCount); - Assert.Equal(jobsBeforePreview, await CountImportJobsAsync(factory, platformTenantId)); - - var firstImportResponse = await client.PostAsJsonAsync("/api/platform/question-banks/imports", import); - var repeatedImportResponse = await client.PostAsJsonAsync("/api/platform/question-banks/imports", import); - Assert.True(firstImportResponse.StatusCode == HttpStatusCode.OK, - await firstImportResponse.Content.ReadAsStringAsync()); - Assert.True(repeatedImportResponse.StatusCode == HttpStatusCode.OK, - await repeatedImportResponse.Content.ReadAsStringAsync()); - var firstImport = - await firstImportResponse.Content.ReadFromJsonAsync(JsonOptions); - var repeatedImport = - await repeatedImportResponse.Content.ReadFromJsonAsync(JsonOptions); - Assert.NotNull(firstImport); - Assert.NotNull(repeatedImport); - Assert.Equal(1, firstImport.InsertedQuestionCount); - Assert.Equal(1, repeatedImport.SkippedQuestionCount); - - var structuredImport = new PlatformQuestionImportCommand( - bank.Id, - null, - "structured-v2", - "平台结构化 JSON 回归", - JsonSerializer.SerializeToElement(new - { - _tikuExport = "2.0", - subjects = new[] - { - new - { - code = "advanced-math", - name = "高等数学", - chapters = new[] - { - new - { - code = "limits", - name = "极限", - questions = new[] - { - new - { - legacyId = "structured-stable-1", type = "choice", content = "下列极限存在的是?", - options = new[] { "A", "B" }, answerText = "A" - } - } - } - } - } - } - })); - var structuredResponse = await client.PostAsJsonAsync("/api/platform/question-banks/imports", structuredImport); - var repeatedStructuredResponse = - await client.PostAsJsonAsync("/api/platform/question-banks/imports", structuredImport); - Assert.True(structuredResponse.StatusCode == HttpStatusCode.OK, - await structuredResponse.Content.ReadAsStringAsync()); - Assert.True(repeatedStructuredResponse.StatusCode == HttpStatusCode.OK, - await repeatedStructuredResponse.Content.ReadAsStringAsync()); - var structured = await structuredResponse.Content.ReadFromJsonAsync(JsonOptions); - var repeatedStructured = - await repeatedStructuredResponse.Content.ReadFromJsonAsync(JsonOptions); - Assert.NotNull(structured); - Assert.NotNull(repeatedStructured); - Assert.Equal(2, structured.CreatedNodeCount); - Assert.Equal(1, structured.InsertedQuestionCount); - Assert.Equal(0, repeatedStructured.CreatedNodeCount); - Assert.Equal(1, repeatedStructured.SkippedQuestionCount); - - var listedResponse = - await client.GetAsync( - $"/api/platform/question-banks/questions?questionBankId={bank.Id}&page=1&pageSize=20"); - Assert.Equal(HttpStatusCode.OK, listedResponse.StatusCode); - var listed = await listedResponse.Content.ReadFromJsonAsync(JsonOptions); - Assert.NotNull(listed); - Assert.Equal(3, listed.Total); - - var bankList = - await client.GetFromJsonAsync("/api/platform/question-banks?status=all", - JsonOptions); - Assert.NotNull(bankList); - Assert.Contains(bankList, item => item.Id == bank.Id); - Assert.DoesNotContain(bankList, item => item.Id == ordinaryBankId); - - using var scope = factory.CreateSystemScope("验证平台公共题库租户隔离与版本"); - var dbContext = scope.ServiceProvider.GetRequiredService(); - Assert.Equal(platformTenantId, - await dbContext.QuestionBanks.Where(item => item.Id == bank.Id).Select(item => item.TenantId) - .SingleAsync()); - Assert.Equal(2, - await dbContext.QuestionVersions.CountAsync(item => - item.TenantId == platformTenantId && item.QuestionId == question.Id)); - Assert.Equal(1, - await dbContext.Questions.CountAsync(item => - item.TenantId == platformTenantId && item.LegacyId == "import-stable-1")); - } - - [Fact] - public async Task Platform_question_bank_rejects_missing_permission_and_non_platform_host() - { - await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary - { - ["Tenancy:Resolution:PlatformHosts:0"] = "platform.example.test" - }); - await factory.SeedAsync( - new Tenant - { - Id = Guid.NewGuid(), - Slug = "platform-content-auth", - Name = "平台公共内容", - Mode = TenantMode.PlatformOwned - }); - var platformWithoutPermission = await SeedPlatformUserAsync(factory, BackendPermissions.PlatformDashboardView); - using var client = factory.CreateClient(); - client.UseAccessToken(await client.LoginAsPlatformAsync(platformWithoutPermission.Email)); - - using var wrongHostRequest = new HttpRequestMessage(HttpMethod.Get, "/api/platform/question-banks"); - wrongHostRequest.Headers.Host = "tenant.example.test"; - var wrongHost = await client.SendAsync(wrongHostRequest); - - using var platformHostRequest = new HttpRequestMessage(HttpMethod.Get, "/api/platform/question-banks"); - platformHostRequest.Headers.Host = "platform.example.test"; - var missingPermission = await client.SendAsync(platformHostRequest); - - Assert.Equal(HttpStatusCode.NotFound, wrongHost.StatusCode); - Assert.Equal(HttpStatusCode.Forbidden, missingPermission.StatusCode); - } - - private static UpsertPlatformQuestionCommand QuestionCommand(Guid? id, Guid bankId, Guid nodeId, string legacyId, - string content) - { - return new UpsertPlatformQuestionCommand( - id, bankId, nodeId, legacyId, "short_answer", "简答题", 3, - JsonSerializer.SerializeToElement(new[] { "极限" }), content, JsonDefaults.Array(), null, - JsonDefaults.Array(), - "按定义作答", "考查函数极限定义", JsonDefaults.Array(), null, null, null, "published", JsonDefaults.Object(), null); - } - - private static async Task CountImportJobsAsync(ApiTestFactory factory, Guid tenantId) - { - using var scope = factory.CreateSystemScope("统计平台公共题库导入任务"); - return await scope.ServiceProvider.GetRequiredService().ContentImportJobs - .CountAsync(item => item.TenantId == tenantId); - } - - private static Task<(Guid UserId, string Email)> SeedPlatformQuestionBankUserAsync(ApiTestFactory factory) - { - return SeedPlatformUserAsync(factory, BackendPermissions.PlatformQuestionBankManage); - } - - private static async Task<(Guid UserId, string Email)> SeedPlatformUserAsync(ApiTestFactory factory, - string permissionCode) - { - var userId = Guid.NewGuid(); - var roleId = Guid.NewGuid(); - var email = $"platform-question-bank-{Guid.NewGuid():N}@example.test"; - var moduleCode = PermissionModuleCatalog.ResolvePermissionModuleCode(permissionCode); - await factory.SeedAsync( - new PermissionModule { Code = moduleCode, Name = "平台公共题库", Area = BackendPermissionArea.Platform }, - new BackendPermission - { - Code = permissionCode, - Name = "平台公共题库运营", - Area = BackendPermissionArea.Platform, - PermissionModuleCode = moduleCode, - IsSystem = true - }, - new User - { - Id = userId, - Email = email, - NormalizedEmail = email.ToUpperInvariant(), - UserName = email, - NormalizedUserName = email.ToUpperInvariant(), - Name = "平台题库运营", - PrimaryRole = "platform_admin", - RawProfile = JsonDefaults.Object() - }.WithTestPassword(), - new PlatformBackendRole - { - Id = roleId, - Code = $"platform_question_bank_{roleId:N}", - Name = "平台题库运营", - Status = BackendRoleStatus.Active - }, - new PlatformBackendRolePermission { RoleId = roleId, PermissionCode = permissionCode }, - new PlatformBackendUserRole { UserId = userId, RoleId = roleId }); - return (userId, email); - } -} \ No newline at end of file diff --git a/Tiku.IntegrationTests/Api/QuestionBankEndpointTests.cs b/Tiku.IntegrationTests/Api/QuestionBankEndpointTests.cs deleted file mode 100644 index 14bad74..0000000 --- a/Tiku.IntegrationTests/Api/QuestionBankEndpointTests.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System.Net; -using System.Text.Json; -using Tiku.Domain.Catalog; -using Tiku.Domain.Common; -using Tiku.Domain.Content; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; - -namespace Tiku.IntegrationTests.Api; - -public sealed class QuestionBankEndpointTests -{ - [Fact] - public async Task Question_banks_return_active_tenant_banks_only() - { - var tenantId = Guid.NewGuid(); - var otherTenantId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - PlatformTenant(), - Tenant(tenantId, "master"), - Tenant(otherTenantId, "other"), - new QuestionBank - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Name = "A 题库", - Status = QuestionBankStatus.Active - }, - new QuestionBank - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Name = "归档题库", - Status = QuestionBankStatus.Archived - }, - new QuestionBank - { - Id = Guid.NewGuid(), - TenantId = otherTenantId, - Name = "其他租户题库", - Status = QuestionBankStatus.Active - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = await client.GetAsync("/api/student/catalog/question-banks?tenantCode=master"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("A 题库", Assert.Single(items).GetProperty("name").GetString()); - } - - [Fact] - public async Task Questions_support_filters_and_current_version_projection() - { - var tenantId = Guid.NewGuid(); - var bankId = Guid.NewGuid(); - var subjectId = Guid.NewGuid(); - var categoryId = Guid.NewGuid(); - var collectionId = Guid.NewGuid(); - var includedQuestionId = Guid.NewGuid(); - var excludedQuestionId = Guid.NewGuid(); - var versionId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - PlatformTenant(), - Tenant(tenantId, "master"), - new Subject { Id = subjectId, TenantId = tenantId, Name = "测试科目" }, - new Category { Id = categoryId, TenantId = tenantId, SubjectId = subjectId, Name = "测试分类" }, - new QuestionBank { Id = bankId, TenantId = tenantId, Name = "题库" }, - new QuestionCollection - { Id = collectionId, TenantId = tenantId, Name = "题集", Status = ContentStatus.Active }, - new Question - { - Id = excludedQuestionId, - TenantId = tenantId, - QuestionBankId = bankId, - SubjectId = subjectId, - Type = "choice", - Status = QuestionStatus.Archived - }); - await factory.SeedQuestionWithVersionAsync( - new Question - { - Id = includedQuestionId, - TenantId = tenantId, - QuestionBankId = bankId, - SubjectId = subjectId, - CategoryId = categoryId, - Type = "choice", - TypeLabel = "单选题", - Difficulty = 2, - Status = QuestionStatus.Published - }, - new QuestionVersion - { - Id = versionId, - TenantId = tenantId, - QuestionId = includedQuestionId, - VersionNo = 2, - Content = "题干关键词" - }); - var questionReference = await factory.SeedQuestionReferenceAsync( - tenantId, - tenantId, - includedQuestionId); - await factory.SeedAsync(new QuestionCollectionItem - { - Id = Guid.NewGuid(), - TenantId = tenantId, - CollectionId = collectionId, - QuestionReferenceId = questionReference.Id, - QuestionOwnerTenantId = tenantId, - QuestionId = includedQuestionId - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = await client.GetAsync( - $"/api/student/catalog/questions?tenantCode=master&questionBankId={bankId}&subjectId={subjectId}&categoryId={categoryId}&collectionId={collectionId}&type=choice&keyword=关键词"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var item = Assert.Single(items); - Assert.Equal(includedQuestionId, item.GetProperty("id").GetGuid()); - Assert.Equal(versionId, item.GetProperty("versionId").GetGuid()); - Assert.Equal("题干关键词", item.GetProperty("content").GetString()); - Assert.False(item.TryGetProperty("correctOptionIndex", out _)); - Assert.False(item.TryGetProperty("correctOptionIndices", out _)); - Assert.False(item.TryGetProperty("answerText", out _)); - Assert.False(item.TryGetProperty("explanation", out _)); - Assert.False(item.TryGetProperty("subQuestions", out _)); - } - - [Fact] - public async Task Question_detail_returns_not_found_for_archived_question() - { - var tenantId = Guid.NewGuid(); - var questionId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - PlatformTenant(), - Tenant(tenantId, "master"), - new Question - { - Id = questionId, - TenantId = tenantId, - Type = "choice", - Status = QuestionStatus.Archived - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = await client.GetAsync($"/api/student/catalog/questions/{questionId}?tenantCode=master"); - var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); - - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - Assert.Equal("question_not_found", body.RootElement.GetProperty("code").GetString()); - } - - [Fact] - public async Task Public_question_version_history_is_retired() - { - var tenantId = Guid.NewGuid(); - var questionId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - PlatformTenant(), - Tenant(tenantId, "master"), - new Question - { - Id = questionId, - TenantId = tenantId, - Type = "choice", - Status = QuestionStatus.Published - }, - new QuestionVersion - { - Id = Guid.NewGuid(), - TenantId = tenantId, - QuestionId = questionId, - VersionNo = 1, - Content = "旧版本" - }, - new QuestionVersion - { - Id = Guid.NewGuid(), - TenantId = tenantId, - QuestionId = questionId, - VersionNo = 2, - Content = "新版本" - }); - using var client = factory.CreateClient(); - - using var response = - await client.GetAsync($"/api/public/catalog/questions/{questionId}/versions?tenantCode=master"); - var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); - - Assert.Equal(HttpStatusCode.Gone, response.StatusCode); - Assert.Equal("public_catalog_retired", body.RootElement.GetProperty("code").GetString()); - } - - private static Tenant Tenant(Guid id, string slug) - { - return new Tenant - { - Id = id, - Slug = slug, - Name = slug, - Status = TenantStatus.Active, - Metadata = JsonDefaults.Object() - }; - } - - private static Tenant PlatformTenant() - { - return new Tenant - { - Id = Guid.NewGuid(), - Slug = $"platform-{Guid.NewGuid():N}", - Name = "Platform Question Bank", - Status = TenantStatus.Active, - Mode = TenantMode.PlatformOwned, - Metadata = JsonDefaults.Object() - }; - } - - private static async Task ReadItemsAsync(HttpResponseMessage response) - { - var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); - return body.RootElement - .GetProperty("items") - .EnumerateArray() - .Select(item => item.Clone()) - .ToArray(); - } -} diff --git a/Tiku.IntegrationTests/Api/RetiredCatalogEndpointTests.cs b/Tiku.IntegrationTests/Api/RetiredCatalogEndpointTests.cs new file mode 100644 index 0000000..0f03c5f --- /dev/null +++ b/Tiku.IntegrationTests/Api/RetiredCatalogEndpointTests.cs @@ -0,0 +1,68 @@ +using System.Net; +using Tiku.Domain.Identity; +using Tiku.Domain.Tenancy; + +namespace Tiku.IntegrationTests.Api; + +public sealed class RetiredCatalogEndpointTests +{ + [Theory] + [InlineData("/api/student/catalog/regions")] + [InlineData("/api/student/catalog/question-banks")] + [InlineData("/api/student/catalog/questions")] + [InlineData("/api/student/catalog/question-collections/questions")] + public async Task V1_student_catalog_routes_are_removed(string route) + { + await using var factory = new ApiTestFactory(); + var tenantId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + var phone = $"137{Random.Shared.Next(10_000_000, 99_999_999)}"; + await factory.SeedAsync( + new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Retired route tenant" }, + new User { Id = userId, Phone = phone, Name = "Route verifier" }.WithTestPassword(), + new TenantMembership + { + TenantId = tenantId, + UserId = userId, + Role = TenantRole.Student, + Status = MembershipStatus.Active + }); + using var client = factory.CreateClient(); + client.UseAccessToken(await client.LoginAsTenantAsync(tenantId, phone)); + + using var response = await client.GetAsync(route); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("/api/tenant/content/entries", HttpStatusCode.NotFound)] + [InlineData("/api/tenant/content/nodes", HttpStatusCode.NotFound)] + [InlineData("/api/tenant/content/question-collections", HttpStatusCode.NotFound)] + [InlineData("/api/tenant/content/practice-blueprints", HttpStatusCode.NotFound)] + [InlineData("/api/tenant/content/imports/field-mapping?importType=questions", HttpStatusCode.MethodNotAllowed)] + [InlineData("/api/tenant/content/imports/templates?importType=questions&format=csv", HttpStatusCode.MethodNotAllowed)] + public async Task V1_tenant_content_routes_are_removed(string route, HttpStatusCode expectedStatus) + { + await using var factory = new ApiTestFactory(); + var tenantId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + var phone = $"136{Random.Shared.Next(10_000_000, 99_999_999)}"; + await factory.SeedAsync( + new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "V2 content tenant" }, + new User { Id = userId, Phone = phone, Name = "Content route verifier" }.WithTestPassword(), + new TenantMembership + { + TenantId = tenantId, + UserId = userId, + Role = TenantRole.TenantAdmin, + Status = MembershipStatus.Active + }); + using var client = factory.CreateClient(); + client.UseAccessToken(await client.LoginAsTenantAsync(tenantId, phone)); + + using var response = await client.GetAsync(route); + + Assert.Equal(expectedStatus, response.StatusCode); + } +} diff --git a/Tiku.IntegrationTests/Api/StudyContentEndpointTests.cs b/Tiku.IntegrationTests/Api/StudyContentEndpointTests.cs deleted file mode 100644 index faa6170..0000000 --- a/Tiku.IntegrationTests/Api/StudyContentEndpointTests.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System.Net; -using System.Text.Json; -using Tiku.Domain.Catalog; -using Tiku.Domain.Common; -using Tiku.Domain.Content; -using Tiku.Domain.Tenancy; - -namespace Tiku.IntegrationTests.Api; - -public sealed class StudyContentEndpointTests -{ - [Fact] - public async Task Vocabulary_units_are_active_sorted_and_region_scoped() - { - var tenantId = Guid.NewGuid(); - var regionId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new Region { Id = regionId, TenantId = tenantId, Name = "测试地区" }, - new VocabularyUnit - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Name = "核心词汇", - SortOrder = 2, - IsActive = true - }, - new VocabularyUnit - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Name = "全局词汇", - SortOrder = 1, - IsActive = true - }, - new VocabularyUnit - { - Id = Guid.NewGuid(), - TenantId = tenantId, - RegionId = regionId, - Name = "停用词汇", - SortOrder = 0, - IsActive = false - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = - await client.GetAsync($"/api/student/catalog/vocabulary-units?tenantCode=master®ionId={regionId}"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(["全局词汇", "核心词汇"], items.Select(item => item.GetProperty("name").GetString()!).ToArray()); - } - - [Fact] - public async Task Vocabulary_words_support_unit_and_keyword_filters() - { - var tenantId = Guid.NewGuid(); - var unitId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new VocabularyUnit { Id = unitId, TenantId = tenantId, Name = "Unit 1" }, - new VocabularyWord - { - Id = Guid.NewGuid(), - TenantId = tenantId, - UnitId = unitId, - Word = "abandon", - Meaning = "放弃", - SortOrder = 1, - IsActive = true - }, - new VocabularyWord - { - Id = Guid.NewGuid(), - TenantId = tenantId, - UnitId = unitId, - Word = "ability", - Meaning = "能力", - SortOrder = 2, - IsActive = true - }, - new VocabularyWord - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Word = "other", - Meaning = "其他", - IsActive = true - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = - await client.GetAsync($"/api/student/catalog/vocabulary-words?tenantCode=master&unitId={unitId}&keyword=放弃"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var item = Assert.Single(items); - Assert.Equal("abandon", item.GetProperty("word").GetString()); - } - - [Fact] - public async Task Handbook_chapters_are_filtered_by_subject() - { - var tenantId = Guid.NewGuid(); - var subjectId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new HandbookSubject - { - Id = subjectId, - TenantId = tenantId, - Name = "语文手册", - IsActive = true - }, - new HandbookChapter - { - Id = Guid.NewGuid(), - TenantId = tenantId, - SubjectId = subjectId, - Name = "古诗词", - SortOrder = 1, - IsActive = true - }, - new HandbookChapter - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Name = "其他章节", - SortOrder = 2, - IsActive = true - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var response = - await client.GetAsync($"/api/student/catalog/handbook-chapters?tenantCode=master&subjectId={subjectId}"); - var items = await ReadItemsAsync(response); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("古诗词", Assert.Single(items).GetProperty("name").GetString()); - } - - [Fact] - public async Task Handbook_entries_hide_content_until_requested() - { - var tenantId = Guid.NewGuid(); - var chapterId = Guid.NewGuid(); - await using var factory = new ApiTestFactory(); - await factory.SeedAsync( - Tenant(tenantId, "master"), - new HandbookChapter - { - Id = chapterId, - TenantId = tenantId, - Name = "章节", - IsActive = true - }, - new HandbookEntry - { - Id = Guid.NewGuid(), - TenantId = tenantId, - ChapterId = chapterId, - Title = "知识点", - Summary = "摘要", - Content = "完整正文", - SortOrder = 1, - IsActive = true - }); - using var client = factory.CreateClient(); - - await factory.AuthorizeStudentCatalogAsync(client, tenantId); - using var listResponse = - await client.GetAsync($"/api/student/catalog/handbook-entries?tenantCode=master&chapterId={chapterId}"); - using var detailResponse = - await client.GetAsync( - $"/api/student/catalog/handbook-entries?tenantCode=master&chapterId={chapterId}&includeContent=true"); - var listItem = Assert.Single(await ReadItemsAsync(listResponse)); - var detailItem = Assert.Single(await ReadItemsAsync(detailResponse)); - - Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, detailResponse.StatusCode); - Assert.Equal(JsonValueKind.Null, listItem.GetProperty("content").ValueKind); - Assert.Equal("完整正文", detailItem.GetProperty("content").GetString()); - } - - private static Tenant Tenant(Guid id, string slug) - { - return new Tenant - { - Id = id, - Slug = slug, - Name = slug, - Status = TenantStatus.Active, - Metadata = JsonDefaults.Object() - }; - } - - private static async Task ReadItemsAsync(HttpResponseMessage response) - { - var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); - return body.RootElement - .GetProperty("items") - .EnumerateArray() - .Select(item => item.Clone()) - .ToArray(); - } -} diff --git a/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs b/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs index e2acfba..c1fb4bc 100644 --- a/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs +++ b/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs @@ -592,10 +592,6 @@ public sealed class ArchitectureBoundaryTests typeof(Tiku.Application.Content.IScorelineManagementService), typeof(Tiku.Application.Content.IVideoManagementService), typeof(Tiku.Application.Content.IOperationContentManagementService), - typeof(Tiku.Application.Content.IContentEntryManagementService), - typeof(Tiku.Application.Content.IContentNodeManagementService), - typeof(Tiku.Application.Content.IQuestionCollectionManagementService), - typeof(Tiku.Application.Content.IPracticeBlueprintManagementService), typeof(Tiku.Application.Content.IContentImportService), typeof(Tiku.Application.Assets.IAssetCatalogManagementService), typeof(Tiku.Application.Assets.IAssetUploadManagementService), @@ -610,11 +606,10 @@ public sealed class ArchitectureBoundaryTests typeof(Tiku.Application.Growth.IStudentReferralService), typeof(Tiku.Application.Growth.IReferralAdministrationService), typeof(Tiku.Application.Growth.IReferralAnalyticsService), - typeof(Tiku.Application.PlatformAdmin.IPlatformQuestionBankCatalogService), - typeof(Tiku.Application.PlatformAdmin.IPlatformQuestionBankNodeService), - typeof(Tiku.Application.PlatformAdmin.IPlatformQuestionAdministrationService), - typeof(Tiku.Application.PlatformAdmin.IPlatformQuestionImportService), - typeof(Tiku.Application.PlatformAdmin.IPlatformQuestionAssetService), + typeof(Tiku.Application.Content.IContentV2AuthoringService), + typeof(Tiku.Application.Content.IContentReleaseCompiler), + typeof(Tiku.Application.Content.IPlatformContentActorResolver), + typeof(Tiku.Application.Learning.IV2LearningAccessAdministrationService), typeof(Tiku.Application.Commerce.IPaymentConfigurationService), typeof(Tiku.Application.Commerce.ICommerceOrderAdministrationService), typeof(Tiku.Application.Commerce.IActivationCodeAdministrationService), diff --git a/Tiku.IntegrationTests/ContentV2/ContentReleaseCompilerTests.cs b/Tiku.IntegrationTests/ContentV2/ContentReleaseCompilerTests.cs new file mode 100644 index 0000000..879c14d --- /dev/null +++ b/Tiku.IntegrationTests/ContentV2/ContentReleaseCompilerTests.cs @@ -0,0 +1,236 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tiku.Application.Content; +using Tiku.Domain.Content; +using Tiku.Domain.Learning; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Persistence; +using Tiku.IntegrationTests.Api; + +namespace Tiku.IntegrationTests.ContentV2; + +public sealed class ContentReleaseCompilerTests +{ + [Fact] + public async Task Publishing_new_revision_creates_new_release_without_mutating_old_delivery() + { + await using var factory = new ApiTestFactory(); + var fixture = await SeedAsync(factory); + using var scope = factory.CreateSystemScope("Compile immutable V2 releases"); + var compiler = scope.ServiceProvider.GetRequiredService(); + var authoring = scope.ServiceProvider.GetRequiredService(); + + var first = await compiler.PublishAsync(new PublishContentReleaseCommand( + fixture.TenantId, + fixture.ActorUserId, + fixture.CurriculumVersionId, + "Release 1")); + var revised = await authoring.CreateRevisionAsync( + new ContentAuthorActor(fixture.TenantId, fixture.ActorUserId), + fixture.QuestionAssetId, + Draft("2 + 2 = ?", 1), + true); + var second = await compiler.PublishAsync(new PublishContentReleaseCommand( + fixture.TenantId, + fixture.ActorUserId, + fixture.CurriculumVersionId, + "Release 2")); + + var db = scope.ServiceProvider.GetRequiredService(); + var firstQuestion = await db.ContentReleaseQuestions.AsNoTracking() + .SingleAsync(item => item.ContentReleaseId == first.ContentReleaseId); + var secondQuestion = await db.ContentReleaseQuestions.AsNoTracking() + .SingleAsync(item => item.ContentReleaseId == second.ContentReleaseId); + Assert.Equal(1, first.ReleaseNo); + Assert.Equal(2, second.ReleaseNo); + Assert.Equal(fixture.QuestionRevisionId, firstQuestion.QuestionRevisionId); + Assert.Equal(revised.QuestionRevisionId, secondQuestion.QuestionRevisionId); + Assert.NotEqual(firstQuestion.QuestionRevisionId, secondQuestion.QuestionRevisionId); + Assert.Equal(2, await db.LearningOutboxMessages.CountAsync(item => + item.EventType == "content_release_published")); + Assert.All(await db.ContentReleases.AsNoTracking().ToArrayAsync(), release => + Assert.Equal(ContentReleaseStatus.Published, release.Status)); + } + + private static async Task SeedAsync(ApiTestFactory factory) + { + var tenantId = Guid.NewGuid(); + var platformOwnerId = Guid.NewGuid(); + var actorUserId = Guid.NewGuid(); + var businessLineId = Guid.NewGuid(); + var profileId = Guid.NewGuid(); + var curriculumId = Guid.NewGuid(); + var curriculumVersionId = Guid.NewGuid(); + var curriculumNodeId = Guid.NewGuid(); + var policyId = Guid.NewGuid(); + var policyVersionId = Guid.NewGuid(); + var questionAssetId = Guid.NewGuid(); + var questionRevisionId = Guid.NewGuid(); + var placementId = Guid.NewGuid(); + var now = DateTimeOffset.UtcNow; + var draft = Draft("1 + 1 = ?", 0); + var fingerprint = QuestionDeliveryFingerprint.Compute(ToFingerprintInput(draft)); + + await factory.SeedAsync( + new Tenant + { + Id = platformOwnerId, + Slug = $"platform-{platformOwnerId:N}", + Name = "Platform owner", + Mode = TenantMode.PlatformOwned + }, + new Tenant + { + Id = tenantId, + Slug = $"tenant-{tenantId:N}", + Name = "V2 compiler tenant" + }, + new BusinessLine + { + Id = businessLineId, + Code = $"business-{businessLineId:N}", + Name = "Extensible exam business" + }, + new ExamTargetProfile + { + Id = profileId, + BusinessLineId = businessLineId, + Code = $"profile-{profileId:N}", + Name = "Profile" + }, + new Curriculum + { + Id = curriculumId, + TenantId = tenantId, + BusinessLineId = businessLineId, + Code = $"curriculum-{curriculumId:N}", + Name = "Curriculum" + }, + new AssessmentPolicy + { + Id = policyId, + TenantId = tenantId, + BusinessLineId = businessLineId, + Code = $"policy-{policyId:N}", + Name = "Policy" + }, + new QuestionAsset + { + Id = questionAssetId, + TenantId = tenantId, + DeliveryFingerprint = fingerprint, + Status = QuestionAssetStatus.Published, + CreatedBy = actorUserId, + UpdatedBy = actorUserId + }); + await factory.SeedAsync( + new ExamTargetProfileVersion + { + ExamTargetProfileId = profileId, + DisplayName = "Profile 2027", + Status = ContentDefinitionStatus.Published, + PublishedAt = now + }, + new CurriculumVersion + { + Id = curriculumVersionId, + TenantId = tenantId, + CurriculumId = curriculumId, + Name = "Curriculum 2027", + Status = ContentDefinitionStatus.Published, + PublishedAt = now + }, + new AssessmentPolicyVersion + { + Id = policyVersionId, + TenantId = tenantId, + AssessmentPolicyId = policyId, + DefaultScore = 1, + Status = ContentDefinitionStatus.Published, + PublishedAt = now + }, + new QuestionRevision + { + Id = questionRevisionId, + TenantId = tenantId, + QuestionAssetId = questionAssetId, + QuestionType = draft.QuestionType, + NormalizedContent = QuestionDeliveryFingerprint.NormalizePrompt(draft.Content), + Content = draft.Content, + Options = draft.Options, + CorrectOptionIndex = draft.CorrectOptionIndex, + DeliveryFingerprint = fingerprint, + CreatedBy = actorUserId + }); + await factory.SeedAsync( + new CurriculumNode + { + Id = curriculumNodeId, + TenantId = tenantId, + CurriculumVersionId = curriculumVersionId, + Code = "math", + Name = "Math", + Kind = CurriculumNodeKind.Subject + }); + await factory.SeedAsync( + new QuestionPlacement + { + Id = placementId, + TenantId = tenantId, + QuestionAssetOwnerTenantId = tenantId, + QuestionAssetId = questionAssetId, + CurriculumVersionId = curriculumVersionId, + CurriculumNodeId = curriculumNodeId, + AssessmentPolicyVersionId = policyVersionId, + Status = QuestionPlacementStatus.Active, + CreatedBy = actorUserId, + UpdatedBy = actorUserId + }); + using (var scope = factory.CreateSystemScope("Set current immutable revision")) + { + var db = scope.ServiceProvider.GetRequiredService(); + await db.QuestionAssets.Where(item => item.Id == questionAssetId) + .ExecuteUpdateAsync(setters => setters.SetProperty(item => item.CurrentRevisionId, questionRevisionId)); + } + + return new CompilerFixture( + tenantId, + actorUserId, + curriculumVersionId, + questionAssetId, + questionRevisionId); + } + + private static QuestionRevisionDraft Draft(string content, int correctOptionIndex) => new( + "choice", + null, + 2, + content, + JsonSerializer.SerializeToElement(new[] { "2", "4" }), + correctOptionIndex, + JsonSerializer.SerializeToElement(Array.Empty()), + null, + "Explanation", + JsonSerializer.SerializeToElement(Array.Empty()), + null, + null); + + private static QuestionDeliveryFingerprintInput ToFingerprintInput(QuestionRevisionDraft draft) => new( + draft.QuestionType, + draft.Content, + draft.Options, + draft.CorrectOptionIndex, + draft.CorrectOptionIndices, + draft.AnswerText, + draft.SubQuestions, + draft.CodeLang, + draft.CodeTemplate); + + private sealed record CompilerFixture( + Guid TenantId, + Guid ActorUserId, + Guid CurriculumVersionId, + Guid QuestionAssetId, + Guid QuestionRevisionId); +} diff --git a/Tiku.IntegrationTests/ContentV2/ContentV2AuthoringTests.cs b/Tiku.IntegrationTests/ContentV2/ContentV2AuthoringTests.cs new file mode 100644 index 0000000..6700dd1 --- /dev/null +++ b/Tiku.IntegrationTests/ContentV2/ContentV2AuthoringTests.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Tiku.Application.Content; +using Tiku.Domain.Tenancy; +using Tiku.IntegrationTests.Api; + +namespace Tiku.IntegrationTests.ContentV2; + +public sealed class ContentV2AuthoringTests +{ + [Fact] + public async Task Exact_duplicates_are_reusable_but_fuzzy_matches_require_manual_review_and_stay_owner_scoped() + { + await using var factory = new ApiTestFactory(); + var platformOwnerId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var otherTenantId = Guid.NewGuid(); + await factory.SeedAsync( + Tenant(platformOwnerId, TenantMode.PlatformOwned), + Tenant(tenantId, TenantMode.Saas), + Tenant(otherTenantId, TenantMode.Saas)); + using var scope = factory.CreateSystemScope("Exercise V2 authoring deduplication"); + var service = scope.ServiceProvider.GetRequiredService(); + var actor = new ContentAuthorActor(tenantId, Guid.NewGuid()); + var otherActor = new ContentAuthorActor(otherTenantId, Guid.NewGuid()); + var original = Draft("What is 1 + 1?", "2", 0); + + var created = await service.ExecuteAsync(actor, + new ExecuteQuestionAuthoringCommand(QuestionImportDecision.CreateNew, original, PublishAsset: true)); + await service.ExecuteAsync(otherActor, + new ExecuteQuestionAuthoringCommand(QuestionImportDecision.CreateNew, original, PublishAsset: true)); + var exact = await service.SearchDuplicatesAsync(actor, original); + var changedAnswer = await service.SearchDuplicatesAsync(actor, Draft("What is 1 + 1?", "3", 1)); + var preview = await service.PreviewImportAsync(actor, + [ + ("exact", original), + ("fuzzy", Draft("What is one plus one?", "2", 0)), + ("new", Draft("Identify the author of the historical passage about maritime navigation.", "A", 1)) + ]); + + var exactCandidate = Assert.Single(exact.Candidates, item => item.ExactDeliveryMatch); + Assert.Equal(tenantId, exactCandidate.QuestionAssetOwnerTenantId); + Assert.DoesNotContain(exact.Candidates, item => item.QuestionAssetOwnerTenantId == otherTenantId); + Assert.Contains(changedAnswer.Candidates, item => + !item.ExactDeliveryMatch && item.QuestionAssetId == created.QuestionAssetId); + Assert.Equal(QuestionImportDecision.ReuseTenant, + preview.Rows.Single(item => item.RowKey == "exact").SuggestedDecision); + Assert.Equal(QuestionImportDecision.ManualReview, + preview.Rows.Single(item => item.RowKey == "fuzzy").SuggestedDecision); + Assert.Equal(QuestionImportDecision.CreateNew, + preview.Rows.Single(item => item.RowKey == "new").SuggestedDecision); + + var reused = await service.ExecuteAsync(actor, + new ExecuteQuestionAuthoringCommand( + QuestionImportDecision.ReuseTenant, + original, + created.QuestionAssetId)); + Assert.Equal(created.QuestionAssetId, reused.QuestionAssetId); + Assert.Equal(created.QuestionRevisionId, reused.QuestionRevisionId); + } + + private static Tenant Tenant(Guid id, TenantMode mode) => new() + { + Id = id, + Slug = $"tenant-{id:N}", + Name = mode.ToString(), + Mode = mode + }; + + private static QuestionRevisionDraft Draft( + string content, + string answer, + int correctOptionIndex) => new( + "choice", + null, + 2, + content, + JsonSerializer.SerializeToElement(new[] { "2", "3" }), + correctOptionIndex, + JsonSerializer.SerializeToElement(Array.Empty()), + answer, + "Explanation", + JsonSerializer.SerializeToElement(Array.Empty()), + null, + null); +} diff --git a/Tiku.IntegrationTests/ContentV2/PlatformContentPackageProjectionTests.cs b/Tiku.IntegrationTests/ContentV2/PlatformContentPackageProjectionTests.cs new file mode 100644 index 0000000..2d6677c --- /dev/null +++ b/Tiku.IntegrationTests/ContentV2/PlatformContentPackageProjectionTests.cs @@ -0,0 +1,180 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tiku.Application.Content; +using Tiku.Domain.Content; +using Tiku.Domain.Learning; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Persistence; +using Tiku.IntegrationTests.Api; + +namespace Tiku.IntegrationTests.ContentV2; + +public sealed class PlatformContentPackageProjectionTests +{ + [Fact] + public async Task Cell_projection_is_idempotent_and_ignores_out_of_order_packages() + { + await using var factory = new ApiTestFactory(); + var fixture = await SeedPackagesAsync(factory); + using var scope = factory.CreateSystemScope("Apply platform package events to a Cell"); + var service = scope.ServiceProvider.GetRequiredService(); + + var applied = await service.ApplyAsync(new ApplyPlatformContentPackageCommand( + fixture.OwnerTenantId, + fixture.NewPackageVersionId, + fixture.PackageCode, + "learning-cell-a", + 2, + fixture.NewHash)); + var duplicate = await service.ApplyAsync(new ApplyPlatformContentPackageCommand( + fixture.OwnerTenantId, + fixture.NewPackageVersionId, + fixture.PackageCode, + "learning-cell-a", + 2, + fixture.NewHash)); + var old = await service.ApplyAsync(new ApplyPlatformContentPackageCommand( + fixture.OwnerTenantId, + fixture.OldPackageVersionId, + fixture.PackageCode, + "learning-cell-a", + 1, + fixture.OldHash)); + + Assert.Equal(CellPackageApplyStatus.Applied, applied.Status); + Assert.Equal(CellPackageApplyStatus.Duplicate, duplicate.Status); + Assert.Equal(CellPackageApplyStatus.IgnoredOutOfOrder, old.Status); + + var conflict = await Assert.ThrowsAsync(() => service.ApplyAsync( + new ApplyPlatformContentPackageCommand( + fixture.OwnerTenantId, + fixture.OldPackageVersionId, + fixture.PackageCode, + "learning-cell-a", + 2, + fixture.OldHash))); + Assert.Equal("platform_package_sequence_conflict", conflict.Code); + + var db = scope.ServiceProvider.GetRequiredService(); + var projection = await db.PlatformContentPackageCellProjections.AsNoTracking().SingleAsync(); + Assert.Equal(fixture.NewPackageVersionId, projection.PlatformContentPackageVersionId); + Assert.Equal(2, projection.EventSequence); + Assert.Equal(fixture.NewHash, projection.ContentHash); + Assert.Equal(CellProjectionStatus.Ready, projection.Status); + } + + private static async Task SeedPackagesAsync(ApiTestFactory factory) + { + var ownerTenantId = Guid.NewGuid(); + var businessLineId = Guid.NewGuid(); + var curriculumId = Guid.NewGuid(); + var curriculumVersionId = Guid.NewGuid(); + var oldReleaseId = Guid.NewGuid(); + var newReleaseId = Guid.NewGuid(); + var oldPackageVersionId = Guid.NewGuid(); + var newPackageVersionId = Guid.NewGuid(); + var packageCode = "national-math"; + var oldHash = new string('1', 64); + var newHash = new string('2', 64); + var now = DateTimeOffset.UtcNow; + + await factory.SeedAsync( + new Tenant + { + Id = ownerTenantId, + Slug = $"platform-{ownerTenantId:N}", + Name = "Platform content owner", + Mode = TenantMode.PlatformOwned + }, + new BusinessLine + { + Id = businessLineId, + Code = $"business-{businessLineId:N}", + Name = "Generic exam business" + }, + new Curriculum + { + Id = curriculumId, + TenantId = ownerTenantId, + BusinessLineId = businessLineId, + Code = $"curriculum-{curriculumId:N}", + Name = "Platform curriculum" + }); + await factory.SeedAsync( + new CurriculumVersion + { + Id = curriculumVersionId, + TenantId = ownerTenantId, + CurriculumId = curriculumId, + Name = "Platform curriculum V1", + Status = ContentDefinitionStatus.Published, + PublishedAt = now + }); + await factory.SeedAsync( + new ContentRelease + { + Id = oldReleaseId, + TenantId = ownerTenantId, + BusinessLineId = businessLineId, + CurriculumVersionId = curriculumVersionId, + ReleaseNo = 1, + Name = "Old release", + SourceFingerprint = oldHash, + Status = ContentReleaseStatus.Published, + PublishedAt = now + }, + new ContentRelease + { + Id = newReleaseId, + TenantId = ownerTenantId, + BusinessLineId = businessLineId, + CurriculumVersionId = curriculumVersionId, + ReleaseNo = 2, + Name = "New release", + SourceFingerprint = newHash, + Status = ContentReleaseStatus.Published, + PublishedAt = now + }); + await factory.SeedAsync( + new PlatformContentPackageVersion + { + Id = oldPackageVersionId, + TenantId = ownerTenantId, + PackageCode = packageCode, + VersionNo = 1, + BusinessLineId = businessLineId, + ContentReleaseId = oldReleaseId, + ContentHash = oldHash, + Status = PlatformContentPackageStatus.Published, + PublishedAt = now + }, + new PlatformContentPackageVersion + { + Id = newPackageVersionId, + TenantId = ownerTenantId, + PackageCode = packageCode, + VersionNo = 2, + BusinessLineId = businessLineId, + ContentReleaseId = newReleaseId, + ContentHash = newHash, + Status = PlatformContentPackageStatus.Published, + PublishedAt = now + }); + + return new PackageFixture( + ownerTenantId, + oldPackageVersionId, + newPackageVersionId, + packageCode, + oldHash, + newHash); + } + + private sealed record PackageFixture( + Guid OwnerTenantId, + Guid OldPackageVersionId, + Guid NewPackageVersionId, + string PackageCode, + string OldHash, + string NewHash); +} diff --git a/Tiku.IntegrationTests/MigrationExecutionTests.cs b/Tiku.IntegrationTests/MigrationExecutionTests.cs index ee7122f..653c7b8 100644 --- a/Tiku.IntegrationTests/MigrationExecutionTests.cs +++ b/Tiku.IntegrationTests/MigrationExecutionTests.cs @@ -56,6 +56,9 @@ public sealed class MigrationExecutionTests Assert.Contains("CREATE INDEX IF NOT EXISTS ix_scoreline_records_field_values_jsonb_path", script); Assert.Contains("CREATE INDEX IF NOT EXISTS ix_scoreline_records_school_name_trgm", script); Assert.Contains("CREATE INDEX IF NOT EXISTS ix_background_jobs_pending_due", script); + Assert.Contains("CREATE OR REPLACE FUNCTION tiku_guard_question_revision_immutable()", script); + Assert.Contains("CREATE TRIGGER trg_product_manifest_resources_published_immutable", script); + Assert.Contains("CREATE TRIGGER trg_content_release_questions_release_immutable", script); } [Fact] diff --git a/Tiku.IntegrationTests/ModulePersistenceBoundaryTests.cs b/Tiku.IntegrationTests/ModulePersistenceBoundaryTests.cs index 6a8c7a5..9929688 100644 --- a/Tiku.IntegrationTests/ModulePersistenceBoundaryTests.cs +++ b/Tiku.IntegrationTests/ModulePersistenceBoundaryTests.cs @@ -34,7 +34,6 @@ public sealed class ModulePersistenceBoundaryTests typeof(IJobsOperationsPersistence), typeof(IPlatformControlPlanePersistence), typeof(IPlatformAdministrationPersistence), - typeof(IPlatformQuestionBankAdministrationPersistence), typeof(IPlatformTenantCapabilitiesPersistence), typeof(IPlatformBillingPersistence), typeof(IOwnerActivationPersistence), diff --git a/Tiku.IntegrationTests/PhaseThreeTenantIsolationTests.cs b/Tiku.IntegrationTests/PhaseThreeTenantIsolationTests.cs deleted file mode 100644 index 65c85cb..0000000 --- a/Tiku.IntegrationTests/PhaseThreeTenantIsolationTests.cs +++ /dev/null @@ -1,457 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Tiku.Application.Learning; -using Tiku.Application.QuestionBanks; -using Tiku.Application.Security; -using Tiku.Domain.Catalog; -using Tiku.Domain.Commerce; -using Tiku.Domain.Common; -using Tiku.Domain.Content; -using Tiku.Domain.Identity; -using Tiku.Domain.Learning; -using Tiku.Domain.Platform; -using Tiku.Domain.QuestionBanks; -using Tiku.Domain.Tenancy; -using Tiku.Infrastructure.Persistence; -using Tiku.IntegrationTests.Api; - -namespace Tiku.IntegrationTests; - -public sealed class PhaseThreeTenantIsolationTests -{ - [Fact] - public async Task Unresolved_context_reads_no_tenant_rows_and_rejects_writes() - { - await using var factory = new ApiTestFactory(); - var tenantId = Guid.NewGuid(); - await factory.SeedAsync(Tenant(tenantId, "tenant-a"), new TenantBranding - { - TenantId = tenantId, - BrandName = "Tenant A" - }); - - using var scope = factory.Services.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - Assert.Empty(await dbContext.TenantBrandings.ToArrayAsync()); - dbContext.TenantBrandings.Add(new TenantBranding { BrandName = "unresolved" }); - await Assert.ThrowsAsync(() => dbContext.SaveChangesAsync()); - } - - [Fact] - public async Task Tenant_a_cannot_read_or_attach_tenant_b_data() - { - await using var factory = new ApiTestFactory(); - var tenantA = Guid.NewGuid(); - var tenantB = Guid.NewGuid(); - await factory.SeedAsync( - Tenant(tenantA, "tenant-a"), - Tenant(tenantB, "tenant-b"), - new TenantBranding { TenantId = tenantA, BrandName = "A" }, - new TenantBranding { TenantId = tenantB, BrandName = "B" }); - - using var scope = factory.CreateTenantScope(tenantA, "tenant-a"); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var visible = await dbContext.TenantBrandings.AsNoTracking().ToArrayAsync(); - Assert.Equal(tenantA, Assert.Single(visible).TenantId); - - var forged = new TenantBranding { TenantId = tenantB, BrandName = "forged" }; - dbContext.Attach(forged); - forged.BrandName = "modified"; - dbContext.Entry(forged).Property(item => item.BrandName).IsModified = true; - await Assert.ThrowsAsync(() => dbContext.SaveChangesAsync()); - } - - [Fact] - public async Task PostgreSql_rejects_reference_to_another_tenant_private_question() - { - await using var factory = new ApiTestFactory(); - var tenantA = Guid.NewGuid(); - var tenantB = Guid.NewGuid(); - var questionId = Guid.NewGuid(); - await factory.SeedAsync( - Tenant(tenantA, "tenant-a"), - Tenant(tenantB, "tenant-b"), - new Question - { - Id = questionId, - TenantId = tenantB, - Type = "choice", - Status = QuestionStatus.Published - }); - - await Assert.ThrowsAsync(() => factory.SeedAsync(new TenantQuestionReference - { - TenantId = tenantA, - QuestionOwnerTenantId = tenantB, - QuestionId = questionId, - Source = QuestionSource.Tenant - })); - } - - [Fact] - public async Task Taxonomy_extension_accepts_platform_parent_and_rejects_other_tenant_parent() - { - await using var factory = new ApiTestFactory(); - var platformId = Guid.NewGuid(); - var tenantA = Guid.NewGuid(); - var tenantB = Guid.NewGuid(); - var platformNodeId = Guid.NewGuid(); - var tenantBNodeId = Guid.NewGuid(); - await factory.SeedAsync( - Tenant(platformId, "platform", TenantMode.PlatformOwned), - Tenant(tenantA, "tenant-a"), - Tenant(tenantB, "tenant-b"), - new TaxonomyNode - { - Id = platformNodeId, - TenantId = platformId, - Code = "platform-subject", - Name = "Platform Subject", - NodeType = TaxonomyNodeType.Subject - }, - new TaxonomyNode - { - Id = tenantBNodeId, - TenantId = tenantB, - Code = "tenant-b-subject", - Name = "Tenant B Subject", - NodeType = TaxonomyNodeType.Subject - }); - - await factory.SeedAsync(new TaxonomyNode - { - TenantId = tenantA, - ParentOwnerTenantId = platformId, - ParentId = platformNodeId, - Code = "tenant-a-platform-extension", - Name = "Valid Extension", - NodeType = TaxonomyNodeType.Chapter - }); - - await Assert.ThrowsAsync(() => factory.SeedAsync(new TaxonomyNode - { - TenantId = tenantA, - ParentOwnerTenantId = tenantB, - ParentId = tenantBNodeId, - Code = "tenant-a-forged-extension", - Name = "Invalid Extension", - NodeType = TaxonomyNodeType.Chapter - })); - } - - [Theory] - [InlineData(TenantSaasSubscriptionStatus.Trial, true)] - [InlineData(TenantSaasSubscriptionStatus.Active, true)] - [InlineData(TenantSaasSubscriptionStatus.PastDue, false)] - [InlineData(TenantSaasSubscriptionStatus.Cancelled, false)] - public async Task Public_question_reference_requires_current_subscription( - TenantSaasSubscriptionStatus subscriptionStatus, - bool allowed) - { - await using var factory = new ApiTestFactory(); - var platformId = Guid.NewGuid(); - var tenantId = Guid.NewGuid(); - var questionId = Guid.NewGuid(); - var offeringId = Guid.NewGuid(); - var versionId = Guid.NewGuid(); - var subscriptionId = Guid.NewGuid(); - var now = DateTimeOffset.UtcNow; - await factory.SeedAsync( - Tenant(platformId, "platform", TenantMode.PlatformOwned), - Tenant(tenantId, "tenant-a"), - new Question - { - Id = questionId, - TenantId = platformId, - Type = "choice", - Status = QuestionStatus.Published - }, - new SaasFeature - { - Code = SaasFeatureCatalog.Practice, - Name = "Practice", - Category = "learning", - Status = SaasFeatureStatus.Active - }, - new SaasOffering - { - Id = offeringId, - Code = "standard", - Name = "Standard", - Type = SaasOfferingType.BasePlan, - Status = SaasOfferingStatus.Active - }, - new SaasOfferingVersion - { - Id = versionId, - OfferingId = offeringId, - Status = SaasOfferingVersionStatus.Published, - OriginalAmountCents = 100, - AmountCents = 100, - PublishedAt = now.AddDays(-1) - }, - new SaasOfferingVersionFeature { OfferingVersionId = versionId, FeatureCode = SaasFeatureCatalog.Practice }, - new TenantSaasSubscription - { - Id = subscriptionId, - TenantId = tenantId, - BaseOfferingVersionId = versionId, - Status = subscriptionStatus, - StartsAt = now.AddDays(-1), - CurrentPeriodStart = now.AddDays(-1), - CurrentPeriodEnd = now.AddDays(1) - }, - new TenantSaasSubscriptionItem - { - TenantId = tenantId, - SubscriptionId = subscriptionId, - OfferingVersionId = versionId, - Status = TenantSaasSubscriptionItemStatus.Active, - StartsAt = now.AddDays(-1), - EndsAt = now.AddDays(1) - }); - - using var scope = factory.CreateTenantScope(tenantId, "tenant-a"); - var service = scope.ServiceProvider.GetRequiredService(); - var operation = async () => - { - var reference = await service.ResolveAsync( - tenantId, - null, - new QuestionLocator(QuestionSource.Platform, questionId)); - await scope.ServiceProvider.GetRequiredService().SaveChangesAsync(); - return reference; - }; - - if (allowed) - { - var reference = await operation(); - Assert.Equal(platformId, reference.QuestionOwnerTenantId); - } - else - { - await Assert.ThrowsAsync(operation); - } - } - - [Fact] - public async Task Mixed_collection_pins_versions_and_new_session_uses_new_platform_version() - { - await using var factory = new ApiTestFactory(); - var platformId = Guid.NewGuid(); - var tenantId = Guid.NewGuid(); - var userId = Guid.NewGuid(); - var platformQuestionId = Guid.NewGuid(); - var privateQuestionId = Guid.NewGuid(); - var platformV1 = Guid.NewGuid(); - var platformV2 = Guid.NewGuid(); - var privateV1 = Guid.NewGuid(); - var collectionId = Guid.NewGuid(); - var platformReferenceId = Guid.NewGuid(); - var privateReferenceId = Guid.NewGuid(); - var businessLineId = Guid.NewGuid(); - var licenseId = Guid.NewGuid(); - var productId = Guid.NewGuid(); - var contentSliceId = Guid.NewGuid(); - await factory.SeedAsync( - Tenant(platformId, "platform", TenantMode.PlatformOwned), - Tenant(tenantId, "tenant-a"), - new User { Id = userId, Phone = "13800009999" }); - await factory.SeedQuestionWithVersionAsync( - new Question - { - Id = platformQuestionId, - TenantId = platformId, - Type = "choice", - Status = QuestionStatus.Published - }, - new QuestionVersion - { - Id = platformV1, - TenantId = platformId, - QuestionId = platformQuestionId, - VersionNo = 1, - Content = "platform-v1", - CorrectOptionIndex = 0 - }); - await factory.SeedQuestionWithVersionAsync( - new Question - { - Id = privateQuestionId, - TenantId = tenantId, - Type = "choice", - Status = QuestionStatus.Published - }, - new QuestionVersion - { - Id = privateV1, - TenantId = tenantId, - QuestionId = privateQuestionId, - VersionNo = 1, - Content = "private-v1", - CorrectOptionIndex = 0 - }); - await factory.SeedAsync( - new TenantQuestionReference - { - Id = platformReferenceId, - TenantId = tenantId, - QuestionOwnerTenantId = platformId, - QuestionId = platformQuestionId, - Source = QuestionSource.Platform - }, - new TenantQuestionReference - { - Id = privateReferenceId, - TenantId = tenantId, - QuestionOwnerTenantId = tenantId, - QuestionId = privateQuestionId, - Source = QuestionSource.Tenant - }, - new QuestionCollection - { - Id = collectionId, - TenantId = tenantId, - Name = "mixed", - Status = ContentStatus.Active - }, - new QuestionCollectionItem - { - TenantId = tenantId, - CollectionId = collectionId, - QuestionReferenceId = platformReferenceId, - QuestionOwnerTenantId = platformId, - QuestionId = platformQuestionId, - SortOrder = 1 - }, - new QuestionCollectionItem - { - TenantId = tenantId, - CollectionId = collectionId, - QuestionReferenceId = privateReferenceId, - QuestionOwnerTenantId = tenantId, - QuestionId = privateQuestionId, - SortOrder = 2 - }, - new BusinessLine - { - Id = businessLineId, - Code = $"upgrade-{tenantId:N}", - Name = "专升本", - RegionAccessStrategy = LearningRegionAccessStrategy.NationalOnly - }, - new TenantLearningLicense - { - Id = licenseId, - TenantId = tenantId, - BusinessLineId = businessLineId, - IncludesNational = true, - StartsAt = DateTimeOffset.UtcNow.AddDays(-1) - }, - new ContentSlice - { - Id = contentSliceId, - TenantId = tenantId, - BusinessLineId = businessLineId, - RegionScope = LearningRegionScopeKind.National, - ResourceType = LearningContentResourceType.Collection, - ResourceId = collectionId, - Status = ContentSliceStatus.Active - }, - new LearningProduct - { - Id = productId, - TenantId = tenantId, - BusinessLineId = businessLineId, - Code = "mixed-collection", - Name = "混合题集" - }, - new LearningProductScope - { - TenantId = tenantId, - ProductId = productId, - ContentSliceOwnerTenantId = tenantId, - ContentSliceId = contentSliceId - }, - new Entitlement - { - TenantId = tenantId, - UserId = userId, - LearningProductId = productId, - StartsAt = DateTimeOffset.UtcNow.AddDays(-1), - ExpiresAt = DateTimeOffset.UtcNow.AddDays(30), - Status = EntitlementStatus.Active, - SourceType = "integration_test" - }); - - Guid firstSessionId; - using (var scope = factory.CreateTenantScope(tenantId, "tenant-a")) - { - var created = await scope.ServiceProvider.GetRequiredService() - .CreatePracticeSessionAsync( - new LearningActor(tenantId, userId), - new PracticeSessionCommand( - "collection", null, null, null, collectionId, null, null, 10, null, null, - JsonDefaults.Object())); - firstSessionId = created.Id; - } - - await factory.SeedAsync(new QuestionVersion - { - Id = platformV2, - TenantId = platformId, - QuestionId = platformQuestionId, - VersionNo = 2, - Content = "platform-v2", - CorrectOptionIndex = 0 - }); - using (var scope = factory.CreateSystemScope("Publish platform question V2")) - { - var dbContext = scope.ServiceProvider.GetRequiredService(); - var question = await dbContext.Questions.SingleAsync(item => - item.TenantId == platformId && item.Id == platformQuestionId); - question.CurrentVersionId = platformV2; - await dbContext.SaveChangesAsync(); - } - - Guid secondSessionId; - using (var scope = factory.CreateTenantScope(tenantId, "tenant-a")) - { - var created = await scope.ServiceProvider.GetRequiredService() - .CreatePracticeSessionAsync( - new LearningActor(tenantId, userId), - new PracticeSessionCommand( - "collection", null, null, null, collectionId, null, null, 10, null, null, - JsonDefaults.Object())); - secondSessionId = created.Id; - } - - using var verificationScope = factory.CreateSystemScope(); - var verificationDb = verificationScope.ServiceProvider.GetRequiredService(); - var firstQuestions = await verificationDb.PracticeSessionQuestions - .Where(item => item.PracticeSessionId == firstSessionId) - .ToArrayAsync(); - var secondQuestions = await verificationDb.PracticeSessionQuestions - .Where(item => item.PracticeSessionId == secondSessionId) - .ToArrayAsync(); - Assert.Equal(2, firstQuestions.Length); - Assert.Equal(2, secondQuestions.Length); - Assert.Equal(platformV1, - firstQuestions.Single(item => item.QuestionId == platformQuestionId).QuestionVersionId); - Assert.Equal(platformV2, - secondQuestions.Single(item => item.QuestionId == platformQuestionId).QuestionVersionId); - Assert.Equal(privateV1, firstQuestions.Single(item => item.QuestionId == privateQuestionId).QuestionVersionId); - } - - private static Tenant Tenant(Guid id, string slug, TenantMode mode = TenantMode.Saas) - { - return new Tenant - { - Id = id, - Slug = slug, - Name = slug, - Status = TenantStatus.Active, - Mode = mode - }; - } -} diff --git a/Tiku.UnitTests/ContentV2RulesTests.cs b/Tiku.UnitTests/ContentV2RulesTests.cs new file mode 100644 index 0000000..22fc625 --- /dev/null +++ b/Tiku.UnitTests/ContentV2RulesTests.cs @@ -0,0 +1,92 @@ +using System.Text.Json; +using Tiku.Application.Content; +using Tiku.Domain.Content; + +namespace Tiku.UnitTests; + +public sealed class ContentV2RulesTests +{ + [Fact] + public void Applicability_uses_or_between_groups_and_and_within_group() + { + var region = Guid.NewGuid(); + var institution = Guid.NewGuid(); + var province = Guid.NewGuid(); + var city = Guid.NewGuid(); + var university = Guid.NewGuid(); + var profile = new[] + { + new TargetProfileFact(region, city, new HashSet { province }), + new TargetProfileFact(institution, university, new HashSet()) + }; + var groups = new[] + { + new PlacementRuleGroupFact(0, + [ + new PlacementRuleConditionFact(region, province, TargetRuleOperator.DescendantOf), + new PlacementRuleConditionFact(institution, university, TargetRuleOperator.Exact) + ]), + new PlacementRuleGroupFact(1, + [ + new PlacementRuleConditionFact(region, Guid.NewGuid(), TargetRuleOperator.Exact) + ]) + }; + + Assert.True(QuestionApplicabilityEvaluator.Matches(profile, groups)); + } + + [Fact] + public void Applicability_supports_explicit_exclusion() + { + var region = Guid.NewGuid(); + var province = Guid.NewGuid(); + var excludedCity = Guid.NewGuid(); + var profile = new[] + { + new TargetProfileFact(region, excludedCity, new HashSet { province }) + }; + var groups = new[] + { + new PlacementRuleGroupFact(0, + [ + new PlacementRuleConditionFact(region, province, TargetRuleOperator.DescendantOf), + new PlacementRuleConditionFact(region, excludedCity, TargetRuleOperator.NotExact) + ]) + }; + + Assert.False(QuestionApplicabilityEvaluator.Matches(profile, groups)); + } + + [Fact] + public void Delivery_fingerprint_is_canonical_and_includes_the_answer() + { + var first = Input(" Two plus two? ", "4", "[{\"text\":\"4\",\"key\":\"A\"}]"); + var equivalent = Input("Two plus two?", "4", "[{\"key\":\"A\",\"text\":\"4\"}]"); + var differentAnswer = Input("Two plus two?", "5", "[{\"key\":\"A\",\"text\":\"4\"}]"); + + Assert.Equal(QuestionDeliveryFingerprint.Compute(first), QuestionDeliveryFingerprint.Compute(equivalent)); + Assert.NotEqual(QuestionDeliveryFingerprint.Compute(first), QuestionDeliveryFingerprint.Compute(differentAnswer)); + } + + [Fact] + public void Delivery_fingerprint_preserves_visible_letter_case() + { + Assert.NotEqual( + QuestionDeliveryFingerprint.Compute(Input("US policy", "A", "[]")), + QuestionDeliveryFingerprint.Compute(Input("us policy", "A", "[]"))); + } + + private static QuestionDeliveryFingerprintInput Input(string content, string answer, string options) + { + return new QuestionDeliveryFingerprintInput( + "choice", + content, + JsonDocument.Parse(options).RootElement.Clone(), + 0, + JsonDocument.Parse("[]").RootElement.Clone(), + answer, + JsonDocument.Parse("[]").RootElement.Clone(), + null, + null); + } +} diff --git a/docs/architecture/content-domain-v2.md b/docs/architecture/content-domain-v2.md new file mode 100644 index 0000000..990e624 --- /dev/null +++ b/docs/architecture/content-domain-v2.md @@ -0,0 +1,113 @@ +# 内容领域 V2 与学习访问投影 + +状态:Accepted(破坏式重做,开发期) +日期:2026-08-05 + +## 决策 + +题目是可复用资产,不再通过题库、地区、章节或知识点字段确定唯一归属。题库仅是编辑文件夹;题集是策划集合。题目通过 `QuestionPlacement` 放入一个大纲节点,并用受限 DNF 规则声明适用目标;发布器把事实编译为不可变 Release、Audience Segment 和候选行。学生请求只读取编译访问快照与候选投影,不在线遍历租户、套餐、地区、班级、老师和作业关系链。 + +所有业务使用同一套可配置目标维度。新增考试业务时创建维度、目标 Profile、业务策略、大纲和产品 Manifest,不增加业务枚举或在刷题代码中加入分支。 + +## 所有权和版本边界 + +- `QuestionAsset.TenantId` 是资产所有者。平台公共内容使用平台内容所有者;租户私题只能由相同租户搜索、放置和发布。 +- `QuestionRevision` 创建后不可修改。完整交付指纹包含题型、题干、选项、答案、子题和代码内容;Unicode 和空白规范化不会忽略可见字符大小写。 +- 模糊相似仅由 `pg_trgm` 产生人工复核候选,不自动合并。 +- Profile、业务目标策略、大纲、试卷规范、评分政策和 Product Manifest 可编辑 Draft;发布后不可修改。 +- Release 内容行不可修改。撤回只改变 Release 生命周期;升级必须创建新 Release。 +- 已开始会话锁定 Revision、Placement、Assessment Policy Version 和分值证据。 + +## 关系图 + +```mermaid +erDiagram + QUESTION_ASSET ||--o{ QUESTION_REVISION : versions + QUESTION_ASSET ||--o{ QUESTION_PLACEMENT : placed_as + CURRICULUM ||--o{ CURRICULUM_VERSION : versions + CURRICULUM_VERSION ||--o{ CURRICULUM_NODE : contains + CURRICULUM_NODE ||--o{ QUESTION_PLACEMENT : receives + QUESTION_PLACEMENT ||--o{ PLACEMENT_RULE_GROUP : OR + PLACEMENT_RULE_GROUP ||--o{ PLACEMENT_RULE_CONDITION : AND + EXAM_TARGET_PROFILE ||--o{ EXAM_TARGET_PROFILE_VERSION : versions + EXAM_TARGET_PROFILE_VERSION ||--o{ EXAM_TARGET_PROFILE_VALUE : dimensions + CONTENT_RELEASE ||--o{ AUDIENCE_SEGMENT : compiles + AUDIENCE_SEGMENT ||--o{ AUDIENCE_SEGMENT_MEMBER : profiles + CONTENT_RELEASE ||--o{ CONTENT_RELEASE_QUESTION : candidates + PRODUCT_ACCESS_MANIFEST_VERSION ||--o{ PRODUCT_MANIFEST_RELEASE : allows + PRODUCT_ACCESS_MANIFEST_VERSION ||--o{ PRODUCT_MANIFEST_TARGET : allows + STUDENT_ENTITLEMENT }o--|| PRODUCT_ACCESS_MANIFEST_VERSION : grants + TENANT_BUSINESS_LICENSE ||--o{ TENANT_LICENSED_TARGET : bounds + EFFECTIVE_ACCESS_PROJECTION }o--o{ CONTENT_RELEASE : ids + EFFECTIVE_ACCESS_PROJECTION }o--o{ AUDIENCE_SEGMENT : ids +``` + +## 目标配置样例 + +以下是种子/管理接口应表达的配置意图,不是在线授权脚本。 + +```json +[ + { + "business": "upgrade_education", + "selectionMode": "single_from_tenant_license", + "maxActiveTargets": 1, + "maxPrimaryTargets": 1, + "maxAlternateTargets": 0, + "cooldownDays": 30, + "requiredDimensions": ["exam_year", "exam_system", "administrative_area", "subject_track"] + }, + { + "business": "civil_service", + "selectionMode": "base_plus_entitled_targets", + "includesBaseContent": true, + "maxActiveTargets": 16, + "maxPrimaryTargets": 1, + "requiredDimensions": ["exam_year", "exam_system", "paper_category", "administrative_area"] + }, + { + "business": "postgraduate", + "selectionMode": "base_plus_primary_and_entitled_alternates", + "includesBaseContent": true, + "maxActiveTargets": 16, + "maxPrimaryTargets": 1, + "requiredDimensions": ["exam_year", "exam_system", "institution", "school", "major", "subject_code"] + } +] +``` + +示例 Profile: + +- `2027 河南统招专升本 / 理科 / 高等数学` +- `2027 国考 / 行政执法类` +- `2027 江苏省考 / A 类` +- `2027 四川大学 / 计算机学院 / 软件工程 / 自主命题 874` + +## 发布和在线不变量 + +发布在 Repeatable Read 事务内执行,并按“所有者 + 大纲版本”取得 PostgreSQL 事务级 advisory lock。发布器验证 Revision、评分政策和 Profile,使用慢速 DNF 判定器生成 Segment,在同一事务写 Release、候选行和 Outbox。相同 Profile 集合共享 Segment;候选 ordinal 在发布时稳定生成。 + +访问编译器只接受当前租户和登录用户,从有效租户业务许可证、许可 Profile、V2 学生权益、Product Manifest、当前主/备目标和班级 Grant 生成 `EffectiveAccessProjection`。Redis/FusionCache 缓存 30 秒且关闭 fail-safe;缓存未命中回源 PostgreSQL,事实库失败时不返回过期授权。 + +创建会话最终只能执行: + +1. 获取一个 `EffectiveAccessProjection`。 +2. 验证请求资源被其中的 Manifest 允许。 +3. 按 Release、Segment、课程节点和稳定随机种子读取候选。 +4. 批量锁定交付版本和评分证据、原子扣减额度、创建会话。 + +答题期间不再查询套餐、班级、老师、目标层级或内容规则。强撤权版本仍在答题和交卷入口检查。 + +## PostgreSQL 选择 + +- `ltree`:行政区、院校等单父层级目标节点及祖先/后代匹配。 +- 邻接表 + 闭包表:大纲节点;支持同一知识概念出现在不同大纲。 +- `pg_trgm`:所有者范围内的疑似重复题检索。 +- 原生月分区:答案尝试、访问事件和 Outbox。 +- 不使用 `ORDER BY random()`;按稳定 ordinal、种子起点和区间回绕采样。 + +首版不引入 Citus、图数据库、Roaring Bitmap 或 pgvector。它们不会参与权限判定或在线刷题。 + +## 基线重建要求 + +删除 V1 内容/地区授权模型和旧迁移后,新的单一 `InitialSchema` 必须重新包含租户隔离函数与触发器、SaaS 防护对象、特殊 GIN/GiST/部分索引和月分区对象。空 PostgreSQL 执行 DbMigrator 后必须无待生成迁移。开发数据不迁移,种子按上述配置重新生成。