diff --git a/Tiku.Api/Contracts/CommissionDtos.cs b/Tiku.Api/Contracts/CommissionDtos.cs new file mode 100644 index 0000000..bd3e967 --- /dev/null +++ b/Tiku.Api/Contracts/CommissionDtos.cs @@ -0,0 +1,136 @@ +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Text.Json; +using Tiku.Application.Growth; + +namespace Tiku.Api.Contracts; + +public sealed class UpdateCommissionSettingsDto +{ + [Range(0, 1)] + public decimal? DefaultRate { get; set; } + [Range(0, int.MaxValue)] + public int? MinSettlementCents { get; set; } + [StringLength(50)] + public string? SettlementCycle { get; set; } + public JsonElement? Config { get; set; } + public UpdateCommissionSettingsCommand ToCommand() => new(DefaultRate, MinSettlementCents, SettlementCycle, Config); +} + +public sealed class UpdateMemberCommissionRateDto +{ + [Required] + public Guid UserId { get; set; } + [Range(0, 1)] + public decimal? CommissionRate { get; set; } + public JsonElement? CommissionConfig { get; set; } + public UpdateMemberCommissionRateCommand ToCommand() => new(UserId, CommissionRate, CommissionConfig); +} + +public class CommissionPeriodQueryDto +{ + [RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")] + public string? StartDate { get; set; } + [RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")] + public string? EndDate { get; set; } + public Guid? ReferrerUserId { get; set; } + [Range(1, 500)] + public int? Limit { get; set; } + public CommissionPeriodQuery ToQuery() => new(ParseDate(StartDate), ParseDate(EndDate), ReferrerUserId, Limit); + protected static DateOnly? ParseDate(string? value) => string.IsNullOrWhiteSpace(value) ? null : DateOnly.ParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture); +} + +public sealed class CommissionSettlementsQueryDto +{ + [StringLength(50)] + public string? Status { get; set; } + public Guid? ReferrerUserId { get; set; } + [Range(1, 500)] + public int? Limit { get; set; } + public CommissionSettlementQuery ToQuery() => new(Status, ReferrerUserId, Limit); +} + +public sealed class GenerateCommissionSettlementDto +{ + [Required] + [RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")] + public string StartDate { get; set; } = string.Empty; + [Required] + [RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")] + public string EndDate { get; set; } = string.Empty; + [Required] + public Guid ReferrerUserId { get; set; } + [StringLength(50)] + public string? Status { get; set; } + [StringLength(1000)] + public string? Remark { get; set; } + public JsonElement? Metadata { get; set; } + public GenerateCommissionSettlementCommand ToCommand() => new(Parse(StartDate), Parse(EndDate), ReferrerUserId, Status, Remark, Metadata); + private static DateOnly Parse(string value) => DateOnly.ParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture); +} + +public sealed class UpdateCommissionSettlementStatusDto +{ + [Required] + public Guid SettlementId { get; set; } + [StringLength(50)] + public string? Status { get; set; } + [StringLength(1000)] + public string? ReviewNote { get; set; } + [StringLength(100)] + public string? PaymentMethod { get; set; } + [StringLength(200)] + public string? PaymentAccount { get; set; } + public JsonElement? Metadata { get; set; } + public UpdateCommissionSettlementStatusCommand ToCommand() => new(SettlementId, Status, ReviewNote, PaymentMethod, PaymentAccount, Metadata); +} + +public sealed class CommissionSettlementExportQueryDto +{ + [Required] + public Guid SettlementId { get; set; } + [StringLength(10)] + public string? Format { get; set; } +} + +public sealed class CommissionSettlementProofQueryDto +{ + [Required] + public Guid SettlementId { get; set; } +} + +public sealed class CreateCommissionProofDto +{ + [Required] + public Guid SettlementId { get; set; } + [StringLength(50)] + public string? ProofType { get; set; } + [StringLength(200)] + public string? Title { get; set; } + [StringLength(2000)] + public string? Description { get; set; } + public Guid? AssetId { get; set; } + [StringLength(2048)] + public string? ExternalUrl { get; set; } + [Range(0, int.MaxValue)] + public int? AmountCents { get; set; } + [StringLength(100)] + public string? PaymentMethod { get; set; } + [StringLength(200)] + public string? PaymentAccount { get; set; } + public DateTimeOffset? PaidAt { get; set; } + public JsonElement? Metadata { get; set; } + public CreateCommissionProofCommand ToCommand() => new(SettlementId, ProofType, Title, Description, AssetId, ExternalUrl, AmountCents, PaymentMethod, PaymentAccount, PaidAt, Metadata); +} + +public sealed class UpdateCommissionProofStatusDto +{ + [Required] + public Guid ProofId { get; set; } + [StringLength(50)] + public string? Status { get; set; } + [StringLength(1000)] + public string? ReviewNote { get; set; } + public JsonElement? Metadata { get; set; } + public UpdateCommissionProofStatusCommand ToCommand() => new(ProofId, Status, ReviewNote, Metadata); +} diff --git a/Tiku.Api/Controllers/CommissionController.cs b/Tiku.Api/Controllers/CommissionController.cs new file mode 100644 index 0000000..e3020ba --- /dev/null +++ b/Tiku.Api/Controllers/CommissionController.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tiku.Api.Contracts; +using Tiku.Application.Growth; +using Tiku.Application.Security; + +namespace Tiku.Api.Controllers; + +[ApiController] +[Authorize(Policy = TikuPolicies.TenantAdmin)] +[Produces("application/json")] +[Route("api/commission")] +public sealed class CommissionController( + ICommissionService commissionService, + ICurrentUser currentUser, + ICurrentTenant currentTenant) : ControllerBase +{ + [HttpGet("settings")] + public async Task> Settings(CancellationToken cancellationToken) => + Ok(await commissionService.GetSettingsAsync(ResolveActor(), cancellationToken)); + + [HttpPut("settings")] + public async Task> UpdateSettings(UpdateCommissionSettingsDto request, CancellationToken cancellationToken) => + Ok(await commissionService.UpdateSettingsAsync(ResolveActor(), request.ToCommand(), cancellationToken)); + + [HttpPut("member-rate")] + public async Task> MemberRate(UpdateMemberCommissionRateDto request, CancellationToken cancellationToken) => + Ok(await commissionService.UpdateMemberRateAsync(ResolveActor(), request.ToCommand(), cancellationToken)); + + [HttpGet("summary")] + public async Task> Summary([FromQuery] CommissionPeriodQueryDto query, CancellationToken cancellationToken) => + Ok(await commissionService.GetSummaryAsync(ResolveActor(), query.ToQuery(), cancellationToken)); + + [HttpGet("orders")] + public async Task>> Orders([FromQuery] CommissionPeriodQueryDto query, CancellationToken cancellationToken) => + Ok(await commissionService.GetOrdersAsync(ResolveActor(), query.ToQuery(), cancellationToken)); + + [HttpGet("settlements")] + public async Task>> Settlements([FromQuery] CommissionSettlementsQueryDto query, CancellationToken cancellationToken) => + Ok(await commissionService.GetSettlementsAsync(ResolveActor(), query.ToQuery(), cancellationToken)); + + [HttpGet("settlements/export")] + public async Task> Export([FromQuery] CommissionSettlementExportQueryDto query, CancellationToken cancellationToken) => + Ok(await commissionService.ExportSettlementAsync(ResolveActor(), query.SettlementId, query.Format, cancellationToken)); + + [HttpPost("settlements/generate")] + public async Task> Generate(GenerateCommissionSettlementDto request, CancellationToken cancellationToken) => + Ok(await commissionService.GenerateSettlementAsync(ResolveActor(), request.ToCommand(), cancellationToken)); + + [HttpPost("settlements/status")] + public async Task> UpdateStatus(UpdateCommissionSettlementStatusDto request, CancellationToken cancellationToken) => + Ok(await commissionService.UpdateSettlementStatusAsync(ResolveActor(), request.ToCommand(), cancellationToken)); + + [HttpGet("settlements/proofs")] + public async Task>> Proofs([FromQuery] CommissionSettlementProofQueryDto query, CancellationToken cancellationToken) => + Ok(await commissionService.GetProofsAsync(ResolveActor(), query.SettlementId, cancellationToken)); + + [HttpPost("settlements/proofs")] + public async Task> CreateProof(CreateCommissionProofDto request, CancellationToken cancellationToken) => + Ok(await commissionService.CreateProofAsync(ResolveActor(), request.ToCommand(), cancellationToken)); + + [HttpPost("settlements/proofs/status")] + public async Task> UpdateProofStatus(UpdateCommissionProofStatusDto request, CancellationToken cancellationToken) => + Ok(await commissionService.UpdateProofStatusAsync(ResolveActor(), request.ToCommand(), cancellationToken)); + + private CommissionAdminActor ResolveActor() + { + if (currentTenant.TenantId is null || currentUser.UserId is null) + { + throw new CommissionException("Commission admin actor was not resolved.", "commission_access_denied"); + } + + return new CommissionAdminActor(currentTenant.TenantId.Value, currentUser.UserId.Value); + } +} diff --git a/Tiku.Api/Middleware/ExceptionHandlingMiddleware.cs b/Tiku.Api/Middleware/ExceptionHandlingMiddleware.cs index 0833161..3efe1ac 100644 --- a/Tiku.Api/Middleware/ExceptionHandlingMiddleware.cs +++ b/Tiku.Api/Middleware/ExceptionHandlingMiddleware.cs @@ -215,6 +215,16 @@ public sealed class ExceptionHandlingMiddleware( return; } + if (exception is CommissionException commissionException) + { + await WriteProblemAsync( + context, + commissionException.Message, + CommissionStatusCode(commissionException.Code), + commissionException.Code); + return; + } + if (exception is PaymentProviderException paymentProviderException) { await WriteProblemAsync( @@ -410,4 +420,17 @@ public sealed class ExceptionHandlingMiddleware( _ => StatusCodes.Status400BadRequest }; } + + private static int CommissionStatusCode(string code) + { + return code switch + { + "commission_access_denied" => StatusCodes.Status403Forbidden, + "commission_member_not_found" or "commission_settlement_not_found" or "commission_proof_not_found" => + StatusCodes.Status404NotFound, + "commission_no_unsettled_sources" or "commission_below_minimum" or "commission_status_invalid" or + "commission_proof_closed" => StatusCodes.Status409Conflict, + _ => StatusCodes.Status400BadRequest + }; + } } diff --git a/Tiku.Application/Growth/CommissionModels.cs b/Tiku.Application/Growth/CommissionModels.cs new file mode 100644 index 0000000..cd14db4 --- /dev/null +++ b/Tiku.Application/Growth/CommissionModels.cs @@ -0,0 +1,90 @@ +using System.Text.Json; + +namespace Tiku.Application.Growth; + +public sealed record CommissionAdminActor(Guid TenantId, Guid UserId); + +public sealed record UpdateCommissionSettingsCommand(decimal? DefaultRate, int? MinSettlementCents, string? SettlementCycle, JsonElement? Config); + +public sealed record UpdateMemberCommissionRateCommand(Guid UserId, decimal? CommissionRate, JsonElement? CommissionConfig); + +public sealed record CommissionPeriodQuery(DateOnly? StartDate = null, DateOnly? EndDate = null, Guid? ReferrerUserId = null, int? Limit = null); + +public sealed record CommissionSettlementQuery(string? Status = null, Guid? ReferrerUserId = null, int? Limit = null); + +public sealed record GenerateCommissionSettlementCommand(DateOnly StartDate, DateOnly EndDate, Guid ReferrerUserId, string? Status, string? Remark, JsonElement? Metadata); + +public sealed record UpdateCommissionSettlementStatusCommand(Guid SettlementId, string? Status, string? ReviewNote, string? PaymentMethod, string? PaymentAccount, JsonElement? Metadata); + +public sealed record CreateCommissionProofCommand(Guid SettlementId, string? ProofType, string? Title, string? Description, Guid? AssetId, string? ExternalUrl, int? AmountCents, string? PaymentMethod, string? PaymentAccount, DateTimeOffset? PaidAt, JsonElement? Metadata); + +public sealed record UpdateCommissionProofStatusCommand(Guid ProofId, string? Status, string? ReviewNote, JsonElement? Metadata); + +public sealed record CommissionSettingsItem(decimal DefaultRate, int MinSettlementCents, string SettlementCycle, JsonElement Config); + +public sealed record CommissionSourceItem( + string SourceType, + Guid SourceId, + string? SourceNo, + Guid ReferrerUserId, + Guid? StudentUserId, + int GrossAmountCents, + decimal CommissionRate, + int CommissionAmountCents, + string RateSource, + Guid? SettlementId); + +public sealed record CommissionSummaryItem(int SourceCount, int PaidUserCount, int GrossAmountCents, int CommissionAmountCents); + +public sealed record CommissionSettlementItemDto( + Guid Id, + string SettlementNo, + Guid ReferrerUserId, + string Status, + DateOnly PeriodStart, + DateOnly PeriodEnd, + int SourceCount, + int PaidUserCount, + int GrossAmountCents, + int CommissionAmountCents, + DateTimeOffset? ReviewedAt, + DateTimeOffset? PaidAt); + +public sealed record CommissionProofItem( + Guid Id, + Guid SettlementId, + string ProofType, + string Status, + string? Title, + Guid? AssetId, + string? ExternalUrl, + int? AmountCents, + string? PaymentMethod, + string? PaymentAccount, + DateTimeOffset? PaidAt, + DateTimeOffset? ReviewedAt); + +public sealed record CommissionExportItem(Guid SettlementId, string Filename, string Format, string MimeType, int RowCount, string ContentBase64, string Sha256, int SizeBytes); + +public sealed record CommissionList(IReadOnlyCollection Items); + +public interface ICommissionService +{ + Task GetSettingsAsync(CommissionAdminActor actor, CancellationToken cancellationToken = default); + Task UpdateSettingsAsync(CommissionAdminActor actor, UpdateCommissionSettingsCommand command, CancellationToken cancellationToken = default); + Task UpdateMemberRateAsync(CommissionAdminActor actor, UpdateMemberCommissionRateCommand command, CancellationToken cancellationToken = default); + Task GetSummaryAsync(CommissionAdminActor actor, CommissionPeriodQuery query, CancellationToken cancellationToken = default); + Task> GetOrdersAsync(CommissionAdminActor actor, CommissionPeriodQuery query, CancellationToken cancellationToken = default); + Task> GetSettlementsAsync(CommissionAdminActor actor, CommissionSettlementQuery query, CancellationToken cancellationToken = default); + Task ExportSettlementAsync(CommissionAdminActor actor, Guid settlementId, string? format, CancellationToken cancellationToken = default); + Task GenerateSettlementAsync(CommissionAdminActor actor, GenerateCommissionSettlementCommand command, CancellationToken cancellationToken = default); + Task UpdateSettlementStatusAsync(CommissionAdminActor actor, UpdateCommissionSettlementStatusCommand command, CancellationToken cancellationToken = default); + Task> GetProofsAsync(CommissionAdminActor actor, Guid settlementId, CancellationToken cancellationToken = default); + Task CreateProofAsync(CommissionAdminActor actor, CreateCommissionProofCommand command, CancellationToken cancellationToken = default); + Task UpdateProofStatusAsync(CommissionAdminActor actor, UpdateCommissionProofStatusCommand command, CancellationToken cancellationToken = default); +} + +public sealed class CommissionException(string message, string code) : Exception(message) +{ + public string Code { get; } = code; +} diff --git a/Tiku.Domain/Growth/GrowthEntities.cs b/Tiku.Domain/Growth/GrowthEntities.cs index b268e66..4c54282 100644 --- a/Tiku.Domain/Growth/GrowthEntities.cs +++ b/Tiku.Domain/Growth/GrowthEntities.cs @@ -168,6 +168,39 @@ public sealed class CommissionSettlementItem : AuditableTenantEntity public JsonElement Metadata { get; set; } = JsonDefaults.Object(); } +public sealed class CommissionSettlementProof : AuditableTenantEntity +{ + public Guid SettlementId { get; set; } + public Guid? AssetId { get; set; } + public Guid? SubmittedBy { get; set; } + public Guid? ReviewedBy { get; set; } + public CommissionProofType ProofType { get; set; } = CommissionProofType.Payment; + public CommissionProofStatus Status { get; set; } = CommissionProofStatus.Submitted; + public string? Title { get; set; } + public string? Description { get; set; } + public string? ExternalUrl { get; set; } + public int? AmountCents { get; set; } + public string? PaymentMethod { get; set; } + public string? PaymentAccount { get; set; } + public DateTimeOffset? PaidAt { get; set; } + public DateTimeOffset? ReviewedAt { get; set; } + public string? ReviewNote { get; set; } + public JsonElement Metadata { get; set; } = JsonDefaults.Object(); +} + +public sealed class CommissionSettlementExportEvent : Entity +{ + public Guid TenantId { get; set; } + public Guid SettlementId { get; set; } + public Guid? ExportedBy { get; set; } + public string ExportFormat { get; set; } = "csv"; + public string Filename { get; set; } = string.Empty; + public int RowCount { get; set; } + public string? ContentSha256 { get; set; } + public JsonElement Metadata { get; set; } = JsonDefaults.Object(); + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; +} + public enum ReferralCodeStatus { Active, Disabled } public enum ReferralLeadBindType { FirstTouch, Manual, Imported } public enum ReferralLeadStatus { Protected, Invalid, Released } @@ -180,3 +213,5 @@ public enum CommissionSettlementCycle { Manual, Weekly, Monthly } public enum CommissionSettlementStatus { Draft, PendingReview, Approved, Paid, Rejected, Cancelled } public enum CommissionSourceType { Order, ActivationCode } public enum CommissionRateSource { Batch, Member, Default } +public enum CommissionProofType { Payment, Invoice, Receipt, Adjustment, Other } +public enum CommissionProofStatus { Submitted, Approved, Rejected, Voided } diff --git a/Tiku.Infrastructure/DependencyInjection.cs b/Tiku.Infrastructure/DependencyInjection.cs index 9b93770..d612da8 100644 --- a/Tiku.Infrastructure/DependencyInjection.cs +++ b/Tiku.Infrastructure/DependencyInjection.cs @@ -72,6 +72,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Tiku.Infrastructure/Growth/CommissionService.cs b/Tiku.Infrastructure/Growth/CommissionService.cs new file mode 100644 index 0000000..1e001c2 --- /dev/null +++ b/Tiku.Infrastructure/Growth/CommissionService.cs @@ -0,0 +1,348 @@ +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Tiku.Application.Growth; +using Tiku.Domain.Commerce; +using Tiku.Domain.Common; +using Tiku.Domain.Growth; +using Tiku.Domain.Operations; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure.Growth; + +public sealed class CommissionService(TikuDbContext dbContext) : ICommissionService +{ + public async Task GetSettingsAsync(CommissionAdminActor actor, CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + return ToSettingsItem(await GetSettingsCoreAsync(actor.TenantId, cancellationToken)); + } + + public async Task UpdateSettingsAsync(CommissionAdminActor actor, UpdateCommissionSettingsCommand command, CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var settings = await GetSettingsCoreAsync(actor.TenantId, cancellationToken); + settings.DefaultRate = command.DefaultRate ?? settings.DefaultRate; + settings.MinSettlementCents = command.MinSettlementCents ?? settings.MinSettlementCents; + settings.SettlementCycle = ParseEnum(command.SettlementCycle, settings.SettlementCycle, "invalid_commission_cycle"); + settings.Config = command.Config ?? settings.Config; + settings.UpdatedBy = actor.UserId; + await dbContext.SaveChangesAsync(cancellationToken); + return ToSettingsItem(settings); + } + + public async Task UpdateMemberRateAsync(CommissionAdminActor actor, UpdateMemberCommissionRateCommand command, CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var member = await dbContext.TenantMemberships + .FirstOrDefaultAsync(item => item.TenantId == actor.TenantId && item.UserId == command.UserId, cancellationToken) + ?? throw new CommissionException("Commission member was not found.", "commission_member_not_found"); + member.Permissions = JsonSerializer.SerializeToElement(new + { + commissionRate = command.CommissionRate, + commissionConfig = command.CommissionConfig + }); + await dbContext.SaveChangesAsync(cancellationToken); + return new { member.UserId, commissionRate = command.CommissionRate, commissionConfig = command.CommissionConfig }; + } + + public async Task GetSummaryAsync(CommissionAdminActor actor, CommissionPeriodQuery query, CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var rows = await BuildSourcesAsync(actor.TenantId, query, cancellationToken); + return new CommissionSummaryItem( + rows.Count, + rows.Select(item => item.StudentUserId).Where(id => id.HasValue).Distinct().Count(), + rows.Sum(item => item.GrossAmountCents), + rows.Sum(item => item.CommissionAmountCents)); + } + + public async Task> GetOrdersAsync(CommissionAdminActor actor, CommissionPeriodQuery query, CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var rows = await BuildSourcesAsync(actor.TenantId, query, cancellationToken); + return new CommissionList(rows.Take(Math.Clamp(query.Limit ?? 100, 1, 500)).Select(ToSourceItem).ToArray()); + } + + public async Task> GetSettlementsAsync(CommissionAdminActor actor, CommissionSettlementQuery query, CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var items = dbContext.CommissionSettlements.AsNoTracking().Where(item => item.TenantId == actor.TenantId); + if (query.ReferrerUserId.HasValue) items = items.Where(item => item.ReferrerUserId == query.ReferrerUserId.Value); + if (!string.IsNullOrWhiteSpace(query.Status)) items = items.Where(item => item.Status == ParseEnum(query.Status, CommissionSettlementStatus.Draft, "invalid_commission_status")); + var result = await items.OrderByDescending(item => item.CreatedAt).Take(Math.Clamp(query.Limit ?? 100, 1, 500)).ToArrayAsync(cancellationToken); + return new CommissionList(result.Select(ToSettlementItem).ToArray()); + } + + public async Task ExportSettlementAsync(CommissionAdminActor actor, Guid settlementId, string? format, CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var settlement = await GetSettlementAsync(actor.TenantId, settlementId, cancellationToken); + var items = await dbContext.CommissionSettlementItems.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && item.SettlementId == settlementId) + .OrderBy(item => item.SourcePaidAt) + .ToArrayAsync(cancellationToken); + var resolvedFormat = string.Equals(format, "json", StringComparison.OrdinalIgnoreCase) ? "json" : "csv"; + var content = resolvedFormat == "json" + ? JsonSerializer.Serialize(items.Select(ToSourceItem)) + : BuildCsv(items); + var bytes = Encoding.UTF8.GetBytes(content); + var sha = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); + var filename = $"{settlement.SettlementNo}.{resolvedFormat}"; + dbContext.CommissionSettlementExportEvents.Add(new CommissionSettlementExportEvent + { + TenantId = actor.TenantId, + SettlementId = settlementId, + ExportedBy = actor.UserId, + ExportFormat = resolvedFormat, + Filename = filename, + RowCount = items.Length, + ContentSha256 = sha, + Metadata = JsonSerializer.SerializeToElement(new { sizeBytes = bytes.Length }) + }); + await AddAuditAsync(actor, "commission.settlement.exported", "commission_settlements", settlementId, new { format = resolvedFormat, filename, rowCount = items.Length, sha }, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + return new CommissionExportItem(settlementId, filename, resolvedFormat, resolvedFormat == "json" ? "application/json" : "text/csv", items.Length, Convert.ToBase64String(bytes), sha, bytes.Length); + } + + public async Task GenerateSettlementAsync(CommissionAdminActor actor, GenerateCommissionSettlementCommand command, CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + if (command.EndDate < command.StartDate) throw new CommissionException("Commission period was invalid.", "invalid_commission_period"); + var query = new CommissionPeriodQuery(command.StartDate, command.EndDate, command.ReferrerUserId, 5000); + var candidates = (await BuildSourcesAsync(actor.TenantId, query, cancellationToken)).Where(item => item.SettlementId is null).ToArray(); + if (candidates.Length == 0) throw new CommissionException("No unsettled commission sources found.", "commission_no_unsettled_sources"); + var settings = await GetSettingsCoreAsync(actor.TenantId, cancellationToken); + var amount = candidates.Sum(item => item.CommissionAmountCents); + if (amount < settings.MinSettlementCents) throw new CommissionException("Commission amount is below settlement minimum.", "commission_below_minimum"); + var settlement = new CommissionSettlement + { + TenantId = actor.TenantId, + ReferrerUserId = command.ReferrerUserId, + GeneratedBy = actor.UserId, + SettlementNo = $"CS{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{RandomNumberGenerator.GetInt32(1000, 9999)}", + Status = ParseEnum(command.Status, CommissionSettlementStatus.PendingReview, "invalid_commission_status"), + PeriodStart = command.StartDate, + PeriodEnd = command.EndDate, + SourceCount = candidates.Length, + PaidUserCount = candidates.Select(item => item.StudentUserId).Where(id => id.HasValue).Distinct().Count(), + GrossAmountCents = candidates.Sum(item => item.GrossAmountCents), + CommissionAmountCents = amount, + DefaultRate = settings.DefaultRate, + Remark = command.Remark, + Metadata = command.Metadata ?? JsonDefaults.Object() + }; + dbContext.CommissionSettlements.Add(settlement); + foreach (var source in candidates) + { + dbContext.CommissionSettlementItems.Add(new CommissionSettlementItem + { + TenantId = actor.TenantId, + SettlementId = settlement.Id, + ReferrerUserId = source.ReferrerUserId, + StudentUserId = source.StudentUserId, + SourceType = source.SourceType, + SourceId = source.SourceId, + SourceNo = source.SourceNo, + SourcePaidAt = source.SourcePaidAt, + GrossAmountCents = source.GrossAmountCents, + CommissionRate = source.CommissionRate, + CommissionAmountCents = source.CommissionAmountCents, + RateSource = source.RateSource, + AttributionType = source.AttributionType, + Metadata = JsonSerializer.SerializeToElement(new { source = "commission_generate" }) + }); + } + await dbContext.SaveChangesAsync(cancellationToken); + return ToSettlementItem(settlement); + } + + public async Task UpdateSettlementStatusAsync(CommissionAdminActor actor, UpdateCommissionSettlementStatusCommand command, CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var item = await GetSettlementAsync(actor.TenantId, command.SettlementId, cancellationToken); + var status = ParseEnum(command.Status, CommissionSettlementStatus.Approved, "invalid_commission_status"); + if (item.Status == CommissionSettlementStatus.Paid && status != CommissionSettlementStatus.Paid) + throw new CommissionException("Paid settlement cannot be reverted.", "commission_status_invalid"); + item.Status = status; + item.Remark = command.ReviewNote ?? item.Remark; + item.Metadata = command.Metadata ?? item.Metadata; + if (status is CommissionSettlementStatus.Approved or CommissionSettlementStatus.Rejected or CommissionSettlementStatus.Cancelled) + { + item.ReviewedBy = actor.UserId; + item.ReviewedAt = DateTimeOffset.UtcNow; + } + if (status == CommissionSettlementStatus.Paid) + { + item.PaidBy = actor.UserId; + item.PaidAt = DateTimeOffset.UtcNow; + item.PaymentMethod = command.PaymentMethod ?? item.PaymentMethod; + item.PaymentAccount = command.PaymentAccount ?? item.PaymentAccount; + } + await dbContext.SaveChangesAsync(cancellationToken); + return ToSettlementItem(item); + } + + public async Task> GetProofsAsync(CommissionAdminActor actor, Guid settlementId, CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + await GetSettlementAsync(actor.TenantId, settlementId, cancellationToken); + var items = await dbContext.CommissionSettlementProofs.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && item.SettlementId == settlementId) + .OrderByDescending(item => item.CreatedAt) + .ToArrayAsync(cancellationToken); + return new CommissionList(items.Select(ToProofItem).ToArray()); + } + + public async Task CreateProofAsync(CommissionAdminActor actor, CreateCommissionProofCommand command, CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var settlement = await GetSettlementAsync(actor.TenantId, command.SettlementId, cancellationToken); + var proof = new CommissionSettlementProof + { + TenantId = actor.TenantId, + SettlementId = command.SettlementId, + SubmittedBy = actor.UserId, + ProofType = ParseEnum(command.ProofType, CommissionProofType.Payment, "invalid_commission_proof_type"), + Status = CommissionProofStatus.Submitted, + Title = command.Title ?? $"{settlement.SettlementNo} 打款凭证", + Description = command.Description, + AssetId = command.AssetId, + ExternalUrl = command.ExternalUrl, + AmountCents = command.AmountCents ?? settlement.CommissionAmountCents, + PaymentMethod = command.PaymentMethod, + PaymentAccount = command.PaymentAccount, + PaidAt = command.PaidAt, + Metadata = command.Metadata ?? JsonDefaults.Object() + }; + dbContext.CommissionSettlementProofs.Add(proof); + await dbContext.SaveChangesAsync(cancellationToken); + return ToProofItem(proof); + } + + public async Task UpdateProofStatusAsync(CommissionAdminActor actor, UpdateCommissionProofStatusCommand command, CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var proof = await dbContext.CommissionSettlementProofs.FirstOrDefaultAsync(item => item.TenantId == actor.TenantId && item.Id == command.ProofId, cancellationToken) + ?? throw new CommissionException("Commission proof was not found.", "commission_proof_not_found"); + var status = ParseEnum(command.Status, CommissionProofStatus.Approved, "invalid_commission_proof_status"); + if (status == CommissionProofStatus.Submitted) throw new CommissionException("Cannot move proof back to submitted.", "invalid_commission_proof_status"); + if (proof.Status is CommissionProofStatus.Approved or CommissionProofStatus.Rejected or CommissionProofStatus.Voided && proof.Status != status) + throw new CommissionException("Closed proof cannot change status.", "commission_proof_closed"); + proof.Status = status; + proof.ReviewedBy = actor.UserId; + proof.ReviewedAt = DateTimeOffset.UtcNow; + proof.ReviewNote = command.ReviewNote ?? proof.ReviewNote; + proof.Metadata = command.Metadata ?? proof.Metadata; + await dbContext.SaveChangesAsync(cancellationToken); + return ToProofItem(proof); + } + + private async Task> BuildSourcesAsync(Guid tenantId, CommissionPeriodQuery query, CancellationToken cancellationToken) + { + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var startDate = query.StartDate ?? today.AddDays(-29); + var endDate = query.EndDate ?? today; + var start = new DateTimeOffset(startDate.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc)); + var end = new DateTimeOffset(endDate.AddDays(1).ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc)); + var settings = await GetSettingsCoreAsync(tenantId, cancellationToken); + var existing = await dbContext.CommissionSettlementItems.AsNoTracking() + .Where(item => item.TenantId == tenantId) + .Select(item => new { item.SourceType, item.SourceId, item.SettlementId }) + .ToArrayAsync(cancellationToken); + var result = new List(); + var orders = await ( + from order in dbContext.Orders.AsNoTracking() + join lead in dbContext.ReferralLeads.AsNoTracking() + on new { order.TenantId, StudentUserId = order.UserId!.Value } equals new { lead.TenantId, lead.StudentUserId } + where order.TenantId == tenantId && order.UserId != null && lead.ReferrerUserId != null && + order.Status == OrderStatus.Paid && order.PaidAt >= start && order.PaidAt < end + select new { order, lead }) + .ToArrayAsync(cancellationToken); + foreach (var row in orders) + { + if (query.ReferrerUserId.HasValue && row.lead.ReferrerUserId != query.ReferrerUserId) continue; + var rate = GetMemberRate(await GetMembershipPermissionsAsync(tenantId, row.lead.ReferrerUserId!.Value, cancellationToken)) ?? settings.DefaultRate; + var settled = existing.FirstOrDefault(item => item.SourceType == CommissionSourceType.Order && item.SourceId == row.order.Id)?.SettlementId; + result.Add(new SourceCandidate(CommissionSourceType.Order, row.order.Id, row.order.OrderNo, row.lead.ReferrerUserId.Value, row.order.UserId, row.order.AmountCents, rate, (int)Math.Round(row.order.AmountCents * rate), CommissionRateSource.Member, settled, row.order.PaidAt, "protected_lead")); + } + var codes = await ( + from code in dbContext.ActivationCodes.AsNoTracking() + join batch in dbContext.CodeBatches.AsNoTracking() + on new { code.TenantId, BatchId = code.BatchId } equals new { batch.TenantId, BatchId = (Guid?)batch.Id } into batches + from batch in batches.DefaultIfEmpty() + where code.TenantId == tenantId && code.IsUsed && code.AgentUserId != null && code.UsedAt >= start && code.UsedAt < end + select new { code, batch }) + .ToArrayAsync(cancellationToken); + foreach (var row in codes) + { + if (!row.code.AgentUserId.HasValue) continue; + var agentUserId = row.code.AgentUserId.Value; + if (query.ReferrerUserId.HasValue && agentUserId != query.ReferrerUserId) continue; + var rate = row.batch?.CommissionRate ?? GetMemberRate(await GetMembershipPermissionsAsync(tenantId, agentUserId, cancellationToken)) ?? settings.DefaultRate; + var sourceAmount = row.code.UnitPriceCents ?? row.batch?.DefaultUnitPriceCents ?? 0; + var settled = existing.FirstOrDefault(item => item.SourceType == CommissionSourceType.ActivationCode && item.SourceId == row.code.Id)?.SettlementId; + result.Add(new SourceCandidate(CommissionSourceType.ActivationCode, row.code.Id, row.code.Code, agentUserId, row.code.UsedBy, sourceAmount, rate, (int)Math.Round(sourceAmount * rate), row.batch?.CommissionRate is null ? CommissionRateSource.Member : CommissionRateSource.Batch, settled, row.code.UsedAt, "activation_code_agent")); + } + return result.OrderBy(item => item.SourcePaidAt).ToArray(); + } + + private async Task GetMembershipPermissionsAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken) => + await dbContext.TenantMemberships.AsNoTracking().Where(item => item.TenantId == tenantId && item.UserId == userId).Select(item => item.Permissions).FirstOrDefaultAsync(cancellationToken); + + private static decimal? GetMemberRate(JsonElement permissions) => + permissions.ValueKind == JsonValueKind.Object && permissions.TryGetProperty("commissionRate", out var value) && value.ValueKind == JsonValueKind.Number && value.TryGetDecimal(out var rate) ? rate : null; + + private async Task GetSettingsCoreAsync(Guid tenantId, CancellationToken cancellationToken) + { + var settings = await dbContext.TenantCommissionSettings.FirstOrDefaultAsync(item => item.TenantId == tenantId, cancellationToken); + if (settings is not null) return settings; + settings = new TenantCommissionSetting { TenantId = tenantId }; + dbContext.TenantCommissionSettings.Add(settings); + return settings; + } + + private async Task GetSettlementAsync(Guid tenantId, Guid settlementId, CancellationToken cancellationToken) => + await dbContext.CommissionSettlements.FirstOrDefaultAsync(item => item.TenantId == tenantId && item.Id == settlementId, cancellationToken) + ?? throw new CommissionException("Commission settlement was not found.", "commission_settlement_not_found"); + + private async Task AssertAdminAsync(CommissionAdminActor actor, CancellationToken cancellationToken) + { + var ok = await dbContext.TenantMemberships.AnyAsync(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId && item.Status == MembershipStatus.Active && (item.Role == TenantRole.PlatformAdmin || item.Role == TenantRole.TenantOwner || item.Role == TenantRole.TenantAdmin), cancellationToken); + if (!ok) throw new CommissionException("Commission admin access was denied.", "commission_access_denied"); + } + + private async Task AddAuditAsync(CommissionAdminActor actor, string action, string targetType, Guid targetId, object details, CancellationToken cancellationToken) + { + dbContext.AuditLogs.Add(new AuditLog { TenantId = actor.TenantId, ActorUserId = actor.UserId, Action = action, TargetType = targetType, TargetId = targetId.ToString(), Details = JsonSerializer.SerializeToElement(details) }); + await Task.CompletedTask.WaitAsync(cancellationToken); + } + + private static string BuildCsv(IEnumerable items) + { + var builder = new StringBuilder("sourceType,sourceNo,referrerUserId,studentUserId,grossAmountCents,commissionRate,commissionAmountCents\n"); + foreach (var item in items) + builder.Append(CultureInfo.InvariantCulture, $"{item.SourceType},{item.SourceNo},{item.ReferrerUserId},{item.StudentUserId},{item.GrossAmountCents},{item.CommissionRate},{item.CommissionAmountCents}\n"); + return builder.ToString(); + } + + private static CommissionSettingsItem ToSettingsItem(TenantCommissionSetting item) => new(item.DefaultRate, item.MinSettlementCents, item.SettlementCycle.ToString(), item.Config); + private static CommissionSourceItem ToSourceItem(SourceCandidate item) => new(item.SourceType.ToString(), item.SourceId, item.SourceNo, item.ReferrerUserId, item.StudentUserId, item.GrossAmountCents, item.CommissionRate, item.CommissionAmountCents, item.RateSource.ToString(), item.SettlementId); + private static CommissionSourceItem ToSourceItem(CommissionSettlementItem item) => new(item.SourceType.ToString(), item.SourceId, item.SourceNo, item.ReferrerUserId, item.StudentUserId, item.GrossAmountCents, item.CommissionRate, item.CommissionAmountCents, item.RateSource.ToString(), item.SettlementId); + private static CommissionSettlementItemDto ToSettlementItem(CommissionSettlement item) => new(item.Id, item.SettlementNo, item.ReferrerUserId, item.Status.ToString(), item.PeriodStart, item.PeriodEnd, item.SourceCount, item.PaidUserCount, item.GrossAmountCents, item.CommissionAmountCents, item.ReviewedAt, item.PaidAt); + private static CommissionProofItem ToProofItem(CommissionSettlementProof item) => new(item.Id, item.SettlementId, item.ProofType.ToString(), item.Status.ToString(), item.Title, item.AssetId, item.ExternalUrl, item.AmountCents, item.PaymentMethod, item.PaymentAccount, item.PaidAt, item.ReviewedAt); + + private static TEnum ParseEnum(string? value, TEnum defaultValue, string errorCode) where TEnum : struct, Enum + { + if (string.IsNullOrWhiteSpace(value)) return defaultValue; + var normalized = value.Replace("_", string.Empty, StringComparison.Ordinal); + foreach (var enumValue in Enum.GetValues()) + if (string.Equals(enumValue.ToString(), normalized, StringComparison.OrdinalIgnoreCase)) return enumValue; + throw new CommissionException("Commission enum value was invalid.", errorCode); + } + + private sealed record SourceCandidate(CommissionSourceType SourceType, Guid SourceId, string? SourceNo, Guid ReferrerUserId, Guid? StudentUserId, int GrossAmountCents, decimal CommissionRate, int CommissionAmountCents, CommissionRateSource RateSource, Guid? SettlementId, DateTimeOffset? SourcePaidAt, string AttributionType); +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/GrowthConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/GrowthConfigurations.cs index c5015ba..5fa7cfe 100644 --- a/Tiku.Infrastructure/Persistence/Configurations/GrowthConfigurations.cs +++ b/Tiku.Infrastructure/Persistence/Configurations/GrowthConfigurations.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Content; using Tiku.Domain.Growth; using Tiku.Domain.Identity; using Tiku.Domain.Tenancy; @@ -257,3 +258,56 @@ internal sealed class CommissionSettlementItemConfiguration : IEntityTypeConfigu builder.HasOne().WithMany().HasForeignKey(entity => entity.StudentUserId).OnDelete(DeleteBehavior.SetNull); } } + +internal sealed class CommissionSettlementProofConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("commission_settlement_proofs"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.ProofType).HasSnakeCaseEnum(); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.Title).HasMaxLength(200); + builder.Property(entity => entity.ExternalUrl).HasMaxLength(2048); + builder.Property(entity => entity.PaymentMethod).HasMaxLength(100); + builder.Property(entity => entity.PaymentAccount).HasMaxLength(200); + builder.Property(entity => entity.Metadata).IsJson("{}"); + builder.HasIndex(entity => new { entity.TenantId, entity.SettlementId, entity.Status, entity.CreatedAt }); + builder.ToTable(table => table.HasCheckConstraint( + "ck_commission_settlement_proofs_amount", + "amount_cents is null or amount_cents >= 0")); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.SettlementId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.AssetId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.SetNull); + builder.HasOne().WithMany().HasForeignKey(entity => entity.SubmittedBy).OnDelete(DeleteBehavior.SetNull); + builder.HasOne().WithMany().HasForeignKey(entity => entity.ReviewedBy).OnDelete(DeleteBehavior.SetNull); + } +} + +internal sealed class CommissionSettlementExportEventConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("commission_settlement_export_events"); + builder.HasKey(entity => entity.Id); + builder.Property(entity => entity.Id).HasDefaultValueSql("gen_random_uuid()"); + builder.Property(entity => entity.ExportFormat).HasMaxLength(20); + builder.Property(entity => entity.Filename).HasMaxLength(500); + builder.Property(entity => entity.ContentSha256).HasMaxLength(128); + builder.Property(entity => entity.Metadata).IsJson("{}"); + builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); + builder.HasIndex(entity => new { entity.TenantId, entity.SettlementId, entity.CreatedAt }); + builder.ToTable(table => table.HasCheckConstraint("ck_commission_export_events_rows", "row_count >= 0")); + builder.HasOne().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.SettlementId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany().HasForeignKey(entity => entity.ExportedBy).OnDelete(DeleteBehavior.SetNull); + } +} diff --git a/Tiku.Infrastructure/Persistence/Migrations/20260726122712_AddCommissionSettlementProofs.Designer.cs b/Tiku.Infrastructure/Persistence/Migrations/20260726122712_AddCommissionSettlementProofs.Designer.cs new file mode 100644 index 0000000..bf0046c --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260726122712_AddCommissionSettlementProofs.Designer.cs @@ -0,0 +1,16416 @@ +// +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("20260726122712_AddCommissionSettlementProofs")] + partial class AddCommissionSettlementProofs + { + /// + 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.UseIdentityByDefaultColumns(modelBuilder); + + 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.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.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("BatchId", "RowNo") + .IsUnique() + .HasDatabaseName("ix_commerce_reconciliation_items_batch_id_row_no"); + + 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", "OrderNo", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_items_tenant_id_order_no_created_at"); + + b.HasIndex("TenantId", "BatchId", "MatchStatus", "RowNo") + .HasDatabaseName("ix_commerce_reconciliation_items_tenant_id_batch_id_match_stat~"); + + b.HasIndex("TenantId", "Provider", "ProviderTradeNo", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_items_tenant_id_provider_provider_t~"); + + b.HasIndex("TenantId", "RefundNo", "ProviderRefundNo", "CreatedAt") + .HasDatabaseName("ix_commerce_reconciliation_items_tenant_id_refund_no_provider_~"); + + b.ToTable("commerce_reconciliation_items", null, t => + { + t.HasCheckConstraint("ck_commerce_reconciliation_items_amounts", "amount_cents >= 0 and refund_amount_cents >= 0"); + + t.HasCheckConstraint("ck_commerce_reconciliation_items_row", "row_no > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceRefundEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActorUserId") + .HasColumnType("uuid") + .HasColumnName("actor_user_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Details") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("details") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("event_type"); + + b.Property("FromStatus") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("from_status"); + + b.Property("RefundRequestId") + .HasColumnType("uuid") + .HasColumnName("refund_request_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("ToStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("to_status"); + + b.HasKey("Id") + .HasName("pk_commerce_refund_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commerce_refund_events_tenant_id_id"); + + b.HasIndex("ActorUserId") + .HasDatabaseName("ix_commerce_refund_events_actor_user_id"); + + b.HasIndex("TenantId", "RefundRequestId", "CreatedAt") + .HasDatabaseName("ix_commerce_refund_events_tenant_id_refund_request_id_created_~"); + + b.ToTable("commerce_refund_events", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceRefundRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("cancelled_at"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EntitlementAction") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("entitlement_action"); + + b.Property("FailedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("failed_at"); + + b.Property("FailureReason") + .HasColumnType("text") + .HasColumnName("failure_reason"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("PaymentId") + .HasColumnType("uuid") + .HasColumnName("payment_id"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at"); + + b.Property("ProcessedBy") + .HasColumnType("uuid") + .HasColumnName("processed_by"); + + b.Property("Provider") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("ProviderRefundNo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("provider_refund_no"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("RefundNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("refund_no"); + + b.Property("RequestedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("requested_at") + .HasDefaultValueSql("now()"); + + b.Property("RequestedBy") + .HasColumnType("uuid") + .HasColumnName("requested_by"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("reviewed_at"); + + b.Property("ReviewedBy") + .HasColumnType("uuid") + .HasColumnName("reviewed_by"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("SucceededAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("succeeded_at"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_commerce_refund_requests"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_commerce_refund_requests_tenant_id_id"); + + b.HasIndex("ProcessedBy") + .HasDatabaseName("ix_commerce_refund_requests_processed_by"); + + b.HasIndex("RequestedBy") + .HasDatabaseName("ix_commerce_refund_requests_requested_by"); + + b.HasIndex("ReviewedBy") + .HasDatabaseName("ix_commerce_refund_requests_reviewed_by"); + + b.HasIndex("TenantId", "PaymentId") + .HasDatabaseName("ix_commerce_refund_requests_tenant_id_payment_id"); + + b.HasIndex("TenantId", "RefundNo") + .IsUnique() + .HasDatabaseName("ix_commerce_refund_requests_tenant_id_refund_no"); + + b.HasIndex("TenantId", "OrderId", "CreatedAt") + .HasDatabaseName("ix_commerce_refund_requests_tenant_id_order_id_created_at"); + + b.HasIndex("TenantId", "Status", "CreatedAt") + .HasDatabaseName("ix_commerce_refund_requests_tenant_id_status_created_at"); + + b.ToTable("commerce_refund_requests", null, t => + { + t.HasCheckConstraint("ck_commerce_refund_requests_amount", "amount_cents > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("code"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DiscountType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("discount_type"); + + b.Property("DiscountValue") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("discount_value"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("MaxUses") + .HasColumnType("integer") + .HasColumnName("max_uses"); + + b.Property("PlanId") + .HasColumnType("uuid") + .HasColumnName("plan_id"); + + b.Property("Remark") + .HasColumnType("text") + .HasColumnName("remark"); + + b.Property("Source") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UsedCount") + .HasColumnType("integer") + .HasColumnName("used_count"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone") + .HasColumnName("valid_from"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone") + .HasColumnName("valid_to"); + + b.HasKey("Id") + .HasName("pk_coupons"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_coupons_tenant_id_id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_coupons_tenant_id_code"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_coupons_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "PlanId") + .HasDatabaseName("ix_coupons_tenant_id_plan_id"); + + b.ToTable("coupons", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CouponRedemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_at"); + + b.Property("CouponCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("coupon_code"); + + b.Property("CouponId") + .HasColumnType("uuid") + .HasColumnName("coupon_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("DiscountAppliedCents") + .HasColumnType("integer") + .HasColumnName("discount_applied_cents"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("PlanId") + .HasColumnType("uuid") + .HasColumnName("plan_id"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("Remark") + .HasColumnType("text") + .HasColumnName("remark"); + + b.Property("Source") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_coupon_redemptions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_coupon_redemptions_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_coupon_redemptions_user_id"); + + b.HasIndex("TenantId", "CouponId") + .HasDatabaseName("ix_coupon_redemptions_tenant_id_coupon_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_coupon_redemptions_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "PlanId") + .HasDatabaseName("ix_coupon_redemptions_tenant_id_plan_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_coupon_redemptions_tenant_id_region_id"); + + b.HasIndex("TenantId", "OrderId", "UserId") + .HasDatabaseName("ix_coupon_redemptions_tenant_id_order_id_user_id") + .HasFilter("order_id is not null"); + + b.HasIndex("TenantId", "UserId", "CouponId") + .IsUnique() + .HasDatabaseName("ix_coupon_redemptions_tenant_id_user_id_coupon_id") + .HasFilter("coupon_id is not null"); + + b.ToTable("coupon_redemptions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Entitlement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EntitlementType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("entitlement_type"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("LegacySourceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_source_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.Property("RevokedBy") + .HasColumnType("uuid") + .HasColumnName("revoked_by"); + + b.Property("RevokedReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("revoked_reason"); + + b.Property("ScopeId") + .HasColumnType("uuid") + .HasColumnName("scope_id"); + + b.Property("ScopeType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("scope_type"); + + b.Property("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_type"); + + b.Property("StartsAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_at") + .HasDefaultValueSql("now()"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_entitlements"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_entitlements_tenant_id_id"); + + b.HasIndex("RevokedBy") + .HasDatabaseName("ix_entitlements_revoked_by"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_entitlements_user_id"); + + b.HasIndex("TenantId", "UserId", "Status", "ExpiresAt") + .HasDatabaseName("ix_entitlements_tenant_id_user_id_status_expires_at"); + + b.ToTable("entitlements", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Days") + .HasColumnType("integer") + .HasColumnName("days"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyPlanId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_plan_id"); + + b.Property("LegacyRegionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_region_id"); + + b.Property("LegacyUserId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_user_id"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("order_no"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("paid_at"); + + b.Property("PayMethod") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("pay_method"); + + b.Property("PayProvider") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("pay_provider"); + + b.Property("PlanId") + .HasColumnType("uuid") + .HasColumnName("plan_id"); + + b.Property("ProductName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("product_name"); + + b.Property("ProductType") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("product_type"); + + b.Property("RawPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("raw_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RefundedAmountCents") + .HasColumnType("integer") + .HasColumnName("refunded_amount_cents"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TradeNo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("trade_no"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_orders"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_orders_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_orders_user_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_orders_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "OrderNo") + .IsUnique() + .HasDatabaseName("ix_orders_tenant_id_order_no"); + + b.HasIndex("TenantId", "PlanId") + .HasDatabaseName("ix_orders_tenant_id_plan_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_orders_tenant_id_region_id"); + + b.HasIndex("TenantId", "UserId", "CreatedAt") + .HasDatabaseName("ix_orders_tenant_id_user_id_created_at"); + + b.ToTable("orders", null, t => + { + t.HasCheckConstraint("ck_orders_amount", "amount_cents >= 0"); + + t.HasCheckConstraint("ck_orders_refunded_amount", "refunded_amount_cents >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ItemId") + .HasColumnType("uuid") + .HasColumnName("item_id"); + + b.Property("ItemType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("item_type"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("quantity"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalAmountCents") + .HasColumnType("integer") + .HasColumnName("total_amount_cents"); + + b.Property("UnitAmountCents") + .HasColumnType("integer") + .HasColumnName("unit_amount_cents"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_order_items"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_order_items_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_order_items_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "OrderId") + .HasDatabaseName("ix_order_items_tenant_id_order_id"); + + b.ToTable("order_items", null, t => + { + t.HasCheckConstraint("ck_order_items_amounts", "unit_amount_cents >= 0 and total_amount_cents >= 0"); + + t.HasCheckConstraint("ck_order_items_quantity", "quantity > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyOrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_order_id"); + + b.Property("Method") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("method"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasColumnName("order_id"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("paid_at"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("ProviderTradeNo") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("provider_trade_no"); + + b.Property("RawPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("raw_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RefundedAmountCents") + .HasColumnType("integer") + .HasColumnName("refunded_amount_cents"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_payments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_payments_tenant_id_id"); + + b.HasIndex("Provider", "ProviderTradeNo") + .IsUnique() + .HasDatabaseName("ix_payments_provider_provider_trade_no") + .HasFilter("provider_trade_no is not null"); + + 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.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("Provider", "EventId") + .IsUnique() + .HasDatabaseName("ix_payment_events_provider_event_id"); + + b.HasIndex("TenantId", "PaymentId") + .HasDatabaseName("ix_payment_events_tenant_id_payment_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.Commerce.TenantPaymentAccount", 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("DisplayName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("mode"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + 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_payment_accounts"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_payment_accounts_tenant_id_id"); + + b.HasIndex("TenantId", "Provider") + .IsUnique() + .HasDatabaseName("ix_tenant_payment_accounts_tenant_id_provider"); + + b.ToTable("tenant_payment_accounts", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.TenantSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amount_cents"); + + b.Property("BillingCycle") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("billing_cycle"); + + 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("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PlanCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("plan_code"); + + 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_subscriptions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_subscriptions_tenant_id_id"); + + b.HasIndex("TenantId", "Status", "ExpiresAt") + .HasDatabaseName("ix_tenant_subscriptions_tenant_id_status_expires_at"); + + b.ToTable("tenant_subscriptions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.TenantUsageRecord", 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("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("MetricKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("metric_key"); + + b.Property("MetricValue") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)") + .HasColumnName("metric_value"); + + b.Property("PeriodEnd") + .HasColumnType("date") + .HasColumnName("period_end"); + + b.Property("PeriodStart") + .HasColumnType("date") + .HasColumnName("period_start"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id") + .HasName("pk_tenant_usage_records"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_usage_records_tenant_id_id"); + + b.HasIndex("TenantId", "MetricKey", "PeriodStart", "PeriodEnd") + .HasDatabaseName("ix_tenant_usage_records_tenant_id_metric_key_period_start_peri~"); + + b.ToTable("tenant_usage_records", (string)null); + }); + + 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("JobId", "RowNo") + .IsUnique() + .HasDatabaseName("ix_content_import_items_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.QuestionBankGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.PrimitiveCollection("AllowedPlanCodes") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text[]") + .HasColumnName("allowed_plan_codes") + .HasDefaultValueSql("'{}'::text[]"); + + b.PrimitiveCollection("AllowedRegionIds") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("uuid[]") + .HasColumnName("allowed_region_ids") + .HasDefaultValueSql("'{}'::uuid[]"); + + b.PrimitiveCollection("AllowedSubjectIds") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("uuid[]") + .HasColumnName("allowed_subject_ids") + .HasDefaultValueSql("'{}'::uuid[]"); + + b.PrimitiveCollection("AllowedTenantIds") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("uuid[]") + .HasColumnName("allowed_tenant_ids") + .HasDefaultValueSql("'{}'::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("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("GrantScope") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("grant_scope"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SourceQuestionBankId") + .HasColumnType("uuid") + .HasColumnName("source_question_bank_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("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_bank_grants"); + + b.HasIndex("AllowedPlanCodes") + .HasDatabaseName("ix_question_bank_grants_allowed_plan_codes"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("AllowedPlanCodes"), "gin"); + + b.HasIndex("AllowedTenantIds") + .HasDatabaseName("ix_question_bank_grants_allowed_tenant_ids"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("AllowedTenantIds"), "gin"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_question_bank_grants_created_by"); + + b.HasIndex("UpdatedBy") + .HasDatabaseName("ix_question_bank_grants_updated_by"); + + b.HasIndex("SourceQuestionBankId", "Status", "StartsAt", "ExpiresAt") + .HasDatabaseName("ix_question_bank_grants_source_question_bank_id_status_starts_~"); + + b.ToTable("question_bank_grants", (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("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("TenantId", "QuestionId") + .HasDatabaseName("ix_question_collection_items_tenant_id_question_id"); + + b.HasIndex("TenantId", "CollectionId", "QuestionId") + .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.TenantQuestionBankAdoption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AdoptionMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("adoption_mode"); + + b.Property("CopiedQuestionCount") + .HasColumnType("integer") + .HasColumnName("copied_question_count"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("GrantId") + .HasColumnType("uuid") + .HasColumnName("grant_id"); + + b.Property("LastSyncedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_synced_at"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SourceQuestionBankId") + .HasColumnType("uuid") + .HasColumnName("source_question_bank_id"); + + b.Property("SourceSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("source_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("SyncStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("sync_status"); + + b.Property("TargetCollectionId") + .HasColumnType("uuid") + .HasColumnName("target_collection_id"); + + b.Property("TargetEntryId") + .HasColumnType("uuid") + .HasColumnName("target_entry_id"); + + b.Property("TargetQuestionBankId") + .HasColumnType("uuid") + .HasColumnName("target_question_bank_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.HasKey("Id") + .HasName("pk_tenant_question_bank_adoptions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_question_bank_adoptions_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_tenant_question_bank_adoptions_created_by"); + + b.HasIndex("GrantId") + .HasDatabaseName("ix_tenant_question_bank_adoptions_grant_id"); + + b.HasIndex("SourceQuestionBankId") + .HasDatabaseName("ix_tenant_question_bank_adoptions_source_question_bank_id"); + + b.HasIndex("UpdatedBy") + .HasDatabaseName("ix_tenant_question_bank_adoptions_updated_by"); + + b.HasIndex("TenantId", "SourceQuestionBankId") + .IsUnique() + .HasDatabaseName("ix_tenant_question_bank_adoptions_tenant_id_source_question_ba~"); + + b.HasIndex("TenantId", "TargetCollectionId") + .HasDatabaseName("ix_tenant_question_bank_adoptions_tenant_id_target_collection_~"); + + b.HasIndex("TenantId", "TargetEntryId") + .HasDatabaseName("ix_tenant_question_bank_adoptions_tenant_id_target_entry_id"); + + b.HasIndex("TenantId", "TargetQuestionBankId") + .HasDatabaseName("ix_tenant_question_bank_adoptions_tenant_id_target_question_ba~"); + + b.HasIndex("TenantId", "Status", "UpdatedAt") + .HasDatabaseName("ix_tenant_question_bank_adoptions_tenant_id_status_updated_at"); + + b.ToTable("tenant_question_bank_adoptions", null, t => + { + t.HasCheckConstraint("ck_tenant_question_bank_adoptions_copied_count", "copied_question_count >= 0"); + }); + }); + + 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.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("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("avatar_url"); + + 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("LastSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_at"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyPasswordHash") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("legacy_password_hash"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PasswordMigrationRequired") + .HasColumnType("boolean") + .HasColumnName("password_migration_required"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("phone"); + + 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("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("Username") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_users_email"); + + b.HasIndex("LegacyId") + .IsUnique() + .HasDatabaseName("ix_users_legacy_id"); + + b.HasIndex("Phone") + .IsUnique() + .HasDatabaseName("ix_users_phone"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("ix_users_username"); + + 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("SecretPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("secret_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + 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("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + 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("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("QuestionVersionId") + .HasColumnType("uuid") + .HasColumnName("question_version_id"); + + b.Property("SelectedOptions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("selected_options") + .HasDefaultValueSql("'[]'::jsonb"); + + 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", "QuestionId") + .HasDatabaseName("ix_answer_records_tenant_id_question_id"); + + b.HasIndex("TenantId", "QuestionId", "QuestionVersionId") + .HasDatabaseName("ix_answer_records_tenant_id_question_id_question_version_id"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId") + .HasDatabaseName("ix_answer_records_tenant_id_user_id_practice_session_id"); + + b.ToTable("answer_records", null, t => + { + t.HasCheckConstraint("ck_answer_records_version_requires_question", "question_version_id is null or question_id is not 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("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source"); + + b.HasKey("TenantId", "UserId", "QuestionId") + .HasName("pk_favorite_questions"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_favorite_questions_user_id"); + + b.HasIndex("TenantId", "QuestionId") + .HasDatabaseName("ix_favorite_questions_tenant_id_question_id"); + + b.ToTable("favorite_questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeAccessEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("access_mode"); + + b.Property("ConsumedFreeQuota") + .HasColumnType("integer") + .HasColumnName("consumed_free_quota"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EntitlementId") + .HasColumnType("uuid") + .HasColumnName("entitlement_id"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("event_type"); + + b.Property("GrantedCount") + .HasColumnType("integer") + .HasColumnName("granted_count"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("reason"); + + b.Property("RequestedCount") + .HasColumnType("integer") + .HasColumnName("requested_count"); + + b.Property("ScopeId") + .HasColumnType("uuid") + .HasColumnName("scope_id"); + + b.Property("ScopeType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("scope_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_practice_access_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_access_events_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_practice_access_events_user_id"); + + b.HasIndex("TenantId", "PracticeSessionId") + .HasDatabaseName("ix_practice_access_events_tenant_id_practice_session_id"); + + b.HasIndex("TenantId", "UserId", "CreatedAt") + .HasDatabaseName("ix_practice_access_events_tenant_id_user_id_created_at"); + + b.ToTable("practice_access_events", null, t => + { + t.HasCheckConstraint("ck_practice_access_events_consumed_free_quota", "consumed_free_quota >= 0"); + + t.HasCheckConstraint("ck_practice_access_events_granted_count", "granted_count >= 0"); + + t.HasCheckConstraint("ck_practice_access_events_requested_count", "requested_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeDailyUsage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FreeLimit") + .HasColumnType("integer") + .HasColumnName("free_limit"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ScopeId") + .HasColumnType("uuid") + .HasColumnName("scope_id"); + + b.Property("ScopeType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("scope_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UsageDate") + .ValueGeneratedOnAdd() + .HasColumnType("date") + .HasColumnName("usage_date") + .HasDefaultValueSql("current_date"); + + b.Property("UsedCount") + .HasColumnType("integer") + .HasColumnName("used_count"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_practice_daily_usage"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_daily_usage_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_practice_daily_usage_user_id"); + + b.HasIndex("TenantId", "UserId", "UsageDate") + .HasDatabaseName("ix_practice_daily_usage_tenant_id_user_id_usage_date"); + + b.HasIndex("TenantId", "UserId", "UsageDate", "ScopeType", "ScopeId") + .IsUnique() + .HasDatabaseName("ix_practice_daily_usage_tenant_id_user_id_usage_date_scope_typ~"); + + NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("TenantId", "UserId", "UsageDate", "ScopeType", "ScopeId"), false); + + b.ToTable("practice_daily_usage", null, t => + { + t.HasCheckConstraint("ck_practice_daily_usage_free_limit", "free_limit >= 0"); + + t.HasCheckConstraint("ck_practice_daily_usage_used_count", "used_count >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessEntitlementId") + .HasColumnType("uuid") + .HasColumnName("access_entitlement_id"); + + b.Property("AccessMode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("free") + .HasColumnName("access_mode"); + + b.Property("AccessSnapshot") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("access_snapshot") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("BlueprintId") + .HasColumnType("uuid") + .HasColumnName("blueprint_id"); + + b.Property("CollectionId") + .HasColumnType("uuid") + .HasColumnName("collection_id"); + + b.Property("ConsumedFreeQuota") + .HasColumnType("integer") + .HasColumnName("consumed_free_quota"); + + b.Property("ContentNodeId") + .HasColumnType("uuid") + .HasColumnName("content_node_id"); + + b.Property("DurationMinutes") + .HasColumnType("integer") + .HasColumnName("duration_minutes"); + + b.Property("EntryId") + .HasColumnType("uuid") + .HasColumnName("entry_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_at"); + + b.Property("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("QuestionIds") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("question_ids") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("StartedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at") + .HasDefaultValueSql("now()"); + + 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.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.ToTable("practice_sessions", null, t => + { + t.HasCheckConstraint("ck_practice_sessions_consumed_free_quota", "consumed_free_quota >= 0"); + }); + }); + + 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("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mode"); + + 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("SectionStats") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("section_stats") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + 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("WrongCount") + .HasColumnType("integer") + .HasColumnName("wrong_count"); + + b.Property("WrongQuestionIds") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("wrong_question_ids") + .HasDefaultValueSql("'[]'::jsonb"); + + b.HasKey("Id") + .HasName("pk_practice_session_reports"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_session_reports_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_practice_session_reports_user_id"); + + b.HasIndex("TenantId", "CollectionId") + .HasDatabaseName("ix_practice_session_reports_tenant_id_collection_id"); + + b.HasIndex("TenantId", "PracticeSessionId") + .IsUnique() + .HasDatabaseName("ix_practice_session_reports_tenant_id_practice_session_id"); + + b.HasIndex("TenantId", "BlueprintId", "SubmittedAt") + .HasDatabaseName("ix_practice_session_reports_tenant_id_blueprint_id_submitted_at") + .HasFilter("blueprint_id is not null"); + + b.HasIndex("TenantId", "UserId", "SubmittedAt") + .HasDatabaseName("ix_practice_session_reports_tenant_id_user_id_submitted_at"); + + b.ToTable("practice_session_reports", null, t => + { + t.HasCheckConstraint("ck_practice_session_reports_accuracy", "accuracy >= 0 and accuracy <= 1"); + + t.HasCheckConstraint("ck_practice_session_reports_counts", "total_questions >= 0 and answered_count >= 0 and correct_count >= 0 and wrong_count >= 0 and unanswered_count >= 0"); + + t.HasCheckConstraint("ck_practice_session_reports_duration", "duration_seconds >= 0"); + + t.HasCheckConstraint("ck_practice_session_reports_scores", "score >= 0 and total_score >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSessionReportSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Accuracy") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)") + .HasColumnName("accuracy"); + + b.Property("AnsweredCount") + .HasColumnType("integer") + .HasColumnName("answered_count"); + + b.Property("CorrectCount") + .HasColumnType("integer") + .HasColumnName("correct_count"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("QuestionCount") + .HasColumnType("integer") + .HasColumnName("question_count"); + + b.Property("QuestionType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("question_type"); + + b.Property("ReportId") + .HasColumnType("uuid") + .HasColumnName("report_id"); + + b.Property("Score") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("score"); + + b.Property("SectionKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("section_key"); + + b.Property("SectionName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("section_name"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalScore") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("total_score"); + + b.Property("UnansweredCount") + .HasColumnType("integer") + .HasColumnName("unanswered_count"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("WrongCount") + .HasColumnType("integer") + .HasColumnName("wrong_count"); + + b.HasKey("Id") + .HasName("pk_practice_session_report_sections"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_session_report_sections_tenant_id_id"); + + b.HasIndex("TenantId", "PracticeSessionId") + .HasDatabaseName("ix_practice_session_report_sections_tenant_id_practice_session~"); + + b.HasIndex("TenantId", "ReportId", "SectionKey") + .IsUnique() + .HasDatabaseName("ix_practice_session_report_sections_tenant_id_report_id_sectio~"); + + b.HasIndex("TenantId", "ReportId", "SortOrder") + .HasDatabaseName("ix_practice_session_report_sections_tenant_id_report_id_sort_o~"); + + b.ToTable("practice_session_report_sections", null, t => + { + t.HasCheckConstraint("ck_practice_session_report_sections_accuracy", "accuracy >= 0 and accuracy <= 1"); + + t.HasCheckConstraint("ck_practice_session_report_sections_counts", "question_count >= 0 and answered_count >= 0 and correct_count >= 0 and wrong_count >= 0 and unanswered_count >= 0"); + + t.HasCheckConstraint("ck_practice_session_report_sections_scores", "score >= 0 and total_score >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.RecentPractice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Color") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("color"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LastAccessAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_access_at"); + + b.Property("LastPracticeAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_practice_at"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PracticeType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("practice_type"); + + b.Property("Progress") + .HasColumnType("integer") + .HasColumnName("progress"); + + b.Property("TargetLegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("target_legacy_id"); + + b.Property("TargetName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("target_name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_recent_practices"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_recent_practices_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_recent_practices_user_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_recent_practices_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "UserId", "LastAccessAt") + .HasDatabaseName("ix_recent_practices_tenant_id_user_id_last_access_at"); + + b.ToTable("recent_practices", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.Report", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Attachments") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("attachments") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("category"); + + b.Property("Contact") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("contact"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("HandledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("handled_at"); + + b.Property("HandledBy") + .HasColumnType("uuid") + .HasColumnName("handled_by"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("priority"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("Resolution") + .HasColumnType("text") + .HasColumnName("resolution"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("title"); + + b.Property("Type") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_reports"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_reports_tenant_id_id"); + + b.HasIndex("HandledBy") + .HasDatabaseName("ix_reports_handled_by"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_reports_user_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_reports_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "QuestionId", "CreatedAt") + .HasDatabaseName("ix_reports_tenant_id_question_id_created_at") + .HasFilter("question_id is not null"); + + b.HasIndex("TenantId", "Status", "CreatedAt") + .HasDatabaseName("ix_reports_tenant_id_status_created_at"); + + b.ToTable("reports", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ReportStatusEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActorUserId") + .HasColumnType("uuid") + .HasColumnName("actor_user_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FromStatus") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("from_status"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Note") + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("ReportId") + .HasColumnType("uuid") + .HasColumnName("report_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("ToStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("to_status"); + + b.HasKey("Id") + .HasName("pk_report_status_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_report_status_events_tenant_id_id"); + + b.HasIndex("ActorUserId") + .HasDatabaseName("ix_report_status_events_actor_user_id"); + + b.HasIndex("TenantId", "ReportId", "CreatedAt") + .HasDatabaseName("ix_report_status_events_tenant_id_report_id_created_at"); + + b.ToTable("report_status_events", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.RevenueDailyStat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CodeCount") + .HasColumnType("integer") + .HasColumnName("code_count"); + + b.Property("CodeEstimated") + .HasColumnType("integer") + .HasColumnName("code_estimated"); + + b.Property("CodeUsed") + .HasColumnType("integer") + .HasColumnName("code_used"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EstimatedRevenueCents") + .HasColumnType("integer") + .HasColumnName("estimated_revenue_cents"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyRegionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_region_id"); + + b.Property("OrderCount") + .HasColumnType("integer") + .HasColumnName("order_count"); + + b.Property("RealRevenueCents") + .HasColumnType("integer") + .HasColumnName("real_revenue_cents"); + + b.Property("RebuiltAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("rebuilt_at"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SaleType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("sale_type"); + + b.Property("StatDate") + .HasColumnType("date") + .HasColumnName("stat_date"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_revenue_daily_stats"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_revenue_daily_stats_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_revenue_daily_stats_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_revenue_daily_stats_tenant_id_region_id"); + + b.HasIndex("TenantId", "StatDate", "RegionId", "SaleType") + .IsUnique() + .HasDatabaseName("ix_revenue_daily_stats_tenant_id_stat_date_region_id_sale_type"); + + NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("TenantId", "StatDate", "RegionId", "SaleType"), false); + + b.ToTable("revenue_daily_stats", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.UserScoreEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BalanceAfter") + .HasColumnType("integer") + .HasColumnName("balance_after"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("event_type"); + + b.Property("IdempotencyKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("idempotency_key"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Points") + .HasColumnType("integer") + .HasColumnName("points"); + + b.Property("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property("SourceType") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_score_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_user_score_events_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_score_events_user_id"); + + b.HasIndex("TenantId", "IdempotencyKey") + .IsUnique() + .HasDatabaseName("ix_user_score_events_tenant_id_idempotency_key"); + + b.HasIndex("TenantId", "UserId", "CreatedAt") + .HasDatabaseName("ix_user_score_events_tenant_id_user_id_created_at"); + + b.ToTable("user_score_events", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.WrongQuestion", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_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", "QuestionId") + .HasName("pk_wrong_questions"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_wrong_questions_user_id"); + + b.HasIndex("TenantId", "QuestionId") + .HasDatabaseName("ix_wrong_questions_tenant_id_question_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.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.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.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.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.PlatformDunningNotificationChannel", 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_dunning_notification_channels"); + + b.HasIndex("ChannelCode") + .IsUnique() + .HasDatabaseName("ix_platform_dunning_notification_channels_channel_code"); + + b.HasIndex("Enabled", "MinReminderLevel", "ChannelCode") + .HasDatabaseName("ix_platform_dunning_notification_channels_enabled_min_reminder~"); + + b.ToTable("platform_dunning_notification_channels", null, t => + { + t.HasCheckConstraint("ck_platform_dunning_channels_level", "min_reminder_level between 1 and 20"); + + t.HasCheckConstraint("ck_platform_dunning_channels_timeout", "timeout_seconds between 1 and 60"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformDunningNotificationEvent", 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_dunning_notification_events"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_platform_dunning_notification_events_tenant_id_id"); + + b.HasIndex("ChannelId", "ReminderId") + .IsUnique() + .HasDatabaseName("ix_platform_dunning_notification_events_channel_id_reminder_id"); + + b.HasIndex("TenantId", "InvoiceId") + .HasDatabaseName("ix_platform_dunning_notification_events_tenant_id_invoice_id"); + + b.HasIndex("TenantId", "ReminderId") + .HasDatabaseName("ix_platform_dunning_notification_events_tenant_id_reminder_id"); + + b.HasIndex("InvoiceId", "Status", "CreatedAt") + .HasDatabaseName("ix_platform_dunning_notification_events_invoice_id_status_crea~"); + + b.HasIndex("ReminderId", "Status", "CreatedAt") + .HasDatabaseName("ix_platform_dunning_notification_events_reminder_id_status_cre~"); + + b.HasIndex("TenantId", "Status", "CreatedAt") + .HasDatabaseName("ix_platform_dunning_notification_events_tenant_id_status_creat~"); + + b.HasIndex("Status", "NextAttemptAt", "ScheduledAt", "CreatedAt") + .HasDatabaseName("ix_platform_dunning_notification_events_status_next_attempt_at~"); + + b.ToTable("platform_dunning_notification_events", null, t => + { + t.HasCheckConstraint("ck_platform_dunning_events_attempts", "attempts >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.PlatformSaasPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BaseAmountCents") + .HasColumnType("integer") + .HasColumnName("base_amount_cents"); + + b.Property("BillingCycle") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("billing_cycle"); + + 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("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("currency"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("FeatureFlags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("feature_flags") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("IncludedQuotas") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("included_quotas") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("OveragePrices") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("overage_prices") + .HasDefaultValueSql("'{}'::jsonb"); + + 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_platform_saas_plans"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_platform_saas_plans_code"); + + b.HasIndex("Status", "SortOrder") + .HasDatabaseName("ix_platform_saas_plans_status_sort_order"); + + b.ToTable("platform_saas_plans", null, t => + { + t.HasCheckConstraint("ck_platform_saas_plans_amount", "base_amount_cents >= 0"); + }); + }); + + 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.TenantInvoice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BalanceCents") + .HasColumnType("integer") + .HasColumnName("balance_cents"); + + b.Property("BillingPeriodEnd") + .HasColumnType("date") + .HasColumnName("billing_period_end"); + + b.Property("BillingPeriodStart") + .HasColumnType("date") + .HasColumnName("billing_period_start"); + + 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("DiscountCents") + .HasColumnType("integer") + .HasColumnName("discount_cents"); + + b.Property("DueDate") + .HasColumnType("date") + .HasColumnName("due_date"); + + b.Property("InvoiceNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("invoice_no"); + + b.Property("InvoiceType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("invoice_type"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("issued_at"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Note") + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("paid_at"); + + b.Property("PaidCents") + .HasColumnType("integer") + .HasColumnName("paid_cents"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("SubtotalCents") + .HasColumnType("integer") + .HasColumnName("subtotal_cents"); + + b.Property("TaxCents") + .HasColumnType("integer") + .HasColumnName("tax_cents"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TotalCents") + .HasColumnType("integer") + .HasColumnName("total_cents"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenant_invoices"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_invoices_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_tenant_invoices_created_by"); + + b.HasIndex("InvoiceNo") + .IsUnique() + .HasDatabaseName("ix_tenant_invoices_invoice_no"); + + b.HasIndex("TenantId", "BillingPeriodStart", "BillingPeriodEnd") + .IsUnique() + .HasDatabaseName("ix_tenant_invoices_tenant_id_billing_period_start_billing_peri~") + .HasFilter("invoice_type = 'usage_overage' and status <> 'void' and metadata->>'source' = 'usage_overage_auto'"); + + b.HasIndex("TenantId", "Status", "DueDate") + .HasDatabaseName("ix_tenant_invoices_tenant_id_status_due_date"); + + b.ToTable("tenant_invoices", null, t => + { + t.HasCheckConstraint("ck_tenant_invoices_amounts", "subtotal_cents >= 0 and discount_cents >= 0 and tax_cents >= 0 and total_cents >= 0 and paid_cents >= 0 and balance_cents >= 0"); + + t.HasCheckConstraint("ck_tenant_invoices_period", "billing_period_end is null or billing_period_start is null or billing_period_end >= billing_period_start"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantInvoiceItem", 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("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("InvoiceId") + .HasColumnType("uuid") + .HasColumnName("invoice_id"); + + b.Property("ItemRefId") + .HasColumnType("uuid") + .HasColumnName("item_ref_id"); + + b.Property("ItemType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("item_type"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Quantity") + .HasPrecision(12, 2) + .HasColumnType("numeric(12,2)") + .HasColumnName("quantity"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UnitAmountCents") + .HasColumnType("integer") + .HasColumnName("unit_amount_cents"); + + b.HasKey("Id") + .HasName("pk_tenant_invoice_items"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_invoice_items_tenant_id_id"); + + b.HasIndex("InvoiceId") + .HasDatabaseName("ix_tenant_invoice_items_invoice_id"); + + b.HasIndex("TenantId", "InvoiceId") + .HasDatabaseName("ix_tenant_invoice_items_tenant_id_invoice_id"); + + b.ToTable("tenant_invoice_items", null, t => + { + t.HasCheckConstraint("ck_tenant_invoice_items_amounts", "unit_amount_cents >= 0 and amount_cents >= 0"); + + t.HasCheckConstraint("ck_tenant_invoice_items_quantity", "quantity > 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantInvoicePayment", 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("InvoiceId") + .HasColumnType("uuid") + .HasColumnName("invoice_id"); + + b.Property("Method") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("method"); + + 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("RawPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("raw_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ReceivedBy") + .HasColumnType("uuid") + .HasColumnName("received_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_invoice_payments"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_invoice_payments_tenant_id_id"); + + b.HasIndex("PaymentNo") + .IsUnique() + .HasDatabaseName("ix_tenant_invoice_payments_payment_no"); + + b.HasIndex("ReceivedBy") + .HasDatabaseName("ix_tenant_invoice_payments_received_by"); + + b.HasIndex("InvoiceId", "Status") + .HasDatabaseName("ix_tenant_invoice_payments_invoice_id_status"); + + b.HasIndex("TenantId", "InvoiceId") + .HasDatabaseName("ix_tenant_invoice_payments_tenant_id_invoice_id"); + + b.ToTable("tenant_invoice_payments", null, t => + { + t.HasCheckConstraint("ck_tenant_invoice_payments_amount", "amount_cents >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantInvoiceReminder", 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_tenant_invoice_reminders"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_invoice_reminders_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_tenant_invoice_reminders_created_by"); + + b.HasIndex("InvoiceId", "ReminderDate") + .HasDatabaseName("ix_tenant_invoice_reminders_invoice_id_reminder_date"); + + b.HasIndex("TenantId", "Status", "ReminderDate") + .HasDatabaseName("ix_tenant_invoice_reminders_tenant_id_status_reminder_date"); + + b.HasIndex("TenantId", "InvoiceId", "ReminderType", "Channel", "ReminderDate") + .IsUnique() + .HasDatabaseName("ix_tenant_invoice_reminders_tenant_id_invoice_id_reminder_type~"); + + b.ToTable("tenant_invoice_reminders", null, t => + { + t.HasCheckConstraint("ck_tenant_invoice_reminders_balance", "balance_cents_snapshot >= 0"); + + t.HasCheckConstraint("ck_tenant_invoice_reminders_level", "reminder_level between 1 and 20"); + }); + }); + + 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("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("SourceScope") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("source_scope"); + + 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", "RegionId") + .HasDatabaseName("ix_question_banks_tenant_id_region_id"); + + b.ToTable("question_banks", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerText") + .HasColumnType("text") + .HasColumnName("answer_text"); + + b.Property("CodeLang") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code_lang"); + + b.Property("CodeTemplate") + .HasColumnType("text") + .HasColumnName("code_template"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CorrectOptionIndex") + .HasColumnType("integer") + .HasColumnName("correct_option_index"); + + b.Property("CorrectOptionIndices") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("correct_option_indices") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("Explanation") + .HasColumnType("text") + .HasColumnName("explanation"); + + b.Property("Options") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("options") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("SourceHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_hash"); + + b.Property("SubQuestions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sub_questions") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_question_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "QuestionId", "Id") + .HasName("ak_question_versions_tenant_id_question_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_question_versions_created_by"); + + b.HasIndex("QuestionId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_question_versions_question_id_version_no"); + + b.ToTable("question_versions", (string)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.HasAlternateKey("TenantId", "Id") + .HasName("ak_auth_login_events_tenant_id_id"); + + 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("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_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.HasAlternateKey("TenantId", "Id") + .HasName("ak_auth_sessions_tenant_id_id"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ix_auth_sessions_token_hash"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_auth_sessions_user_id"); + + b.HasIndex("TenantId", "UserId", "ExpiresAt") + .HasDatabaseName("ix_auth_sessions_tenant_id_user_id_expires_at") + .HasFilter("revoked_at is null"); + + b.ToTable("auth_sessions", (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.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.HasAlternateKey("TenantId", "Id") + .HasName("ak_sms_verification_codes_tenant_id_id"); + + b.HasIndex("TenantId", "Phone", "Purpose") + .IsUnique() + .HasDatabaseName("ix_sms_verification_codes_tenant_id_phone_purpose") + .HasFilter("consumed_at is null and status in ('pending', 'sent')"); + + b.HasIndex("TenantId", "Phone", "Purpose", "CreatedAt") + .HasDatabaseName("ix_sms_verification_codes_tenant_id_phone_purpose_created_at") + .HasFilter("consumed_at is null and status in ('pending', 'sent')"); + + b.HasIndex("TenantId", "Phone", "Purpose", "ExpiresAt") + .HasDatabaseName("ix_sms_verification_codes_tenant_id_phone_purpose_expires_at"); + + b.ToTable("sms_verification_codes", null, t => + { + t.HasCheckConstraint("ck_sms_verification_codes_attempts", "attempts >= 0"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BillingStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("billing_status"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegalName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("legal_name"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("mode"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("OwnerUserId") + .HasColumnType("uuid") + .HasColumnName("owner_user_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("slug"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.HasIndex("LegacyId") + .IsUnique() + .HasDatabaseName("ix_tenants_legacy_id"); + + b.HasIndex("OwnerUserId") + .HasDatabaseName("ix_tenants_owner_user_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_tenants_slug"); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantAuthProvider", 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("DisplayName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + 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_auth_providers"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_auth_providers_tenant_id_id"); + + b.HasIndex("TenantId", "Provider") + .IsUnique() + .HasDatabaseName("ix_tenant_auth_providers_tenant_id_provider"); + + b.HasIndex("TenantId", "Provider", "Status") + .HasDatabaseName("ix_tenant_auth_providers_tenant_id_provider_status"); + + b.ToTable("tenant_auth_providers", (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("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("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("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"); + + 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.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("Permissions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("permissions") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("role"); + + b.Property("RoleTemplateId") + .HasColumnType("uuid") + .HasColumnName("role_template_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("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", "RoleTemplateId") + .HasDatabaseName("ix_tenant_memberships_tenant_id_role_template_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.TenantRoleTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BaseRole") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("base_role"); + + 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("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("DataScope") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("data_scope") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("FieldPermissions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("field_permissions") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("MenuPermissions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("menu_permissions") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ModulePermissions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("module_permissions") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Permissions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("permissions") + .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("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_role_templates"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_role_templates_tenant_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_tenant_role_templates_created_by"); + + b.HasIndex("UpdatedBy") + .HasDatabaseName("ix_tenant_role_templates_updated_by"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasDatabaseName("ix_tenant_role_templates_tenant_id_code"); + + b.HasIndex("TenantId", "Status", "SortOrder") + .HasDatabaseName("ix_tenant_role_templates_tenant_id_status_sort_order"); + + b.ToTable("tenant_role_templates", (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("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("SecretPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("secret_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + 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", (string)null); + }); + + 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("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.CommerceReconciliationBatch", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_reconciliation_batches_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_reconciliation_batches_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceReconciliationIssue", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("AssignedTo") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_reconciliation_issues_users_assigned_to"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_reconciliation_issues_users_created_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ResolvedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_reconciliation_issues_users_resolved_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_reconciliation_issues_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceReconciliationBatch", null) + .WithMany() + .HasForeignKey("TenantId", "BatchId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_issues_commerce_reconciliation_batc~"); + + b.HasOne("Tiku.Domain.Commerce.CommerceReconciliationItem", null) + .WithMany() + .HasForeignKey("TenantId", "ItemId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_issues_commerce_reconciliation_item~"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_issues_orders_tenant_id_order_id"); + + b.HasOne("Tiku.Domain.Commerce.Payment", null) + .WithMany() + .HasForeignKey("TenantId", "PaymentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_issues_payments_tenant_id_payment_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceRefundRequest", null) + .WithMany() + .HasForeignKey("TenantId", "RefundRequestId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_issues_commerce_refund_requests_ten~"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceReconciliationIssueEvent", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ActorUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_reconciliation_issue_events_users_actor_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_reconciliation_issue_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceReconciliationIssue", null) + .WithMany() + .HasForeignKey("TenantId", "IssueId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_reconciliation_issue_events_commerce_reconciliatio~"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceReconciliationItem", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_reconciliation_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceReconciliationBatch", null) + .WithMany() + .HasForeignKey("TenantId", "BatchId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_reconciliation_items_commerce_reconciliation_batch~"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_items_orders_tenant_id_order_id"); + + b.HasOne("Tiku.Domain.Commerce.Payment", null) + .WithMany() + .HasForeignKey("TenantId", "PaymentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_items_payments_tenant_id_payment_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceRefundRequest", null) + .WithMany() + .HasForeignKey("TenantId", "RefundRequestId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_reconciliation_items_commerce_refund_requests_tena~"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceRefundEvent", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ActorUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_refund_events_users_actor_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_refund_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.CommerceRefundRequest", null) + .WithMany() + .HasForeignKey("TenantId", "RefundRequestId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_refund_events_commerce_refund_requests_tenant_id_r~"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CommerceRefundRequest", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ProcessedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_refund_requests_users_processed_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RequestedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_refund_requests_users_requested_by"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ReviewedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_commerce_refund_requests_users_reviewed_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_refund_requests_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_commerce_refund_requests_orders_tenant_id_order_id"); + + b.HasOne("Tiku.Domain.Commerce.Payment", null) + .WithMany() + .HasForeignKey("TenantId", "PaymentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_commerce_refund_requests_payments_tenant_id_payment_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Coupon", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_coupons_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.SvipPlan", null) + .WithMany() + .HasForeignKey("TenantId", "PlanId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_coupons_svip_plans_tenant_id_plan_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.CouponRedemption", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_coupon_redemptions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_coupon_redemptions_users_user_id"); + + b.HasOne("Tiku.Domain.Commerce.Coupon", null) + .WithMany() + .HasForeignKey("TenantId", "CouponId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_coupon_redemptions_coupons_tenant_id_coupon_id"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_coupon_redemptions_orders_tenant_id_order_id"); + + b.HasOne("Tiku.Domain.Commerce.SvipPlan", null) + .WithMany() + .HasForeignKey("TenantId", "PlanId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_coupon_redemptions_svip_plans_tenant_id_plan_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_coupon_redemptions_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Entitlement", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("RevokedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_entitlements_users_revoked_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_entitlements_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_entitlements_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Order", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_orders_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_orders_users_user_id"); + + b.HasOne("Tiku.Domain.Commerce.SvipPlan", null) + .WithMany() + .HasForeignKey("TenantId", "PlanId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_orders_svip_plans_tenant_id_plan_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_orders_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.OrderItem", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_order_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_order_items_orders_tenant_id_order_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Payment", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_payments_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.Order", null) + .WithMany() + .HasForeignKey("TenantId", "OrderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_payments_orders_tenant_id_order_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PaymentEvent", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_payment_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Commerce.Payment", null) + .WithMany() + .HasForeignKey("TenantId", "PaymentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_payment_events_payments_tenant_id_payment_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PointActivityClaim", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_point_activity_claims_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_point_activity_claims_users_user_id"); + + b.HasOne("Tiku.Domain.Commerce.PointActivityTask", null) + .WithMany() + .HasForeignKey("TenantId", "TaskId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_point_activity_claims_point_activity_tasks_tenant_id_task_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PointActivityTask", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_point_activity_tasks_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PointExchangeItem", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_point_exchange_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_point_exchange_items_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.PointExchangeOrder", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_point_exchange_orders_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_point_exchange_orders_users_user_id"); + + b.HasOne("Tiku.Domain.Commerce.PointExchangeItem", null) + .WithMany() + .HasForeignKey("TenantId", "ItemId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_point_exchange_orders_point_exchange_items_tenant_id_item_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.Product", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_products_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_products_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.SvipPlan", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_svip_plans_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_svip_plans_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.TenantPaymentAccount", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_payment_accounts_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.TenantSubscription", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_subscriptions_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Commerce.TenantUsageRecord", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_usage_records_tenants_tenant_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.QuestionBankGrant", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_question_bank_grants_users_created_by"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionBank", null) + .WithMany() + .HasForeignKey("SourceQuestionBankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_bank_grants_question_banks_source_question_bank_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_question_bank_grants_users_updated_by"); + }); + + 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.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.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_collection_items_questions_tenant_id_question_id"); + }); + + 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.TenantQuestionBankAdoption", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_question_bank_adoptions_users_created_by"); + + b.HasOne("Tiku.Domain.Content.QuestionBankGrant", null) + .WithMany() + .HasForeignKey("GrantId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_question_bank_adoptions_question_bank_grants_grant_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionBank", null) + .WithMany() + .HasForeignKey("SourceQuestionBankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_question_bank_adoptions_question_banks_source_questi~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_question_bank_adoptions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_question_bank_adoptions_users_updated_by"); + + b.HasOne("Tiku.Domain.Content.QuestionCollection", null) + .WithMany() + .HasForeignKey("TenantId", "TargetCollectionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tenant_question_bank_adoptions_question_collections_tenant_~"); + + b.HasOne("Tiku.Domain.Content.ContentEntry", null) + .WithMany() + .HasForeignKey("TenantId", "TargetEntryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tenant_question_bank_adoptions_content_entries_tenant_id_ta~"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionBank", null) + .WithMany() + .HasForeignKey("TenantId", "TargetQuestionBankId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tenant_question_bank_adoptions_question_banks_tenant_id_tar~"); + }); + + 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.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.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_answer_records_questions_tenant_id_question_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionVersion", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId", "QuestionVersionId") + .HasPrincipalKey("TenantId", "QuestionId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_answer_records_question_versions_tenant_id_question_id_ques~"); + + b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + .WithMany() + .HasForeignKey("TenantId", "UserId", "PracticeSessionId") + .HasPrincipalKey("TenantId", "UserId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_answer_records_practice_sessions_tenant_id_user_id_practice~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.DashboardDailyStat", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_dashboard_daily_stats_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_dashboard_daily_stats_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ExamDate", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_exam_dates_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_exam_dates_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_exam_dates_schools_tenant_id_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.FavoriteQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_favorite_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_favorite_questions_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_favorite_questions_questions_tenant_id_question_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.PracticeSessionReport", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_reports_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_reports_users_user_id"); + + b.HasOne("Tiku.Domain.Content.PracticeBlueprint", null) + .WithMany() + .HasForeignKey("TenantId", "BlueprintId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_practice_session_reports_practice_blueprints_tenant_id_blue~"); + + b.HasOne("Tiku.Domain.Content.QuestionCollection", null) + .WithMany() + .HasForeignKey("TenantId", "CollectionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_practice_session_reports_question_collections_tenant_id_col~"); + + b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + .WithMany() + .HasForeignKey("TenantId", "PracticeSessionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_reports_practice_sessions_tenant_id_practi~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSessionReportSection", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_report_sections_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + .WithMany() + .HasForeignKey("TenantId", "PracticeSessionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_report_sections_practice_sessions_tenant_i~"); + + b.HasOne("Tiku.Domain.Learning.PracticeSessionReport", null) + .WithMany() + .HasForeignKey("TenantId", "ReportId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_session_report_sections_practice_session_reports_t~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.RecentPractice", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_recent_practices_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_recent_practices_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.Report", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("HandledBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_reports_users_handled_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_reports_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_reports_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_reports_questions_tenant_id_question_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.ReportStatusEvent", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ActorUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_report_status_events_users_actor_user_id"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_report_status_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Learning.Report", null) + .WithMany() + .HasForeignKey("TenantId", "ReportId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_report_status_events_reports_tenant_id_report_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.RevenueDailyStat", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_revenue_daily_stats_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_revenue_daily_stats_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.UserScoreEvent", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_score_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_score_events_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.WrongQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_questions_tenant_id_question_id"); + }); + + 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.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.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.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.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.PlatformDunningNotificationEvent", b => + { + b.HasOne("Tiku.Domain.Platform.PlatformDunningNotificationChannel", null) + .WithMany() + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_dunning_notification_events_platform_dunning_notif~"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_dunning_notification_events_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.TenantInvoice", null) + .WithMany() + .HasForeignKey("TenantId", "InvoiceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_dunning_notification_events_tenant_invoices_tenant~"); + + b.HasOne("Tiku.Domain.Platform.TenantInvoiceReminder", null) + .WithMany() + .HasForeignKey("TenantId", "ReminderId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_platform_dunning_notification_events_tenant_invoice_reminde~"); + }); + + 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.TenantInvoice", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_invoices_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_invoices_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantInvoiceItem", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_invoice_items_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.TenantInvoice", null) + .WithMany() + .HasForeignKey("TenantId", "InvoiceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_invoice_items_tenant_invoices_tenant_id_invoice_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantInvoicePayment", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ReceivedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_invoice_payments_users_received_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_invoice_payments_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.TenantInvoice", null) + .WithMany() + .HasForeignKey("TenantId", "InvoiceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_invoice_payments_tenant_invoices_tenant_id_invoice_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Platform.TenantInvoiceReminder", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_invoice_reminders_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_invoice_reminders_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Platform.TenantInvoice", null) + .WithMany() + .HasForeignKey("TenantId", "InvoiceId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_invoice_reminders_tenant_invoices_tenant_id_invoice_~"); + }); + + 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.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.AuthLoginEvent", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .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) + .IsRequired() + .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.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.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.TenantAuthProvider", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_auth_providers_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.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"); + + b.HasOne("Tiku.Domain.Tenancy.TenantRoleTemplate", null) + .WithMany() + .HasForeignKey("TenantId", "RoleTemplateId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tenant_memberships_tenant_role_templates_tenant_id_role_tem~"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantRoleTemplate", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_role_templates_users_created_by"); + + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_role_templates_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenant_role_templates_users_updated_by"); + }); + + 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/20260726122712_AddCommissionSettlementProofs.cs b/Tiku.Infrastructure/Persistence/Migrations/20260726122712_AddCommissionSettlementProofs.cs new file mode 100644 index 0000000..d00a44c --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260726122712_AddCommissionSettlementProofs.cs @@ -0,0 +1,157 @@ +using System; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Tiku.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddCommissionSettlementProofs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "commission_settlement_export_events", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + tenant_id = table.Column(type: "uuid", nullable: false), + settlement_id = table.Column(type: "uuid", nullable: false), + exported_by = table.Column(type: "uuid", nullable: true), + export_format = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + filename = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + row_count = table.Column(type: "integer", nullable: false), + content_sha256 = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + metadata = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_commission_settlement_export_events", x => x.id); + table.CheckConstraint("ck_commission_export_events_rows", "row_count >= 0"); + table.ForeignKey( + name: "fk_commission_settlement_export_events_commission_settlements_~", + columns: x => new { x.tenant_id, x.settlement_id }, + principalTable: "commission_settlements", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_commission_settlement_export_events_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_commission_settlement_export_events_users_exported_by", + column: x => x.exported_by, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "commission_settlement_proofs", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + settlement_id = table.Column(type: "uuid", nullable: false), + asset_id = table.Column(type: "uuid", nullable: true), + submitted_by = table.Column(type: "uuid", nullable: true), + reviewed_by = table.Column(type: "uuid", nullable: true), + proof_type = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + title = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + description = table.Column(type: "text", nullable: true), + external_url = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + amount_cents = table.Column(type: "integer", nullable: true), + payment_method = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + payment_account = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + paid_at = table.Column(type: "timestamp with time zone", nullable: true), + reviewed_at = table.Column(type: "timestamp with time zone", nullable: true), + review_note = table.Column(type: "text", nullable: true), + metadata = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + 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_commission_settlement_proofs", x => x.id); + table.UniqueConstraint("ak_commission_settlement_proofs_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_commission_settlement_proofs_amount", "amount_cents is null or amount_cents >= 0"); + table.ForeignKey( + name: "fk_commission_settlement_proofs_commission_settlements_tenant_~", + columns: x => new { x.tenant_id, x.settlement_id }, + principalTable: "commission_settlements", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_commission_settlement_proofs_content_assets_tenant_id_asset~", + columns: x => new { x.tenant_id, x.asset_id }, + principalTable: "content_assets", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "fk_commission_settlement_proofs_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_commission_settlement_proofs_users_reviewed_by", + column: x => x.reviewed_by, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "fk_commission_settlement_proofs_users_submitted_by", + column: x => x.submitted_by, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "ix_commission_settlement_export_events_exported_by", + table: "commission_settlement_export_events", + column: "exported_by"); + + migrationBuilder.CreateIndex( + name: "ix_commission_settlement_export_events_tenant_id_settlement_id~", + table: "commission_settlement_export_events", + columns: new[] { "tenant_id", "settlement_id", "created_at" }); + + migrationBuilder.CreateIndex( + name: "ix_commission_settlement_proofs_reviewed_by", + table: "commission_settlement_proofs", + column: "reviewed_by"); + + migrationBuilder.CreateIndex( + name: "ix_commission_settlement_proofs_submitted_by", + table: "commission_settlement_proofs", + column: "submitted_by"); + + migrationBuilder.CreateIndex( + name: "ix_commission_settlement_proofs_tenant_id_asset_id", + table: "commission_settlement_proofs", + columns: new[] { "tenant_id", "asset_id" }); + + migrationBuilder.CreateIndex( + name: "ix_commission_settlement_proofs_tenant_id_settlement_id_status~", + table: "commission_settlement_proofs", + columns: new[] { "tenant_id", "settlement_id", "status", "created_at" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "commission_settlement_export_events"); + + migrationBuilder.DropTable( + name: "commission_settlement_proofs"); + } + } +} diff --git a/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs b/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs index f8d33a1..bf0b265 100644 --- a/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs +++ b/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs @@ -6635,6 +6635,74 @@ namespace Tiku.Infrastructure.Persistence.Migrations }); }); + 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") @@ -6751,6 +6819,128 @@ namespace Tiku.Infrastructure.Persistence.Migrations }); }); + 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") @@ -14747,6 +14937,30 @@ namespace Tiku.Infrastructure.Persistence.Migrations .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) @@ -14777,6 +14991,43 @@ namespace Tiku.Infrastructure.Persistence.Migrations .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) diff --git a/Tiku.Infrastructure/Persistence/TikuDbContext.cs b/Tiku.Infrastructure/Persistence/TikuDbContext.cs index 3589359..8651a2a 100644 --- a/Tiku.Infrastructure/Persistence/TikuDbContext.cs +++ b/Tiku.Infrastructure/Persistence/TikuDbContext.cs @@ -124,6 +124,8 @@ public sealed class TikuDbContext(DbContextOptions options) : DbC public DbSet TenantCommissionSettings => Set(); public DbSet CommissionSettlements => Set(); public DbSet CommissionSettlementItems => Set(); + public DbSet CommissionSettlementProofs => Set(); + public DbSet CommissionSettlementExportEvents => Set(); public DbSet Banners => Set(); public DbSet Faqs => Set(); public DbSet Announcements => Set(); diff --git a/Tiku.IntegrationTests/Api/CommissionEndpointTests.cs b/Tiku.IntegrationTests/Api/CommissionEndpointTests.cs new file mode 100644 index 0000000..ff1d09a --- /dev/null +++ b/Tiku.IntegrationTests/Api/CommissionEndpointTests.cs @@ -0,0 +1,179 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using Tiku.Api.Contracts; +using Tiku.Application.Auth; +using Tiku.Application.Growth; +using Tiku.Domain.Commerce; +using Tiku.Domain.Growth; +using Tiku.Domain.Identity; +using Tiku.Domain.Tenancy; +using Tiku.Infrastructure.Auth; + +namespace Tiku.IntegrationTests.Api; + +public sealed class CommissionEndpointTests +{ + [Fact] + public async Task Anonymous_commission_request_returns_401() + { + await using var factory = new ApiTestFactory(); + using var client = factory.CreateClient(); + + var response = await client.GetAsync("/api/commission/settings"); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task Admin_can_generate_export_and_proof_commission_settlement() + { + await using var factory = new ApiTestFactory(); + var seed = await SeedCommissionAsync(factory); + using var client = factory.CreateClient(); + await LoginAsync(client, seed.Admin); + + var settings = await client.PutAsJsonAsync( + "/api/commission/settings", + new UpdateCommissionSettingsDto { DefaultRate = 0.2m, MinSettlementCents = 1, SettlementCycle = "monthly" }); + var sources = await client.GetAsync($"/api/commission/orders?referrerUserId={seed.Referrer.UserId}&startDate=2026-07-01&endDate=2026-07-31"); + var summary = await client.GetAsync($"/api/commission/summary?referrerUserId={seed.Referrer.UserId}&startDate=2026-07-01&endDate=2026-07-31"); + var generate = await client.PostAsJsonAsync( + "/api/commission/settlements/generate", + new GenerateCommissionSettlementDto + { + ReferrerUserId = seed.Referrer.UserId, + StartDate = "2026-07-01", + EndDate = "2026-07-31", + Status = "pending_review" + }); + var settlement = await generate.Content.ReadFromJsonAsync(); + var duplicate = await client.PostAsJsonAsync( + "/api/commission/settlements/generate", + new GenerateCommissionSettlementDto + { + ReferrerUserId = seed.Referrer.UserId, + StartDate = "2026-07-01", + EndDate = "2026-07-31" + }); + var status = await client.PostAsJsonAsync( + "/api/commission/settlements/status", + new UpdateCommissionSettlementStatusDto + { + SettlementId = settlement!.Id, + Status = "paid", + PaymentMethod = "bank", + PaymentAccount = "****1234" + }); + var proof = await client.PostAsJsonAsync( + "/api/commission/settlements/proofs", + new CreateCommissionProofDto + { + SettlementId = settlement.Id, + ProofType = "payment", + ExternalUrl = "https://example.test/proof.png" + }); + var proofItem = await proof.Content.ReadFromJsonAsync(); + var proofStatus = await client.PostAsJsonAsync( + "/api/commission/settlements/proofs/status", + new UpdateCommissionProofStatusDto { ProofId = proofItem!.Id, Status = "approved" }); + var export = await client.GetAsync($"/api/commission/settlements/export?settlementId={settlement.Id}&format=csv"); + var exportItem = await export.Content.ReadFromJsonAsync(); + + Assert.Equal(HttpStatusCode.OK, settings.StatusCode); + Assert.Equal(HttpStatusCode.OK, sources.StatusCode); + Assert.Equal(HttpStatusCode.OK, summary.StatusCode); + Assert.Equal(HttpStatusCode.OK, generate.StatusCode); + Assert.Equal(2, settlement.SourceCount); + Assert.Equal(3000, settlement.CommissionAmountCents); + Assert.Equal(HttpStatusCode.Conflict, duplicate.StatusCode); + Assert.Equal(HttpStatusCode.OK, status.StatusCode); + Assert.Equal(HttpStatusCode.OK, proof.StatusCode); + Assert.Equal(HttpStatusCode.OK, proofStatus.StatusCode); + Assert.Equal(HttpStatusCode.OK, export.StatusCode); + Assert.Equal("csv", exportItem!.Format); + Assert.False(string.IsNullOrWhiteSpace(exportItem.Sha256)); + } + + private static async Task SeedCommissionAsync(ApiTestFactory factory) + { + var tenantId = Guid.NewGuid(); + var admin = new LoginSeed(tenantId, Guid.NewGuid(), "13800003001"); + var referrer = new LoginSeed(tenantId, Guid.NewGuid(), "13800003002"); + var student = new LoginSeed(tenantId, Guid.NewGuid(), "13800003003"); + var paidAt = new DateTimeOffset(2026, 7, 10, 12, 0, 0, TimeSpan.Zero); + var batchId = Guid.NewGuid(); + await factory.SeedAsync( + new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Commission Tenant" }, + User(admin, "tenant_admin"), + User(referrer, "sales"), + User(student, "student"), + Identity(admin), + Identity(referrer), + Identity(student), + Membership(admin, TenantRole.TenantAdmin), + Membership(referrer, TenantRole.Sales), + Membership(student, TenantRole.Student), + new ReferralLead + { + TenantId = tenantId, + StudentUserId = student.UserId, + ReferrerUserId = referrer.UserId, + RefCode = "COMM2026", + Source = "manual" + }, + new Order + { + TenantId = tenantId, + UserId = student.UserId, + OrderNo = "C202607100001", + Status = OrderStatus.Paid, + AmountCents = 10000, + ProductType = "svip", + ProductName = "SVIP", + PaidAt = paidAt + }, + new CodeBatch + { + Id = batchId, + TenantId = tenantId, + Name = "代理码", + TotalCount = 1, + DefaultUnitPriceCents = 5000, + CommissionRate = 0.2m + }, + new ActivationCode + { + TenantId = tenantId, + BatchId = batchId, + AgentUserId = referrer.UserId, + UsedBy = student.UserId, + Code = "CODE2026", + IsUsed = true, + UnitPriceCents = 5000, + UsedAt = paidAt + }); + return new CommissionSeed(tenantId, admin, referrer, student); + } + + private static User User(LoginSeed seed, string role) => new() { Id = seed.UserId, Phone = seed.Phone, Name = role, PrimaryRole = role }; + private static TenantMembership Membership(LoginSeed seed, TenantRole role) => new() { TenantId = seed.TenantId, UserId = seed.UserId, Role = role, Status = MembershipStatus.Active }; + private static UserIdentity Identity(LoginSeed seed) => new() { UserId = seed.UserId, Provider = "password", ProviderSubject = seed.Phone, Phone = seed.Phone, SecretPayload = CreateSecretPayload(new PasswordHasher().Hash("passw0rd!")) }; + + private static async Task LoginAsync(HttpClient client, LoginSeed seed) + { + var loginResponse = await client.PostAsJsonAsync("/api/auth/login/password", new PasswordLoginDto { TenantId = seed.TenantId, Phone = seed.Phone, Password = "passw0rd!" }); + loginResponse.EnsureSuccessStatusCode(); + using var loginJson = await JsonDocument.ParseAsync(await loginResponse.Content.ReadAsStreamAsync()); + client.DefaultRequestHeaders.Authorization = new("Bearer", loginJson.RootElement.GetProperty("tokens").GetProperty("accessToken").GetString()); + } + + private static JsonElement CreateSecretPayload(string passwordHash) + { + using var document = JsonDocument.Parse($$"""{"passwordHash":{{JsonSerializer.Serialize(passwordHash)}}}"""); + return document.RootElement.Clone(); + } + + private sealed record LoginSeed(Guid TenantId, Guid UserId, string Phone); + private sealed record CommissionSeed(Guid TenantId, LoginSeed Admin, LoginSeed Referrer, LoginSeed Student); +} diff --git a/Tiku.IntegrationTests/PersistenceModelTests.cs b/Tiku.IntegrationTests/PersistenceModelTests.cs index 53b6a09..6013a04 100644 --- a/Tiku.IntegrationTests/PersistenceModelTests.cs +++ b/Tiku.IntegrationTests/PersistenceModelTests.cs @@ -31,7 +31,7 @@ public sealed class PersistenceModelTests .Select(entity => entity.GetTableName()) .ToHashSet(StringComparer.Ordinal); - Assert.Equal(131, tableNames.Count); + Assert.Equal(133, tableNames.Count); Assert.Contains("tenants", tableNames); Assert.Contains("tenant_settings", tableNames); Assert.Contains("content_entries", tableNames); @@ -116,6 +116,8 @@ public sealed class PersistenceModelTests Assert.Contains("tenant_commission_settings", tableNames); Assert.Contains("commission_settlements", tableNames); Assert.Contains("commission_settlement_items", tableNames); + Assert.Contains("commission_settlement_proofs", tableNames); + Assert.Contains("commission_settlement_export_events", tableNames); Assert.Contains("banners", tableNames); Assert.Contains("faqs", tableNames); Assert.Contains("announcements", tableNames); @@ -684,6 +686,8 @@ public sealed class PersistenceModelTests [InlineData(typeof(TenantCommissionSetting), nameof(TenantCommissionSetting.Config), "'{}'::jsonb")] [InlineData(typeof(CommissionSettlement), nameof(CommissionSettlement.Metadata), "'{}'::jsonb")] [InlineData(typeof(CommissionSettlementItem), nameof(CommissionSettlementItem.Metadata), "'{}'::jsonb")] + [InlineData(typeof(CommissionSettlementProof), nameof(CommissionSettlementProof.Metadata), "'{}'::jsonb")] + [InlineData(typeof(CommissionSettlementExportEvent), nameof(CommissionSettlementExportEvent.Metadata), "'{}'::jsonb")] public void Growth_json_properties_are_mapped_to_jsonb( Type entityType, string propertyName,