From 3265e321e33e331551e597489eea790b798c6eec Mon Sep 17 00:00:00 2001 From: MOHAMED RASHED Date: Tue, 26 May 2026 13:36:34 +0300 Subject: [PATCH] fix(verification, identity): bind OTP to user, fix email/phone change persistence, add unique email constraint OTP Ownership & Security: - Add UserId to OtpVerification entity (nullable, for anonymous registration flows) - Pass recipientUserId through ContactChangeOtpService into OTP create/refresh - Add user-scoped FindActiveAsync overload in OtpVerificationRepository to prevent contact-level collisions - Add OTP ownership validation in ConfirmEmailChange and ConfirmPhoneChange handlers - Use entity.UserId in VerifyOtpCommandHandler with fallback to contact lookup for anonymous flows Email/Phone Change Fixes: - Use UserManager.SetEmailAsync + SetUserNameAsync in ConfirmEmailChange instead of raw property mutation (fixes stale NormalizedEmail) - Use UserManager for phone change confirmation with ownership validation Duplicate Account Prevention: - Add filtered unique index on NormalizedEmail to prevent duplicate accounts at DB level - Create EF migration: AddOtpVerificationUserId (adds user_id column + composite index + unique email index) --- .../src/CCE.Api.Common/Auth/DevAuthHandler.cs | 112 +- .../Endpoints/ProfileEndpoints.cs | 56 + .../CCE.Application/DependencyInjection.cs | 1 + .../Identity/IUserRepository.cs | 1 + .../ConfirmEmailChangeCommand.cs | 9 + .../ConfirmEmailChangeCommandHandler.cs | 101 + .../ConfirmEmailChangeCommandValidator.cs | 13 + .../ConfirmEmailChangeRequest.cs | 3 + .../ConfirmPhoneChangeCommand.cs | 9 + .../ConfirmPhoneChangeCommandHandler.cs | 97 + .../ConfirmPhoneChangeCommandValidator.cs | 13 + .../ConfirmPhoneChangeRequest.cs | 3 + .../Commands/ContactChangeOtpService.cs | 83 + .../RequestEmailChangeCommand.cs | 9 + .../RequestEmailChangeCommandHandler.cs | 61 + .../RequestEmailChangeCommandValidator.cs | 12 + .../RequestEmailChangeRequest.cs | 3 + .../RequestPhoneChangeCommand.cs | 10 + .../RequestPhoneChangeCommandHandler.cs | 72 + .../RequestPhoneChangeCommandValidator.cs | 12 + .../RequestPhoneChangeRequest.cs | 3 + .../Messages/MessageFactory.cs | 15 +- .../CCE.Application/Messages/SystemCode.cs | 3 + .../CCE.Application/Messages/SystemCodeMap.cs | 3 + .../RequestVerificationCommandHandler.cs | 2 +- .../VerifyOtp/VerifyOtpCommandHandler.cs | 10 +- .../IOtpVerificationRepository.cs | 4 + backend/src/CCE.Domain/Identity/User.cs | 22 + .../Verification/OtpVerification.cs | 18 +- .../Identity/UserConfiguration.cs | 7 + .../OtpVerificationConfiguration.cs | 4 + ...51_AddOtpVerificationExtraData.Designer.cs | 3848 ++++++++++++++++ ...60525154051_AddOtpVerificationExtraData.cs | 28 + ...95022_AddOtpVerificationUserId.Designer.cs | 3857 +++++++++++++++++ ...20260526095022_AddOtpVerificationUserId.cs | 58 + .../Migrations/CceDbContextModelSnapshot.cs | 33 +- .../Repositories/OtpVerificationRepository.cs | 15 +- .../Repositories/UserRepository.cs | 18 + .../CCE.Seeder/Seeders/ReferenceDataSeeder.cs | 12 + 39 files changed, 8586 insertions(+), 54 deletions(-) create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommand.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandHandler.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandValidator.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeRequest.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommand.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandHandler.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandValidator.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeRequest.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/ContactChangeOtpService.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommand.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandHandler.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandValidator.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeRequest.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommand.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandHandler.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandValidator.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeRequest.cs create mode 100644 backend/src/CCE.Infrastructure/Persistence/Migrations/20260525154051_AddOtpVerificationExtraData.Designer.cs create mode 100644 backend/src/CCE.Infrastructure/Persistence/Migrations/20260525154051_AddOtpVerificationExtraData.cs create mode 100644 backend/src/CCE.Infrastructure/Persistence/Migrations/20260526095022_AddOtpVerificationUserId.Designer.cs create mode 100644 backend/src/CCE.Infrastructure/Persistence/Migrations/20260526095022_AddOtpVerificationUserId.cs diff --git a/backend/src/CCE.Api.Common/Auth/DevAuthHandler.cs b/backend/src/CCE.Api.Common/Auth/DevAuthHandler.cs index 23299a7a..eef35ea9 100644 --- a/backend/src/CCE.Api.Common/Auth/DevAuthHandler.cs +++ b/backend/src/CCE.Api.Common/Auth/DevAuthHandler.cs @@ -71,7 +71,15 @@ public DevAuthHandler( protected override Task HandleAuthenticateAsync() { - var roles = ReadRoles(); + // PRIORITY 1: If the request carries a real JWT (e.g. from /api/auth/login), + // authenticate as the real user and skip dev-mode entirely. + var realJwtResult = TryAuthenticateRealJwt(); + if (realJwtResult is not null) + return Task.FromResult(realJwtResult); + + // PRIORITY 2: Dev-mode auth — cookie or dev-prefixed bearer header. + // Only reached when no valid real JWT is present. + var roles = ReadDevRoles(); if (roles is null || roles.Count == 0) { return Task.FromResult(AuthenticateResult.NoResult()); @@ -101,38 +109,29 @@ protected override Task HandleAuthenticateAsync() return Task.FromResult(AuthenticateResult.Success(ticket)); } - private List? ReadRoles() + /// + /// Attempts to validate the Authorization header as a real JWT issued by + /// /api/auth/login. Returns null when no header is present, + /// the token is invalid, or it is a dev-mode token. + /// + private AuthenticateResult? TryAuthenticateRealJwt() { - // Prefer cookie (browser path); fall back to bearer header (curl / Postman). - if (Request.Cookies.TryGetValue(DevCookieName, out var cookieValue) && !string.IsNullOrEmpty(cookieValue)) - { - return new List { cookieValue.Trim() }; - } + if (!Request.Headers.TryGetValue("Authorization", out var auth)) + return null; - if (Request.Headers.TryGetValue("Authorization", out var auth)) - { - var raw = auth.ToString(); + var raw = auth.ToString(); - const string devPrefix = "Bearer dev:"; - if (raw.StartsWith(devPrefix, StringComparison.OrdinalIgnoreCase)) - { - return new List { raw.Substring(devPrefix.Length).Trim() }; - } + // Skip dev-prefixed tokens — they are handled by the dev-mode path. + const string devPrefix = "Bearer dev:"; + if (raw.StartsWith(devPrefix, StringComparison.OrdinalIgnoreCase)) + return null; - // Fallback: try to decode as a real JWT (e.g. issued by /api/auth/login) - const string bearerPrefix = "Bearer "; - if (raw.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase)) - { - var token = raw.Substring(bearerPrefix.Length).Trim(); - return TryReadRolesFromJwt(token); - } - } + const string bearerPrefix = "Bearer "; + if (!raw.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase)) + return null; - return null; - } + var token = raw.Substring(bearerPrefix.Length).Trim(); - private List? TryReadRolesFromJwt(string token) - { var opts = _localAuthOptions.Value; var profiles = new[] { opts.External, opts.Internal }; var handler = new JwtSecurityTokenHandler { MapInboundClaims = false }; @@ -155,7 +154,7 @@ protected override Task HandleAuthenticateAsync() ClockSkew = TimeSpan.FromMinutes(2), }; - ClaimsPrincipal? principal; + ClaimsPrincipal principal; try { principal = handler.ValidateToken(token, parameters, out _); @@ -166,12 +165,61 @@ protected override Task HandleAuthenticateAsync() continue; } - var roles = principal.FindAll("roles").Select(c => c.Value).ToList(); - if (roles.Count > 0) - return roles; + // Extract claims directly from the validated JWT — do NOT remap to dev users. + var sub = principal.FindFirstValue("sub") + ?? principal.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrEmpty(sub)) + continue; + + var email = principal.FindFirstValue("email") ?? string.Empty; + var preferredUsername = principal.FindFirstValue("preferred_username") ?? email; + var name = principal.FindFirstValue("name") + ?? principal.FindFirstValue(ClaimTypes.Name) + ?? preferredUsername; + + var claims = new List + { + new("sub", sub), + new("oid", sub), + new("preferred_username", preferredUsername), + new("name", name), + new("email", email), + }; + claims.AddRange(principal.FindAll("roles").Select(c => new Claim("roles", c.Value))); + + var identity = new ClaimsIdentity(claims, SchemeName, "preferred_username", "roles"); + var realPrincipal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(realPrincipal, SchemeName); + return AuthenticateResult.Success(ticket); + } + + Logger.LogDebug("No valid real JWT found in DevAuthHandler; falling back to dev-mode auth"); + return null; + } + + /// + /// Reads dev-mode credentials from cookie or the Bearer dev:<role> header. + /// Returns null when neither is present. + /// + private List? ReadDevRoles() + { + // Prefer bearer header (curl / Postman) over cookie. + if (Request.Headers.TryGetValue("Authorization", out var auth)) + { + var raw = auth.ToString(); + const string devPrefix = "Bearer dev:"; + if (raw.StartsWith(devPrefix, StringComparison.OrdinalIgnoreCase)) + { + return new List { raw.Substring(devPrefix.Length).Trim() }; + } + } + + // Fall back to cookie (browser path). + if (Request.Cookies.TryGetValue(DevCookieName, out var cookieValue) && !string.IsNullOrEmpty(cookieValue)) + { + return new List { cookieValue.Trim() }; } - Logger.LogWarning("JWT validation failed for all profiles in DevAuthHandler"); return null; } } diff --git a/backend/src/CCE.Api.External/Endpoints/ProfileEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/ProfileEndpoints.cs index 11be4efc..6c484671 100644 --- a/backend/src/CCE.Api.External/Endpoints/ProfileEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/ProfileEndpoints.cs @@ -2,6 +2,10 @@ using CCE.Api.Common.Extensions; using CCE.Application.Common.Interfaces; using CCE.Application.Identity.Auth.Register; +using CCE.Application.Identity.Public.Commands.ConfirmEmailChange; +using CCE.Application.Identity.Public.Commands.ConfirmPhoneChange; +using CCE.Application.Identity.Public.Commands.RequestEmailChange; +using CCE.Application.Identity.Public.Commands.RequestPhoneChange; using CCE.Application.Identity.Public.Commands.SubmitExpertRequest; using CCE.Application.Identity.Public.Commands.UpdateMyProfile; using CCE.Application.Identity.Public.Queries.GetMyExpertStatus; @@ -100,6 +104,58 @@ public static IEndpointRouteBuilder MapProfileEndpoints(this IEndpointRouteBuild }) .WithName("GetMyExpertStatus"); + me.MapPost("/email/request-change", async ( + RequestEmailChangeRequest body, + ICurrentUserAccessor currentUser, + IMediator mediator, CancellationToken ct) => + { + var userId = currentUser.GetUserId() ?? System.Guid.Empty; + if (userId == System.Guid.Empty) return Results.Unauthorized(); + var result = await mediator.Send( + new RequestEmailChangeCommand(userId, body.NewEmail), ct).ConfigureAwait(false); + return result.ToHttpResult(); + }) + .WithName("RequestEmailChange"); + + me.MapPost("/email/confirm-change", async ( + ConfirmEmailChangeRequest body, + ICurrentUserAccessor currentUser, + IMediator mediator, CancellationToken ct) => + { + var userId = currentUser.GetUserId() ?? System.Guid.Empty; + if (userId == System.Guid.Empty) return Results.Unauthorized(); + var result = await mediator.Send( + new ConfirmEmailChangeCommand(userId, body.VerificationId, body.Code), ct).ConfigureAwait(false); + return result.ToHttpResult(); + }) + .WithName("ConfirmEmailChange"); + + me.MapPost("/phone/request-change", async ( + RequestPhoneChangeRequest body, + ICurrentUserAccessor currentUser, + IMediator mediator, CancellationToken ct) => + { + var userId = currentUser.GetUserId() ?? System.Guid.Empty; + if (userId == System.Guid.Empty) return Results.Unauthorized(); + var result = await mediator.Send( + new RequestPhoneChangeCommand(userId, body.NewPhone, body.CountryCodeId), ct).ConfigureAwait(false); + return result.ToHttpResult(); + }) + .WithName("RequestPhoneChange"); + + me.MapPost("/phone/confirm-change", async ( + ConfirmPhoneChangeRequest body, + ICurrentUserAccessor currentUser, + IMediator mediator, CancellationToken ct) => + { + var userId = currentUser.GetUserId() ?? System.Guid.Empty; + if (userId == System.Guid.Empty) return Results.Unauthorized(); + var result = await mediator.Send( + new ConfirmPhoneChangeCommand(userId, body.VerificationId, body.Code), ct).ConfigureAwait(false); + return result.ToHttpResult(); + }) + .WithName("ConfirmPhoneChange"); + return app; } } diff --git a/backend/src/CCE.Application/DependencyInjection.cs b/backend/src/CCE.Application/DependencyInjection.cs index 9d9d10e5..c6a37d47 100644 --- a/backend/src/CCE.Application/DependencyInjection.cs +++ b/backend/src/CCE.Application/DependencyInjection.cs @@ -26,6 +26,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); diff --git a/backend/src/CCE.Application/Identity/IUserRepository.cs b/backend/src/CCE.Application/Identity/IUserRepository.cs index 2a9cbab3..4b7b2701 100644 --- a/backend/src/CCE.Application/Identity/IUserRepository.cs +++ b/backend/src/CCE.Application/Identity/IUserRepository.cs @@ -6,4 +6,5 @@ public interface IUserRepository { Task FindUserIdByContactAsync(string contact, OtpVerificationType type, CancellationToken ct = default); Task StampConfirmedAsync(Guid userId, OtpVerificationType type, CancellationToken ct = default); + Task IsContactTakenAsync(string contact, OtpVerificationType type, Guid excludeUserId, CancellationToken ct = default); } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommand.cs b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommand.cs new file mode 100644 index 00000000..3f2b744b --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommand.cs @@ -0,0 +1,9 @@ +using CCE.Application.Common; +using MediatR; + +namespace CCE.Application.Identity.Public.Commands.ConfirmEmailChange; + +public sealed record ConfirmEmailChangeCommand( + System.Guid UserId, + System.Guid VerificationId, + string Code) : IRequest>; diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandHandler.cs new file mode 100644 index 00000000..64672679 --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandHandler.cs @@ -0,0 +1,101 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Messages; +using CCE.Application.Verification; +using CCE.Domain.Identity; +using MediatR; +using Microsoft.AspNetCore.Identity; + +namespace CCE.Application.Identity.Public.Commands.ConfirmEmailChange; + +internal sealed class ConfirmEmailChangeCommandHandler + : IRequestHandler> +{ + private readonly IOtpVerificationRepository _otpRepo; + private readonly IUserProfileRepository _userRepo; + private readonly UserManager _userManager; + private readonly ICceDbContext _db; + private readonly MessageFactory _msg; + private readonly IOtpCodeGenerator _codeGenerator; + + public ConfirmEmailChangeCommandHandler( + IOtpVerificationRepository otpRepo, + IUserProfileRepository userRepo, + UserManager userManager, + ICceDbContext db, + MessageFactory msg, + IOtpCodeGenerator codeGenerator) + { + _otpRepo = otpRepo; + _userRepo = userRepo; + _userManager = userManager; + _db = db; + _msg = msg; + _codeGenerator = codeGenerator; + } + + public async Task> Handle( + ConfirmEmailChangeCommand request, CancellationToken ct) + { + var now = DateTimeOffset.UtcNow; + + // WRITE — fetch OTP via repository + var otp = await _otpRepo + .GetByIdAsync(request.VerificationId, ct) + .ConfigureAwait(false); + + if (otp is null) + return _msg.OtpNotFound(); + + if (otp.IsInvalidated) + return _msg.OtpInvalidated(); + + if (otp.IsExpired(now)) + return _msg.OtpExpired(); + + if (otp.HasExceededMaxAttempts()) + return _msg.OtpMaxAttempts(); + + // Ownership validation — OTP must belong to the authenticated user + if (otp.UserId.HasValue && otp.UserId.Value != request.UserId) + return _msg.Unauthorized("OTP_UNAUTHORIZED"); + + otp.IncrementAttempt(); + + if (!_codeGenerator.Verify(request.Code, otp.CodeHash)) + { + _otpRepo.Update(otp); + await _db.SaveChangesAsync(ct).ConfigureAwait(false); + return _msg.OtpInvalidCode(); + } + + // WRITE — fetch user via repository + var user = await _userRepo + .FindAsync(request.UserId, ct) + .ConfigureAwait(false); + + if (user is null) + return _msg.UserNotFound(); + + // Use UserManager to ensure NormalizedEmail and SecurityStamp are properly updated + var setEmailResult = await _userManager.SetEmailAsync(user, otp.Contact).ConfigureAwait(false); + if (!setEmailResult.Succeeded) + return _msg.BusinessRule("EMAIL_CHANGE_FAILED"); + + // Update UserName to match the new email + var setUserNameResult = await _userManager.SetUserNameAsync(user, otp.Contact).ConfigureAwait(false); + if (!setUserNameResult.Succeeded) + return _msg.BusinessRule("EMAIL_CHANGE_FAILED"); + + // domain methods + otp.MarkVerified(); + otp.Invalidate(); + + _otpRepo.Update(otp); + + // ICceDbContext as unit of work + await _db.SaveChangesAsync(ct).ConfigureAwait(false); + + return _msg.EmailUpdated(); + } +} diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandValidator.cs b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandValidator.cs new file mode 100644 index 00000000..f65a5f4e --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; + +namespace CCE.Application.Identity.Public.Commands.ConfirmEmailChange; + +public sealed class ConfirmEmailChangeCommandValidator : AbstractValidator +{ + public ConfirmEmailChangeCommandValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.VerificationId).NotEmpty(); + RuleFor(x => x.Code).NotEmpty().Length(6).Matches(@"^\d{6}$"); + } +} diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeRequest.cs b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeRequest.cs new file mode 100644 index 00000000..d92dcfa1 --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeRequest.cs @@ -0,0 +1,3 @@ +namespace CCE.Application.Identity.Public.Commands.ConfirmEmailChange; + +public sealed record ConfirmEmailChangeRequest(System.Guid VerificationId, string Code); diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommand.cs b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommand.cs new file mode 100644 index 00000000..10778a34 --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommand.cs @@ -0,0 +1,9 @@ +using CCE.Application.Common; +using MediatR; + +namespace CCE.Application.Identity.Public.Commands.ConfirmPhoneChange; + +public sealed record ConfirmPhoneChangeCommand( + System.Guid UserId, + System.Guid VerificationId, + string Code) : IRequest>; diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandHandler.cs new file mode 100644 index 00000000..0eff3bff --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandHandler.cs @@ -0,0 +1,97 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Identity.Public.Commands.RequestPhoneChange; +using CCE.Application.Messages; +using CCE.Application.Verification; +using MediatR; + +namespace CCE.Application.Identity.Public.Commands.ConfirmPhoneChange; + +internal sealed class ConfirmPhoneChangeCommandHandler + : IRequestHandler> +{ + private readonly IOtpVerificationRepository _otpRepo; + private readonly IUserProfileRepository _userRepo; + private readonly ICceDbContext _db; + private readonly MessageFactory _msg; + private readonly IOtpCodeGenerator _codeGenerator; + + public ConfirmPhoneChangeCommandHandler( + IOtpVerificationRepository otpRepo, + IUserProfileRepository userRepo, + ICceDbContext db, + MessageFactory msg, + IOtpCodeGenerator codeGenerator) + { + _otpRepo = otpRepo; + _userRepo = userRepo; + _db = db; + _msg = msg; + _codeGenerator = codeGenerator; + } + + public async Task> Handle( + ConfirmPhoneChangeCommand request, CancellationToken ct) + { + var now = DateTimeOffset.UtcNow; + + // WRITE — fetch OTP via repository + var otp = await _otpRepo + .GetByIdAsync(request.VerificationId, ct) + .ConfigureAwait(false); + + if (otp is null) + return _msg.OtpNotFound(); + + if (otp.IsInvalidated) + return _msg.OtpInvalidated(); + + if (otp.IsExpired(now)) + return _msg.OtpExpired(); + + if (otp.HasExceededMaxAttempts()) + return _msg.OtpMaxAttempts(); + + // Ownership validation — OTP must belong to the authenticated user + if (otp.UserId.HasValue && otp.UserId.Value != request.UserId) + return _msg.Unauthorized("OTP_UNAUTHORIZED"); + + otp.IncrementAttempt(); + + if (!_codeGenerator.Verify(request.Code, otp.CodeHash)) + { + _otpRepo.Update(otp); + await _db.SaveChangesAsync(ct).ConfigureAwait(false); + return _msg.OtpInvalidCode(); + } + + // WRITE — fetch user via repository + var user = await _userRepo + .FindAsync(request.UserId, ct) + .ConfigureAwait(false); + + if (user is null) + return _msg.UserNotFound(); + + // Read CountryCodeId stored at request-time — client does not need to re-send it + System.Guid? countryCodeId = null; + if (otp.ExtraData is not null) + { + var extra = System.Text.Json.JsonSerializer.Deserialize(otp.ExtraData); + countryCodeId = extra?.CountryCodeId; + } + + // domain methods + otp.MarkVerified(); + otp.Invalidate(); + user.UpdatePhoneNumber(otp.Contact, countryCodeId); + + _otpRepo.Update(otp); + _userRepo.Update(user); + + // ICceDbContext as unit of work + await _db.SaveChangesAsync(ct).ConfigureAwait(false); + + return _msg.PhoneUpdated(); + } +} diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandValidator.cs b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandValidator.cs new file mode 100644 index 00000000..9a82ca23 --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; + +namespace CCE.Application.Identity.Public.Commands.ConfirmPhoneChange; + +public sealed class ConfirmPhoneChangeCommandValidator : AbstractValidator +{ + public ConfirmPhoneChangeCommandValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.VerificationId).NotEmpty(); + RuleFor(x => x.Code).NotEmpty().Length(6).Matches(@"^\d{6}$"); + } +} diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeRequest.cs b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeRequest.cs new file mode 100644 index 00000000..4babb61b --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeRequest.cs @@ -0,0 +1,3 @@ +namespace CCE.Application.Identity.Public.Commands.ConfirmPhoneChange; + +public sealed record ConfirmPhoneChangeRequest(System.Guid VerificationId, string Code); diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ContactChangeOtpService.cs b/backend/src/CCE.Application/Identity/Public/Commands/ContactChangeOtpService.cs new file mode 100644 index 00000000..a6d3633c --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/ContactChangeOtpService.cs @@ -0,0 +1,83 @@ +using CCE.Application.Common; +using CCE.Application.Messages; +using CCE.Application.Notifications; +using CCE.Application.Verification; +using CCE.Application.Verification.Dtos; +using CCE.Domain.Notifications; +using CCE.Domain.Verification; + +namespace CCE.Application.Identity.Public.Commands; + +/// +/// Shared OTP preparation logic for email and phone contact-change flows. +/// Handles cooldown check, code generation, OTP create-or-refresh, and notification dispatch. +/// Each handler only needs to perform its contact-specific uniqueness check then delegate here. +/// +internal sealed class ContactChangeOtpService +{ + private readonly IOtpVerificationRepository _otpRepo; + private readonly INotificationGateway _gateway; + private readonly MessageFactory _msg; + private readonly IOtpCodeGenerator _codeGenerator; + + public ContactChangeOtpService( + IOtpVerificationRepository otpRepo, + INotificationGateway gateway, + MessageFactory msg, + IOtpCodeGenerator codeGenerator) + { + _otpRepo = otpRepo; + _gateway = gateway; + _msg = msg; + _codeGenerator = codeGenerator; + } + + /// + /// Prepares an OTP for a contact-change request. + /// Returns (entity, null) on success; (null, failResponse) on cooldown. + /// Caller must call ICceDbContext.SaveChangesAsync after this returns successfully. + /// + public async Task<(OtpVerification? Entity, Response? Fail)> PrepareAsync( + string contact, + OtpVerificationType type, + string templateCode, + NotificationChannel channel, + System.Guid recipientUserId, + string? extraData, + DateTimeOffset now, + CancellationToken ct) + { + var existing = await _otpRepo + .FindActiveAsync(contact, type, now, recipientUserId, ct) + .ConfigureAwait(false); + + if (existing is not null && !existing.CanResend(now)) + return (null, _msg.OtpCooldownActive()); + + var (plainCode, codeHash) = _codeGenerator.Generate(); + + OtpVerification entity; + if (existing is not null) + { + existing.Refresh(codeHash, now, extraData, recipientUserId); + _otpRepo.Update(existing); + entity = existing; + } + else + { + entity = OtpVerification.Create(contact, type, codeHash, now, extraData, recipientUserId); + await _otpRepo.AddAsync(entity, ct).ConfigureAwait(false); + } + + await _gateway.SendAsync(new NotificationDispatchRequest( + TemplateCode: templateCode, + RecipientUserId: recipientUserId, + Channels: [channel], + Variables: new Dictionary { ["Code"] = plainCode }, + PhoneNumber: type == OtpVerificationType.Sms ? contact : null, + Email: type == OtpVerificationType.Email ? contact : null, + BypassSettings: true), ct).ConfigureAwait(false); + + return (entity, null); + } +} diff --git a/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommand.cs b/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommand.cs new file mode 100644 index 00000000..38d75d73 --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommand.cs @@ -0,0 +1,9 @@ +using CCE.Application.Common; +using CCE.Application.Verification.Dtos; +using MediatR; + +namespace CCE.Application.Identity.Public.Commands.RequestEmailChange; + +public sealed record RequestEmailChangeCommand( + System.Guid UserId, + string NewEmail) : IRequest>; diff --git a/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandHandler.cs new file mode 100644 index 00000000..c06df468 --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandHandler.cs @@ -0,0 +1,61 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Identity; +using CCE.Application.Messages; +using CCE.Application.Verification.Dtos; +using CCE.Domain.Notifications; +using CCE.Domain.Verification; +using MediatR; + +namespace CCE.Application.Identity.Public.Commands.RequestEmailChange; + +internal sealed class RequestEmailChangeCommandHandler + : IRequestHandler> +{ + private readonly IUserRepository _userRepo; + private readonly ICceDbContext _db; + private readonly ContactChangeOtpService _otpService; + private readonly MessageFactory _msg; + + public RequestEmailChangeCommandHandler( + IUserRepository userRepo, + ICceDbContext db, + ContactChangeOtpService otpService, + MessageFactory msg) + { + _userRepo = userRepo; + _db = db; + _otpService = otpService; + _msg = msg; + } + + public async Task> Handle( + RequestEmailChangeCommand request, CancellationToken ct) + { + var now = DateTimeOffset.UtcNow; + + // fetch via repository — check new email not already taken by another account + var taken = await _userRepo + .IsContactTakenAsync(request.NewEmail, OtpVerificationType.Email, request.UserId, ct) + .ConfigureAwait(false); + + if (taken) + return _msg.ContactAlreadyTaken(); + + var (entity, fail) = await _otpService.PrepareAsync( + request.NewEmail, + OtpVerificationType.Email, + templateCode: "EMAIL_CHANGE_OTP", + channel: NotificationChannel.Email, + recipientUserId: request.UserId, + extraData: null, + now, ct).ConfigureAwait(false); + + if (fail is not null) return fail; + + // ICceDbContext as unit of work + await _db.SaveChangesAsync(ct).ConfigureAwait(false); + + return _msg.Ok(new RequestVerificationResponseDto(entity!.Id, entity.ExpiresAt), "OTP_SENT"); + } +} diff --git a/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandValidator.cs b/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandValidator.cs new file mode 100644 index 00000000..26ae0786 --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; + +namespace CCE.Application.Identity.Public.Commands.RequestEmailChange; + +public sealed class RequestEmailChangeCommandValidator : AbstractValidator +{ + public RequestEmailChangeCommandValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.NewEmail).NotEmpty().EmailAddress().MaximumLength(256); + } +} diff --git a/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeRequest.cs b/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeRequest.cs new file mode 100644 index 00000000..4e6bf6d5 --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeRequest.cs @@ -0,0 +1,3 @@ +namespace CCE.Application.Identity.Public.Commands.RequestEmailChange; + +public sealed record RequestEmailChangeRequest(string NewEmail); diff --git a/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommand.cs b/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommand.cs new file mode 100644 index 00000000..869f9cd9 --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommand.cs @@ -0,0 +1,10 @@ +using CCE.Application.Common; +using CCE.Application.Verification.Dtos; +using MediatR; + +namespace CCE.Application.Identity.Public.Commands.RequestPhoneChange; + +public sealed record RequestPhoneChangeCommand( + System.Guid UserId, + string NewPhone, + System.Guid? CountryCodeId) : IRequest>; diff --git a/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandHandler.cs new file mode 100644 index 00000000..f418ac11 --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandHandler.cs @@ -0,0 +1,72 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Identity; +using CCE.Application.Messages; +using CCE.Application.Verification.Dtos; +using CCE.Domain.Identity; +using CCE.Domain.Notifications; +using CCE.Domain.Verification; +using MediatR; + +namespace CCE.Application.Identity.Public.Commands.RequestPhoneChange; + +internal sealed class RequestPhoneChangeCommandHandler + : IRequestHandler> +{ + private readonly IUserRepository _userRepo; + private readonly ICceDbContext _db; + private readonly ContactChangeOtpService _otpService; + private readonly MessageFactory _msg; + + public RequestPhoneChangeCommandHandler( + IUserRepository userRepo, + ICceDbContext db, + ContactChangeOtpService otpService, + MessageFactory msg) + { + _userRepo = userRepo; + _db = db; + _otpService = otpService; + _msg = msg; + } + + public async Task> Handle( + RequestPhoneChangeCommand request, CancellationToken ct) + { + var now = DateTimeOffset.UtcNow; + + // Normalize to digits-only for consistent storage and comparison + var normalizedPhone = User.NormalizePhone(request.NewPhone); + + // fetch via repository — check new phone not already taken by another account + var taken = await _userRepo + .IsContactTakenAsync(normalizedPhone, OtpVerificationType.Sms, request.UserId, ct) + .ConfigureAwait(false); + + if (taken) + return _msg.ContactAlreadyTaken(); + + // Serialize CountryCodeId into ExtraData so it survives to confirm-time without client round-trip + var extraData = request.CountryCodeId.HasValue + ? System.Text.Json.JsonSerializer.Serialize(new PhoneChangeExtra(request.CountryCodeId.Value)) + : null; + + var (entity, fail) = await _otpService.PrepareAsync( + normalizedPhone, + OtpVerificationType.Sms, + templateCode: "PHONE_CHANGE_OTP", + channel: NotificationChannel.Sms, + recipientUserId: request.UserId, + extraData, + now, ct).ConfigureAwait(false); + + if (fail is not null) return fail; + + // ICceDbContext as unit of work + await _db.SaveChangesAsync(ct).ConfigureAwait(false); + + return _msg.Ok(new RequestVerificationResponseDto(entity!.Id, entity.ExpiresAt), "OTP_SENT"); + } +} + +internal sealed record PhoneChangeExtra(System.Guid CountryCodeId); diff --git a/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandValidator.cs b/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandValidator.cs new file mode 100644 index 00000000..8e5a892a --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; + +namespace CCE.Application.Identity.Public.Commands.RequestPhoneChange; + +public sealed class RequestPhoneChangeCommandValidator : AbstractValidator +{ + public RequestPhoneChangeCommandValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.NewPhone).NotEmpty().Matches(@"^\d{7,15}$"); + } +} diff --git a/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeRequest.cs b/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeRequest.cs new file mode 100644 index 00000000..82cd917c --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeRequest.cs @@ -0,0 +1,3 @@ +namespace CCE.Application.Identity.Public.Commands.RequestPhoneChange; + +public sealed record RequestPhoneChangeRequest(string NewPhone, System.Guid? CountryCodeId); diff --git a/backend/src/CCE.Application/Messages/MessageFactory.cs b/backend/src/CCE.Application/Messages/MessageFactory.cs index 8a750f2b..26abf7d6 100644 --- a/backend/src/CCE.Application/Messages/MessageFactory.cs +++ b/backend/src/CCE.Application/Messages/MessageFactory.cs @@ -104,12 +104,15 @@ public FieldError Field(string fieldName, string domainKey) // ─── Convenience shortcuts (Verification domain) ─── - public Response OtpNotFound() => NotFound("OTP_NOT_FOUND"); - public Response OtpExpired() => BusinessRule("OTP_EXPIRED"); - public Response OtpInvalidCode() => BusinessRule("OTP_INVALID_CODE"); - public Response OtpMaxAttempts() => BusinessRule("OTP_MAX_ATTEMPTS"); - public Response OtpCooldownActive() => BusinessRule("OTP_COOLDOWN_ACTIVE"); - public Response OtpInvalidated() => BusinessRule("OTP_INVALIDATED"); + public Response OtpNotFound() => NotFound("OTP_NOT_FOUND"); + public Response OtpExpired() => BusinessRule("OTP_EXPIRED"); + public Response OtpInvalidCode() => BusinessRule("OTP_INVALID_CODE"); + public Response OtpMaxAttempts() => BusinessRule("OTP_MAX_ATTEMPTS"); + public Response OtpCooldownActive() => BusinessRule("OTP_COOLDOWN_ACTIVE"); + public Response OtpInvalidated() => BusinessRule("OTP_INVALIDATED"); + public Response ContactAlreadyTaken() => Conflict("CONTACT_ALREADY_TAKEN"); + public Response EmailUpdated() => Ok("EMAIL_UPDATED"); + public Response PhoneUpdated() => Ok("PHONE_UPDATED"); // ─── Convenience shortcuts (Notification domain) ─── diff --git a/backend/src/CCE.Application/Messages/SystemCode.cs b/backend/src/CCE.Application/Messages/SystemCode.cs index b1a5a61b..d9ddfe12 100644 --- a/backend/src/CCE.Application/Messages/SystemCode.cs +++ b/backend/src/CCE.Application/Messages/SystemCode.cs @@ -113,6 +113,7 @@ public static class SystemCode public const string ERR123 = "ERR123"; // OTP max attempts exceeded public const string ERR124 = "ERR124"; // OTP cooldown active public const string ERR125 = "ERR125"; // OTP invalidated + public const string ERR126 = "ERR126"; // Contact already taken // ─── General Errors ─── public const string ERR900 = "ERR900"; // Internal server error @@ -187,6 +188,8 @@ public static class SystemCode // ─── Verification Success ─── public const string CON060 = "CON060"; // OTP sent public const string CON061 = "CON061"; // OTP verified + public const string CON062 = "CON062"; // Email updated + public const string CON063 = "CON063"; // Phone updated // ─── Notification Success ─── public const string CON040 = "CON040"; // Notification created diff --git a/backend/src/CCE.Application/Messages/SystemCodeMap.cs b/backend/src/CCE.Application/Messages/SystemCodeMap.cs index f9cd8a9d..3bd0323e 100644 --- a/backend/src/CCE.Application/Messages/SystemCodeMap.cs +++ b/backend/src/CCE.Application/Messages/SystemCodeMap.cs @@ -93,6 +93,7 @@ public static class SystemCodeMap ["OTP_MAX_ATTEMPTS"] = SystemCode.ERR123, ["OTP_COOLDOWN_ACTIVE"] = SystemCode.ERR124, ["OTP_INVALIDATED"] = SystemCode.ERR125, + ["CONTACT_ALREADY_TAKEN"] = SystemCode.ERR126, // ─── General Errors ─── ["INTERNAL_ERROR"] = SystemCode.ERR900, @@ -159,6 +160,8 @@ public static class SystemCodeMap // ─── Verification Success ─── ["OTP_SENT"] = SystemCode.CON060, ["OTP_VERIFIED"] = SystemCode.CON061, + ["EMAIL_UPDATED"] = SystemCode.CON062, + ["PHONE_UPDATED"] = SystemCode.CON063, // ─── General Success ─── ["ITEMS_LISTED"] = SystemCode.CON100, diff --git a/backend/src/CCE.Application/Verification/Commands/RequestVerification/RequestVerificationCommandHandler.cs b/backend/src/CCE.Application/Verification/Commands/RequestVerification/RequestVerificationCommandHandler.cs index 9549ef20..f3cb313a 100644 --- a/backend/src/CCE.Application/Verification/Commands/RequestVerification/RequestVerificationCommandHandler.cs +++ b/backend/src/CCE.Application/Verification/Commands/RequestVerification/RequestVerificationCommandHandler.cs @@ -54,7 +54,7 @@ public async Task> Handle( } else { - entity = OtpVerification.Create(request.Contact, request.TypeId, codeHash, now); + entity = OtpVerification.Create(request.Contact, request.TypeId, codeHash, now, userId: null); await _otpRepo.AddAsync(entity, ct).ConfigureAwait(false); } diff --git a/backend/src/CCE.Application/Verification/Commands/VerifyOtp/VerifyOtpCommandHandler.cs b/backend/src/CCE.Application/Verification/Commands/VerifyOtp/VerifyOtpCommandHandler.cs index 95e169a6..9f32b933 100644 --- a/backend/src/CCE.Application/Verification/Commands/VerifyOtp/VerifyOtpCommandHandler.cs +++ b/backend/src/CCE.Application/Verification/Commands/VerifyOtp/VerifyOtpCommandHandler.cs @@ -74,7 +74,7 @@ public async Task> Handle( if (userVerification is null) { - userVerification = UserVerification.Create(null, entity.Contact, entity.TypeId); + userVerification = UserVerification.Create(entity.UserId, entity.Contact, entity.TypeId); await _verificationRepo.AddAsync(userVerification, ct).ConfigureAwait(false); } userVerification.MarkVerified(now); @@ -89,6 +89,14 @@ public async Task> Handle( private async Task StampUserConfirmedAsync(OtpVerification entity, CancellationToken ct) { + // Prefer the explicit user link from the OTP record over ambiguous contact lookup + if (entity.UserId.HasValue) + { + await _userRepo.StampConfirmedAsync(entity.UserId.Value, entity.TypeId, ct).ConfigureAwait(false); + return entity.UserId.Value; + } + + // Fallback for anonymous flows (registration) where OTP was not bound to a user var userId = await _userRepo .FindUserIdByContactAsync(entity.Contact, entity.TypeId, ct) .ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Verification/IOtpVerificationRepository.cs b/backend/src/CCE.Application/Verification/IOtpVerificationRepository.cs index 22fe8d37..41966e90 100644 --- a/backend/src/CCE.Application/Verification/IOtpVerificationRepository.cs +++ b/backend/src/CCE.Application/Verification/IOtpVerificationRepository.cs @@ -8,4 +8,8 @@ public interface IOtpVerificationRepository : IRepository Task FindActiveAsync( string contact, OtpVerificationType typeId, DateTimeOffset now, CancellationToken ct = default); + + Task FindActiveAsync( + string contact, OtpVerificationType typeId, + DateTimeOffset now, Guid? userId, CancellationToken ct = default); } diff --git a/backend/src/CCE.Domain/Identity/User.cs b/backend/src/CCE.Domain/Identity/User.cs index c522b467..c20ea5e7 100644 --- a/backend/src/CCE.Domain/Identity/User.cs +++ b/backend/src/CCE.Domain/Identity/User.cs @@ -232,6 +232,28 @@ public void SetAvatarUrl(string? url) AvatarUrl = url; } + public void UpdateEmail(string newEmail) + { + if (string.IsNullOrWhiteSpace(newEmail)) throw new DomainException("Email is required."); + var trimmed = newEmail.Trim(); + Email = trimmed; + NormalizedEmail = trimmed.ToUpperInvariant(); + UserName = trimmed; + NormalizedUserName = trimmed.ToUpperInvariant(); + EmailConfirmed = true; + } + + public void UpdatePhoneNumber(string newPhone, System.Guid? countryCodeId) + { + if (string.IsNullOrWhiteSpace(newPhone)) throw new DomainException("Phone number is required."); + PhoneNumber = NormalizePhone(newPhone); + PhoneNumberConfirmed = true; + CountryCodeId = countryCodeId; + } + + public static string NormalizePhone(string phone) + => new string(System.Linq.Enumerable.Where(phone, char.IsDigit).ToArray()); + public void ChangeStatus(UserStatus newStatus) => Status = newStatus; public void Activate() => Status = UserStatus.Active; diff --git a/backend/src/CCE.Domain/Verification/OtpVerification.cs b/backend/src/CCE.Domain/Verification/OtpVerification.cs index 73a0c1fe..74880442 100644 --- a/backend/src/CCE.Domain/Verification/OtpVerification.cs +++ b/backend/src/CCE.Domain/Verification/OtpVerification.cs @@ -18,11 +18,19 @@ private OtpVerification(Guid id) : base(id) { } public bool IsVerified { get; private set; } public bool IsInvalidated { get; private set; } + /// Optional user identifier. Null for anonymous flows (registration), set for authenticated contact-change flows. + public Guid? UserId { get; private set; } + + /// Optional JSON payload for context that varies by OTP flow (e.g. CountryCodeId for phone change). + public string? ExtraData { get; private set; } + public static OtpVerification Create( string contact, OtpVerificationType typeId, string codeHash, - DateTimeOffset now) + DateTimeOffset now, + string? extraData = null, + Guid? userId = null) { return new OtpVerification(Guid.NewGuid()) { @@ -35,6 +43,8 @@ public static OtpVerification Create( AttemptCount = 0, IsVerified = false, IsInvalidated = false, + UserId = userId, + ExtraData = extraData, }; } @@ -45,13 +55,17 @@ public bool CanResend(DateTimeOffset now) public bool HasExceededMaxAttempts() => AttemptCount >= 5; - public void Refresh(string newCodeHash, DateTimeOffset now) + public void Refresh(string newCodeHash, DateTimeOffset now, string? extraData = null, Guid? userId = null) { CodeHash = newCodeHash; ExpiresAt = now.AddMinutes(5); LastSentAt = now; AttemptCount = 0; IsInvalidated = false; + if (extraData is not null) + ExtraData = extraData; + if (userId is not null) + UserId = userId; } public void IncrementAttempt() => AttemptCount++; diff --git a/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/UserConfiguration.cs b/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/UserConfiguration.cs index 4f23d074..7115844e 100644 --- a/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/UserConfiguration.cs +++ b/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/UserConfiguration.cs @@ -20,6 +20,13 @@ public void Configure(EntityTypeBuilder builder) builder.HasIndex(u => u.CountryId).HasDatabaseName("ix_users_country_id"); builder.HasIndex(u => u.CountryCodeId).HasDatabaseName("ix_users_country_code_id"); + // Enforce unique email at the database level to prevent duplicate accounts. + // Filtered index: only non-null values (Identity allows null emails historically). + builder.HasIndex(u => u.NormalizedEmail) + .HasDatabaseName("ix_users_normalized_email_unique") + .IsUnique() + .HasFilter("[normalized_email] IS NOT NULL"); + // Sub-11: filtered unique index on EntraIdObjectId. Only enforces uniqueness on // non-null values so existing rows pre-cutover (NULL) don't conflict, and so that // the lazy-resolver's idempotent linkage stays safe under concurrent first-sign-ins. diff --git a/backend/src/CCE.Infrastructure/Persistence/Configurations/Verification/OtpVerificationConfiguration.cs b/backend/src/CCE.Infrastructure/Persistence/Configurations/Verification/OtpVerificationConfiguration.cs index 4163716e..bdf39965 100644 --- a/backend/src/CCE.Infrastructure/Persistence/Configurations/Verification/OtpVerificationConfiguration.cs +++ b/backend/src/CCE.Infrastructure/Persistence/Configurations/Verification/OtpVerificationConfiguration.cs @@ -14,6 +14,10 @@ public void Configure(EntityTypeBuilder builder) builder.Property(e => e.Contact).HasMaxLength(256).IsRequired(); builder.Property(e => e.TypeId).IsRequired(); builder.Property(e => e.CodeHash).HasMaxLength(512).IsRequired(); + builder.Property(e => e.ExtraData).HasColumnType("nvarchar(max)").IsRequired(false); + builder.Property(e => e.UserId).IsRequired(false); builder.HasIndex(e => new { e.Contact, e.TypeId }); + builder.HasIndex(e => new { e.UserId, e.Contact, e.TypeId }) + .HasDatabaseName("ix_otp_verifications_user_contact_type"); } } diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/20260525154051_AddOtpVerificationExtraData.Designer.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260525154051_AddOtpVerificationExtraData.Designer.cs new file mode 100644 index 00000000..4f29e325 --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260525154051_AddOtpVerificationExtraData.Designer.cs @@ -0,0 +1,3848 @@ +// +using System; +using CCE.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CCE.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(CceDbContext))] + [Migration("20260525154051_AddOtpVerificationExtraData")] + partial class AddOtpVerificationExtraData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CCE.Domain.Audit.AuditEvent", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("action"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("actor"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier") + .HasColumnName("correlation_id"); + + b.Property("Diff") + .HasColumnType("nvarchar(max)") + .HasColumnName("diff"); + + b.Property("OccurredOn") + .HasColumnType("datetimeoffset") + .HasColumnName("occurred_on"); + + b.Property("Resource") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("resource"); + + b.HasKey("Id") + .HasName("pk_audit_events"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_events_correlation_id"); + + b.HasIndex("Actor", "OccurredOn") + .HasDatabaseName("ix_audit_events_actor_occurred_on"); + + b.ToTable("audit_events", null, t => + { + t.HasTrigger("trg_audit_events_no_update_delete"); + }); + + b.HasAnnotation("SqlServer:UseSqlOutputClause", false); + }); + + modelBuilder.Entity("CCE.Domain.Community.Post", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AnsweredReplyId") + .HasColumnType("uniqueidentifier") + .HasColumnName("answered_reply_id"); + + b.Property("AuthorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("author_id"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)") + .HasColumnName("content"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsAnswerable") + .HasColumnType("bit") + .HasColumnName("is_answerable"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale"); + + b.Property("TopicId") + .HasColumnType("uniqueidentifier") + .HasColumnName("topic_id"); + + b.HasKey("Id") + .HasName("pk_posts"); + + b.HasIndex("TopicId") + .HasDatabaseName("ix_post_topic_id"); + + b.HasIndex("AuthorId", "CreatedOn") + .HasDatabaseName("ix_post_author_created"); + + b.ToTable("posts", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Community.PostFollow", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("FollowedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("followed_on"); + + b.Property("PostId") + .HasColumnType("uniqueidentifier") + .HasColumnName("post_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_post_follows"); + + b.HasIndex("PostId", "UserId") + .IsUnique() + .HasDatabaseName("ux_post_follow_post_user"); + + b.ToTable("post_follows", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Community.PostRating", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("PostId") + .HasColumnType("uniqueidentifier") + .HasColumnName("post_id"); + + b.Property("RatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("rated_on"); + + b.Property("Stars") + .HasColumnType("int") + .HasColumnName("stars"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_post_ratings"); + + b.HasIndex("PostId", "UserId") + .IsUnique() + .HasDatabaseName("ux_post_rating_post_user"); + + b.ToTable("post_ratings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Community.PostReply", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AuthorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("author_id"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)") + .HasColumnName("content"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsByExpert") + .HasColumnType("bit") + .HasColumnName("is_by_expert"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale"); + + b.Property("ParentReplyId") + .HasColumnType("uniqueidentifier") + .HasColumnName("parent_reply_id"); + + b.Property("PostId") + .HasColumnType("uniqueidentifier") + .HasColumnName("post_id"); + + b.HasKey("Id") + .HasName("pk_post_replies"); + + b.HasIndex("ParentReplyId") + .HasDatabaseName("ix_post_reply_parent_id"); + + b.HasIndex("PostId") + .HasDatabaseName("ix_post_reply_post_id"); + + b.ToTable("post_replies", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Community.Topic", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IconUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("icon_url"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("ParentId") + .HasColumnType("uniqueidentifier") + .HasColumnName("parent_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_topics"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ux_topic_slug_active") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("topics", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Community.TopicFollow", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("FollowedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("followed_on"); + + b.Property("TopicId") + .HasColumnType("uniqueidentifier") + .HasColumnName("topic_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_topic_follows"); + + b.HasIndex("TopicId", "UserId") + .IsUnique() + .HasDatabaseName("ux_topic_follow_topic_user"); + + b.ToTable("topic_follows", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Community.UserFollow", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("FollowedId") + .HasColumnType("uniqueidentifier") + .HasColumnName("followed_id"); + + b.Property("FollowedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("followed_on"); + + b.Property("FollowerId") + .HasColumnType("uniqueidentifier") + .HasColumnName("follower_id"); + + b.HasKey("Id") + .HasName("pk_user_follows"); + + b.HasIndex("FollowerId", "FollowedId") + .IsUnique() + .HasDatabaseName("ux_user_follow_follower_followed"); + + b.ToTable("user_follows", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.AssetFile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("MimeType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("mime_type"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("original_file_name"); + + b.Property("ScannedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("scanned_on"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size_bytes"); + + b.Property("UploadedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("uploaded_by_id"); + + b.Property("UploadedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("uploaded_on"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("url"); + + b.Property("VirusScanStatus") + .HasColumnType("int") + .HasColumnName("virus_scan_status"); + + b.HasKey("Id") + .HasName("pk_asset_files"); + + b.HasIndex("VirusScanStatus") + .HasDatabaseName("ix_asset_file_scan_status"); + + b.ToTable("asset_files", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.Event", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("EndsOn") + .HasColumnType("datetimeoffset") + .HasColumnName("ends_on"); + + b.Property("FeaturedImageUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("featured_image_url"); + + b.Property("ICalUid") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("i_cal_uid"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LocationAr") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("location_ar"); + + b.Property("LocationEn") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("location_en"); + + b.Property("OnlineMeetingUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("online_meeting_url"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("StartsOn") + .HasColumnType("datetimeoffset") + .HasColumnName("starts_on"); + + b.Property("TitleAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_en"); + + b.HasKey("Id") + .HasName("pk_events"); + + b.HasIndex("ICalUid") + .IsUnique() + .HasDatabaseName("ux_event_ical_uid"); + + b.HasIndex("StartsOn") + .HasDatabaseName("ix_event_starts_on"); + + b.ToTable("events", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.HomepageSection", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ContentAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_ar"); + + b.Property("ContentEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("SectionType") + .HasColumnType("int") + .HasColumnName("section_type"); + + b.HasKey("Id") + .HasName("pk_homepage_sections"); + + b.HasIndex("IsActive", "OrderIndex") + .HasDatabaseName("ix_homepage_section_active_order"); + + b.ToTable("homepage_sections", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.News", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AuthorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("author_id"); + + b.Property("ContentAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_ar"); + + b.Property("ContentEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("FeaturedImageUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("featured_image_url"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("IsFeatured") + .HasColumnType("bit") + .HasColumnName("is_featured"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("PublishedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("published_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("slug"); + + b.Property("TitleAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_en"); + + b.HasKey("Id") + .HasName("pk_news"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_news_published_on"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ux_news_slug_active") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("news", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.NewsletterSubscription", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ConfirmationToken") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("confirmation_token"); + + b.Property("ConfirmedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("confirmed_on"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("nvarchar(320)") + .HasColumnName("email"); + + b.Property("IsConfirmed") + .HasColumnType("bit") + .HasColumnName("is_confirmed"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LocalePreference") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale_preference"); + + b.Property("UnsubscribedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("unsubscribed_on"); + + b.HasKey("Id") + .HasName("pk_newsletter_subscriptions"); + + b.HasIndex("ConfirmationToken") + .HasDatabaseName("ix_newsletter_token"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ux_newsletter_email"); + + b.ToTable("newsletter_subscriptions", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.Page", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ContentAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_ar"); + + b.Property("ContentEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("PageType") + .HasColumnType("int") + .HasColumnName("page_type"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("slug"); + + b.Property("TitleAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_en"); + + b.HasKey("Id") + .HasName("pk_pages"); + + b.HasIndex("PageType", "Slug") + .IsUnique() + .HasDatabaseName("ux_page_type_slug_active") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("pages", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.Resource", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssetFileId") + .HasColumnType("uniqueidentifier") + .HasColumnName("asset_file_id"); + + b.Property("CategoryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("category_id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("PublishedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("published_on"); + + b.Property("ResourceType") + .HasColumnType("int") + .HasColumnName("resource_type"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("TitleAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_en"); + + b.Property("UploadedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("uploaded_by_id"); + + b.Property("ViewCount") + .HasColumnType("bigint") + .HasColumnName("view_count"); + + b.HasKey("Id") + .HasName("pk_resources"); + + b.HasIndex("AssetFileId") + .HasDatabaseName("ix_resource_asset_file_id"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_resource_country_id"); + + b.HasIndex("CategoryId", "PublishedOn") + .HasDatabaseName("ix_resource_category_published"); + + b.ToTable("resources", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.ResourceCategory", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("ParentId") + .HasColumnType("uniqueidentifier") + .HasColumnName("parent_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_resource_categories"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_resource_category_parent_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ux_resource_category_slug"); + + b.ToTable("resource_categories", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Country.Country", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("FlagUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("flag_url"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("IsoAlpha2") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("iso_alpha2"); + + b.Property("IsoAlpha3") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)") + .HasColumnName("iso_alpha3"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LatestKapsarcSnapshotId") + .HasColumnType("uniqueidentifier") + .HasColumnName("latest_kapsarc_snapshot_id"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("RegionAr") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("region_ar"); + + b.Property("RegionEn") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("region_en"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasIndex("IsoAlpha2") + .HasDatabaseName("ix_country_iso_alpha2"); + + b.HasIndex("IsoAlpha3") + .IsUnique() + .HasDatabaseName("ux_country_iso_alpha3_active") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("countries", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Country.CountryKapsarcSnapshot", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Classification") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("classification"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("PerformanceScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)") + .HasColumnName("performance_score"); + + b.Property("SnapshotTakenOn") + .HasColumnType("datetimeoffset") + .HasColumnName("snapshot_taken_on"); + + b.Property("SourceVersion") + .HasMaxLength(32) + .HasColumnType("nvarchar(32)") + .HasColumnName("source_version"); + + b.Property("TotalIndex") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)") + .HasColumnName("total_index"); + + b.HasKey("Id") + .HasName("pk_country_kapsarc_snapshots"); + + b.HasIndex("CountryId", "SnapshotTakenOn") + .HasDatabaseName("ix_kapsarc_snapshot_country_taken"); + + b.ToTable("country_kapsarc_snapshots", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Country.CountryProfile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ContactInfoAr") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("contact_info_ar"); + + b.Property("ContactInfoEn") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("contact_info_en"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("KeyInitiativesAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("key_initiatives_ar"); + + b.Property("KeyInitiativesEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("key_initiatives_en"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.HasKey("Id") + .HasName("pk_country_profiles"); + + b.HasIndex("CountryId") + .IsUnique() + .HasDatabaseName("ux_country_profile_country_id"); + + b.ToTable("country_profiles", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Country.CountryResourceRequest", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AdminNotesAr") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("admin_notes_ar"); + + b.Property("AdminNotesEn") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("admin_notes_en"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("ProcessedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("processed_by_id"); + + b.Property("ProcessedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("processed_on"); + + b.Property("ProposedAssetFileId") + .HasColumnType("uniqueidentifier") + .HasColumnName("proposed_asset_file_id"); + + b.Property("ProposedDescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("proposed_description_ar"); + + b.Property("ProposedDescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("proposed_description_en"); + + b.Property("ProposedResourceType") + .HasColumnType("int") + .HasColumnName("proposed_resource_type"); + + b.Property("ProposedTitleAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("proposed_title_ar"); + + b.Property("ProposedTitleEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("proposed_title_en"); + + b.Property("RequestedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("requested_by_id"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.HasKey("Id") + .HasName("pk_country_resource_requests"); + + b.HasIndex("CountryId", "Status") + .HasDatabaseName("ix_country_request_country_status"); + + b.ToTable("country_resource_requests", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertProfile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AcademicTitleAr") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("academic_title_ar"); + + b.Property("AcademicTitleEn") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("academic_title_en"); + + b.Property("ApprovedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("approved_by_id"); + + b.Property("ApprovedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("approved_on"); + + b.Property("BioAr") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("bio_ar"); + + b.Property("BioEn") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("bio_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.PrimitiveCollection("ExpertiseTags") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("expertise_tags"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_expert_profiles"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("ux_expert_profile_active_user") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("expert_profiles", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRegistrationRequest", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("ProcessedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("processed_by_id"); + + b.Property("ProcessedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("processed_on"); + + b.Property("RejectionReasonAr") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("rejection_reason_ar"); + + b.Property("RejectionReasonEn") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("rejection_reason_en"); + + b.Property("RequestedBioAr") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("requested_bio_ar"); + + b.Property("RequestedBioEn") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("requested_bio_en"); + + b.Property("RequestedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("requested_by_id"); + + b.PrimitiveCollection("RequestedTags") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("requested_tags"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.HasKey("Id") + .HasName("pk_expert_registration_requests"); + + b.HasIndex("RequestedById") + .HasDatabaseName("ix_expert_request_requested_by"); + + b.HasIndex("Status") + .HasDatabaseName("ix_expert_request_status"); + + b.ToTable("expert_registration_requests", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRequestAttachment", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssetFileId") + .HasColumnType("uniqueidentifier") + .HasColumnName("asset_file_id"); + + b.Property("AttachmentType") + .HasColumnType("int") + .HasColumnName("attachment_type"); + + b.Property("ExpertRequestId") + .HasColumnType("uniqueidentifier") + .HasColumnName("expert_request_id"); + + b.Property("UploadedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("uploaded_at"); + + b.HasKey("Id") + .HasName("pk_expert_request_attachments"); + + b.HasIndex("ExpertRequestId") + .HasDatabaseName("ix_expert_request_attachments_expert_request_id"); + + b.ToTable("expert_request_attachments", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset") + .HasColumnName("created_at_utc"); + + b.Property("CreatedByIp") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("created_by_ip"); + + b.Property("ExpiresAtUtc") + .HasColumnType("datetimeoffset") + .HasColumnName("expires_at_utc"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("replaced_by_token_hash"); + + b.Property("RevokedAtUtc") + .HasColumnType("datetimeoffset") + .HasColumnName("revoked_at_utc"); + + b.Property("RevokedByIp") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("revoked_by_ip"); + + b.Property("TokenFamilyId") + .HasColumnType("uniqueidentifier") + .HasColumnName("token_family_id"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("token_hash"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("user_agent"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("TokenFamilyId") + .HasDatabaseName("ix_refresh_tokens_token_family_id"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ux_refresh_tokens_token_hash"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_refresh_tokens_user_id"); + + b.ToTable("refresh_tokens", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("normalized_name"); + + b.HasKey("Id") + .HasName("pk_asp_net_roles"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[normalized_name] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.StateRepresentativeAssignment", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssignedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("assigned_by_id"); + + b.Property("AssignedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("assigned_on"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RevokedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("revoked_by_id"); + + b.Property("RevokedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("revoked_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_state_representative_assignments"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_state_rep_country_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_state_rep_user_id"); + + b.HasIndex("UserId", "CountryId") + .IsUnique() + .HasDatabaseName("ux_state_rep_active_user_country") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("state_representative_assignments", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AccessFailedCount") + .HasColumnType("int") + .HasColumnName("access_failed_count"); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("avatar_url"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)") + .HasColumnName("concurrency_stamp"); + + b.Property("CountryCodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_code_id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("bit") + .HasColumnName("email_confirmed"); + + b.Property("EntraIdObjectId") + .HasColumnType("uniqueidentifier") + .HasColumnName("entra_id_object_id"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("first_name"); + + b.PrimitiveCollection("Interests") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("interests"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("job_title"); + + b.Property("KnowledgeLevel") + .HasColumnType("int") + .HasColumnName("knowledge_level"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("last_name"); + + b.Property("LocalePreference") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale_preference"); + + b.Property("LockoutEnabled") + .HasColumnType("bit") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("normalized_user_name"); + + b.Property("OrganizationName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("organization_name"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)") + .HasColumnName("password_hash"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit") + .HasColumnName("phone_number_confirmed"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)") + .HasColumnName("security_stamp"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit") + .HasColumnName("two_factor_enabled"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("pk_asp_net_users"); + + b.HasIndex("CountryCodeId") + .HasDatabaseName("ix_users_country_code_id"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_users_country_id"); + + b.HasIndex("EntraIdObjectId") + .IsUnique() + .HasDatabaseName("ix_asp_net_users_entra_id_object_id") + .HasFilter("[entra_id_object_id] IS NOT NULL"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[normalized_user_name] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityScenario", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CityType") + .HasColumnType("int") + .HasColumnName("city_type"); + + b.Property("ConfigurationJson") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("configuration_json"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("TargetYear") + .HasColumnType("int") + .HasColumnName("target_year"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_city_scenarios"); + + b.HasIndex("UserId", "LastModifiedOn") + .HasDatabaseName("ix_city_scenario_user_modified"); + + b.ToTable("city_scenarios", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityScenarioResult", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ComputedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("computed_at"); + + b.Property("ComputedCarbonNeutralityYear") + .HasColumnType("int") + .HasColumnName("computed_carbon_neutrality_year"); + + b.Property("ComputedTotalCostUsd") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("computed_total_cost_usd"); + + b.Property("EngineVersion") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("engine_version"); + + b.Property("ScenarioId") + .HasColumnType("uniqueidentifier") + .HasColumnName("scenario_id"); + + b.HasKey("Id") + .HasName("pk_city_scenario_results"); + + b.HasIndex("ScenarioId", "ComputedAt") + .HasDatabaseName("ix_city_result_scenario_at"); + + b.ToTable("city_scenario_results", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityTechnology", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CarbonImpactKgPerYear") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("carbon_impact_kg_per_year"); + + b.Property("CategoryAr") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("category_ar"); + + b.Property("CategoryEn") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("category_en"); + + b.Property("CostUsd") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("cost_usd"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IconUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("icon_url"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.HasKey("Id") + .HasName("pk_city_technologies"); + + b.HasIndex("IsActive") + .HasDatabaseName("ix_city_tech_is_active"); + + b.ToTable("city_technologies", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMap", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_knowledge_maps"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ux_knowledge_map_slug_active") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("knowledge_maps", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMapAssociation", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssociatedId") + .HasColumnType("uniqueidentifier") + .HasColumnName("associated_id"); + + b.Property("AssociatedType") + .HasColumnType("int") + .HasColumnName("associated_type"); + + b.Property("NodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("node_id"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_knowledge_map_associations"); + + b.HasIndex("NodeId", "AssociatedType", "AssociatedId") + .IsUnique() + .HasDatabaseName("ux_km_assoc_node_type_id"); + + b.ToTable("knowledge_map_associations", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMapEdge", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("FromNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("from_node_id"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("map_id"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("RelationshipType") + .HasColumnType("int") + .HasColumnName("relationship_type"); + + b.Property("ToNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("to_node_id"); + + b.HasKey("Id") + .HasName("pk_knowledge_map_edges"); + + b.HasIndex("FromNodeId") + .HasDatabaseName("ix_km_edge_from_node"); + + b.HasIndex("ToNodeId") + .HasDatabaseName("ix_km_edge_to_node"); + + b.HasIndex("MapId", "FromNodeId", "ToNodeId", "RelationshipType") + .IsUnique() + .HasDatabaseName("ux_km_edge_map_from_to_relation"); + + b.ToTable("knowledge_map_edges", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMapNode", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("DescriptionAr") + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IconUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("icon_url"); + + b.Property("LayoutX") + .HasColumnType("float") + .HasColumnName("layout_x"); + + b.Property("LayoutY") + .HasColumnType("float") + .HasColumnName("layout_y"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("map_id"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("NodeType") + .HasColumnType("int") + .HasColumnName("node_type"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_knowledge_map_nodes"); + + b.HasIndex("MapId", "OrderIndex") + .HasDatabaseName("ix_km_node_map_order"); + + b.ToTable("knowledge_map_nodes", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Lookups.CountryCode", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DialCode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("nvarchar(16)") + .HasColumnName("dial_code"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.HasKey("Id") + .HasName("pk_country_codes"); + + b.HasIndex("DialCode") + .HasDatabaseName("ix_country_code_dial_code"); + + b.ToTable("country_codes", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Media.MediaFile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AltTextAr") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("alt_text_ar"); + + b.Property("AltTextEn") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("alt_text_en"); + + b.Property("DescriptionAr") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_en"); + + b.Property("MimeType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("mime_type"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("original_file_name"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size_bytes"); + + b.Property("StorageKey") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("storage_key"); + + b.Property("TitleAr") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("title_en"); + + b.Property("UploadedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("uploaded_by_id"); + + b.Property("UploadedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("uploaded_on"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("pk_media_files"); + + b.ToTable("media_files", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.NotificationLog", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AttemptCount") + .HasColumnType("int") + .HasColumnName("attempt_count"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("correlation_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FailedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("failed_on"); + + b.Property("PayloadJson") + .HasColumnType("nvarchar(max)") + .HasColumnName("payload_json"); + + b.Property("ProviderMessageId") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("provider_message_id"); + + b.Property("RecipientUserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("recipient_user_id"); + + b.Property("SentOn") + .HasColumnType("datetimeoffset") + .HasColumnName("sent_on"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("TemplateCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("template_code"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier") + .HasColumnName("template_id"); + + b.HasKey("Id") + .HasName("pk_notification_logs"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_notification_log_correlation_id"); + + b.HasIndex("TemplateCode", "Channel") + .HasDatabaseName("ix_notification_log_template_channel"); + + b.HasIndex("RecipientUserId", "Status", "CreatedOn") + .HasDatabaseName("ix_notification_log_recipient_status_created"); + + b.ToTable("notification_logs", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.NotificationTemplate", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("BodyAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("body_ar"); + + b.Property("BodyEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("body_en"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("code"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("SubjectAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("subject_ar"); + + b.Property("SubjectEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("subject_en"); + + b.Property("VariableSchemaJson") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("variable_schema_json"); + + b.HasKey("Id") + .HasName("pk_notification_templates"); + + b.HasIndex("Code", "Channel") + .IsUnique() + .HasDatabaseName("ux_notification_template_code_channel"); + + b.ToTable("notification_templates", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.UserNotification", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("ReadOn") + .HasColumnType("datetimeoffset") + .HasColumnName("read_on"); + + b.Property("RenderedBody") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("rendered_body"); + + b.Property("RenderedLocale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("rendered_locale"); + + b.Property("RenderedSubjectAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("rendered_subject_ar"); + + b.Property("RenderedSubjectEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("rendered_subject_en"); + + b.Property("SentOn") + .HasColumnType("datetimeoffset") + .HasColumnName("sent_on"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier") + .HasColumnName("template_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_notifications"); + + b.HasIndex("UserId", "Status") + .HasDatabaseName("ix_user_notification_user_status"); + + b.ToTable("user_notifications", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.UserNotificationSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("EventCode") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("event_code"); + + b.Property("IsEnabled") + .HasColumnType("bit") + .HasColumnName("is_enabled"); + + b.Property("UpdatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("updated_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_notification_settings"); + + b.HasIndex("UserId", "Channel", "EventCode") + .IsUnique() + .HasDatabaseName("ux_user_notification_settings_user_channel_event") + .HasFilter("[event_code] IS NOT NULL"); + + b.ToTable("user_notification_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.AboutSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("HowToUseVideoUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("how_to_use_video_url"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.HasKey("Id") + .HasName("pk_about_settings"); + + b.ToTable("about_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.GlossaryEntry", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AboutSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("about_settings_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_glossary_entries"); + + b.HasIndex("AboutSettingsId") + .HasDatabaseName("ix_glossary_entries_about_settings_id"); + + b.ToTable("glossary_entries", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageCountry", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("HomepageSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("homepage_settings_id"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_homepage_countries"); + + b.HasIndex("HomepageSettingsId", "CountryId") + .IsUnique() + .HasDatabaseName("ix_homepage_country_settings_country"); + + b.ToTable("homepage_countries", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CceConceptsAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("cce_concepts_ar"); + + b.Property("CceConceptsEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("cce_concepts_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("VideoUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("video_url"); + + b.HasKey("Id") + .HasName("pk_homepage_settings"); + + b.ToTable("homepage_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.KnowledgePartner", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AboutSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("about_settings_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LogoUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("logo_url"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("WebsiteUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("website_url"); + + b.HasKey("Id") + .HasName("pk_knowledge_partners"); + + b.HasIndex("AboutSettingsId") + .HasDatabaseName("ix_knowledge_partners_about_settings_id"); + + b.ToTable("knowledge_partners", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PoliciesSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.HasKey("Id") + .HasName("pk_policies_settings"); + + b.ToTable("policies_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PolicySection", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("PoliciesSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("policies_settings_id"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_policy_sections"); + + b.HasIndex("PoliciesSettingsId") + .HasDatabaseName("ix_policy_sections_policies_settings_id"); + + b.ToTable("policy_sections", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Surveys.SearchQueryLog", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale"); + + b.Property("QueryText") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("query_text"); + + b.Property("ResponseTimeMs") + .HasColumnType("int") + .HasColumnName("response_time_ms"); + + b.Property("ResultsCount") + .HasColumnType("int") + .HasColumnName("results_count"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_search_query_logs"); + + b.HasIndex("SubmittedOn") + .HasDatabaseName("ix_search_query_log_submitted_on"); + + b.ToTable("search_query_logs", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Surveys.ServiceRating", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CommentAr") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("comment_ar"); + + b.Property("CommentEn") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("comment_en"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale"); + + b.Property("Page") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("page"); + + b.Property("Rating") + .HasColumnType("int") + .HasColumnName("rating"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_service_ratings"); + + b.HasIndex("SubmittedOn") + .HasDatabaseName("ix_service_rating_submitted_on"); + + b.ToTable("service_ratings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Verification.OtpVerification", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AttemptCount") + .HasColumnType("int") + .HasColumnName("attempt_count"); + + b.Property("CodeHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("code_hash"); + + b.Property("Contact") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("contact"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset") + .HasColumnName("expires_at"); + + b.Property("ExtraData") + .HasColumnType("nvarchar(max)") + .HasColumnName("extra_data"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("IsInvalidated") + .HasColumnType("bit") + .HasColumnName("is_invalidated"); + + b.Property("IsVerified") + .HasColumnType("bit") + .HasColumnName("is_verified"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LastSentAt") + .HasColumnType("datetimeoffset") + .HasColumnName("last_sent_at"); + + b.Property("TypeId") + .HasColumnType("int") + .HasColumnName("type_id"); + + b.HasKey("Id") + .HasName("pk_otp_verifications"); + + b.HasIndex("Contact", "TypeId") + .HasDatabaseName("ix_otp_verifications_contact_type_id"); + + b.ToTable("otp_verifications", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Verification.UserVerification", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Contact") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("contact"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("IsVerified") + .HasColumnType("bit") + .HasColumnName("is_verified"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("TypeId") + .HasColumnType("int") + .HasColumnName("type_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("VerifiedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_user_verifications"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_verifications_user_id"); + + b.HasIndex("Contact", "TypeId") + .IsUnique() + .HasDatabaseName("ix_user_verifications_contact_type_id"); + + b.ToTable("user_verifications", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_asp_net_role_claims"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_asp_net_role_claims_role_id"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_asp_net_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_asp_net_user_claims_user_id"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_asp_net_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_asp_net_user_logins_user_id"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier") + .HasColumnName("role_id"); + + b.HasKey("UserId", "RoleId") + .HasName("pk_asp_net_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_asp_net_user_roles_role_id"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("nvarchar(450)") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("nvarchar(max)") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_asp_net_user_tokens"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRequestAttachment", b => + { + b.HasOne("CCE.Domain.Identity.ExpertRegistrationRequest", null) + .WithMany("Attachments") + .HasForeignKey("ExpertRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_expert_request_attachments_expert_registration_requests_expert_request_id"); + }); + + modelBuilder.Entity("CCE.Domain.Identity.RefreshToken", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_asp_net_users_user_id"); + }); + + modelBuilder.Entity("CCE.Domain.Lookups.CountryCode", b => + { + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Name", b1 => + { + b1.Property("CountryCodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b1.HasKey("CountryCodeId"); + + b1.ToTable("country_codes"); + + b1.WithOwner() + .HasForeignKey("CountryCodeId") + .HasConstraintName("fk_country_codes_country_codes_id"); + }); + + b.Navigation("Name") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.AboutSettings", b => + { + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Description", b1 => + { + b1.Property("AboutSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_en"); + + b1.HasKey("AboutSettingsId"); + + b1.ToTable("about_settings"); + + b1.WithOwner() + .HasForeignKey("AboutSettingsId") + .HasConstraintName("fk_about_settings_about_settings_id"); + }); + + b.Navigation("Description") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.GlossaryEntry", b => + { + b.HasOne("CCE.Domain.PlatformSettings.AboutSettings", null) + .WithMany("GlossaryEntries") + .HasForeignKey("AboutSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_glossary_entries_about_settings_about_settings_id"); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Definition", b1 => + { + b1.Property("GlossaryEntryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("definition_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("definition_en"); + + b1.HasKey("GlossaryEntryId"); + + b1.ToTable("glossary_entries"); + + b1.WithOwner() + .HasForeignKey("GlossaryEntryId") + .HasConstraintName("fk_glossary_entries_glossary_entries_id"); + }); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Term", b1 => + { + b1.Property("GlossaryEntryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("term_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("term_en"); + + b1.HasKey("GlossaryEntryId"); + + b1.ToTable("glossary_entries"); + + b1.WithOwner() + .HasForeignKey("GlossaryEntryId") + .HasConstraintName("fk_glossary_entries_glossary_entries_id"); + }); + + b.Navigation("Definition") + .IsRequired(); + + b.Navigation("Term") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageCountry", b => + { + b.HasOne("CCE.Domain.PlatformSettings.HomepageSettings", null) + .WithMany("Countries") + .HasForeignKey("HomepageSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_homepage_countries_homepage_settings_homepage_settings_id"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageSettings", b => + { + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Objective", b1 => + { + b1.Property("HomepageSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("objective_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("objective_en"); + + b1.HasKey("HomepageSettingsId"); + + b1.ToTable("homepage_settings"); + + b1.WithOwner() + .HasForeignKey("HomepageSettingsId") + .HasConstraintName("fk_homepage_settings_homepage_settings_id"); + }); + + b.Navigation("Objective") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.KnowledgePartner", b => + { + b.HasOne("CCE.Domain.PlatformSettings.AboutSettings", null) + .WithMany("KnowledgePartners") + .HasForeignKey("AboutSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_knowledge_partners_about_settings_about_settings_id"); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Description", b1 => + { + b1.Property("KnowledgePartnerId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_en"); + + b1.HasKey("KnowledgePartnerId"); + + b1.ToTable("knowledge_partners"); + + b1.WithOwner() + .HasForeignKey("KnowledgePartnerId") + .HasConstraintName("fk_knowledge_partners_knowledge_partners_id"); + }); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Name", b1 => + { + b1.Property("KnowledgePartnerId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("name_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("name_en"); + + b1.HasKey("KnowledgePartnerId"); + + b1.ToTable("knowledge_partners"); + + b1.WithOwner() + .HasForeignKey("KnowledgePartnerId") + .HasConstraintName("fk_knowledge_partners_knowledge_partners_id"); + }); + + b.Navigation("Description"); + + b.Navigation("Name") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PolicySection", b => + { + b.HasOne("CCE.Domain.PlatformSettings.PoliciesSettings", null) + .WithMany("Sections") + .HasForeignKey("PoliciesSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_policy_sections_policies_settings_policies_settings_id"); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Content", b1 => + { + b1.Property("PolicySectionId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_ar"); + + b1.Property("En") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_en"); + + b1.HasKey("PolicySectionId"); + + b1.ToTable("policy_sections"); + + b1.WithOwner() + .HasForeignKey("PolicySectionId") + .HasConstraintName("fk_policy_sections_policy_sections_id"); + }); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Title", b1 => + { + b1.Property("PolicySectionId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("title_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("title_en"); + + b1.HasKey("PolicySectionId"); + + b1.ToTable("policy_sections"); + + b1.WithOwner() + .HasForeignKey("PolicySectionId") + .HasConstraintName("fk_policy_sections_policy_sections_id"); + }); + + b.Navigation("Content") + .IsRequired(); + + b.Navigation("Title") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.Verification.UserVerification", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_user_verifications_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("CCE.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_role_claims_asp_net_roles_role_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_claims_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_logins_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("CCE.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_roles_asp_net_roles_role_id"); + + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_roles_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_tokens_asp_net_users_user_id"); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRegistrationRequest", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.AboutSettings", b => + { + b.Navigation("GlossaryEntries"); + + b.Navigation("KnowledgePartners"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageSettings", b => + { + b.Navigation("Countries"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PoliciesSettings", b => + { + b.Navigation("Sections"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/20260525154051_AddOtpVerificationExtraData.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260525154051_AddOtpVerificationExtraData.cs new file mode 100644 index 00000000..7c4854e0 --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260525154051_AddOtpVerificationExtraData.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CCE.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddOtpVerificationExtraData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "extra_data", + table: "otp_verifications", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "extra_data", + table: "otp_verifications"); + } + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/20260526095022_AddOtpVerificationUserId.Designer.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260526095022_AddOtpVerificationUserId.Designer.cs new file mode 100644 index 00000000..0f629fc6 --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260526095022_AddOtpVerificationUserId.Designer.cs @@ -0,0 +1,3857 @@ +// +using System; +using CCE.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CCE.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(CceDbContext))] + [Migration("20260526095022_AddOtpVerificationUserId")] + partial class AddOtpVerificationUserId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CCE.Domain.Audit.AuditEvent", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("action"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("actor"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier") + .HasColumnName("correlation_id"); + + b.Property("Diff") + .HasColumnType("nvarchar(max)") + .HasColumnName("diff"); + + b.Property("OccurredOn") + .HasColumnType("datetimeoffset") + .HasColumnName("occurred_on"); + + b.Property("Resource") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("resource"); + + b.HasKey("Id") + .HasName("pk_audit_events"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_events_correlation_id"); + + b.HasIndex("Actor", "OccurredOn") + .HasDatabaseName("ix_audit_events_actor_occurred_on"); + + b.ToTable("audit_events", null, t => + { + t.HasTrigger("trg_audit_events_no_update_delete"); + }); + + b.HasAnnotation("SqlServer:UseSqlOutputClause", false); + }); + + modelBuilder.Entity("CCE.Domain.Community.Post", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AnsweredReplyId") + .HasColumnType("uniqueidentifier") + .HasColumnName("answered_reply_id"); + + b.Property("AuthorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("author_id"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)") + .HasColumnName("content"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsAnswerable") + .HasColumnType("bit") + .HasColumnName("is_answerable"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale"); + + b.Property("TopicId") + .HasColumnType("uniqueidentifier") + .HasColumnName("topic_id"); + + b.HasKey("Id") + .HasName("pk_posts"); + + b.HasIndex("TopicId") + .HasDatabaseName("ix_post_topic_id"); + + b.HasIndex("AuthorId", "CreatedOn") + .HasDatabaseName("ix_post_author_created"); + + b.ToTable("posts", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Community.PostFollow", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("FollowedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("followed_on"); + + b.Property("PostId") + .HasColumnType("uniqueidentifier") + .HasColumnName("post_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_post_follows"); + + b.HasIndex("PostId", "UserId") + .IsUnique() + .HasDatabaseName("ux_post_follow_post_user"); + + b.ToTable("post_follows", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Community.PostRating", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("PostId") + .HasColumnType("uniqueidentifier") + .HasColumnName("post_id"); + + b.Property("RatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("rated_on"); + + b.Property("Stars") + .HasColumnType("int") + .HasColumnName("stars"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_post_ratings"); + + b.HasIndex("PostId", "UserId") + .IsUnique() + .HasDatabaseName("ux_post_rating_post_user"); + + b.ToTable("post_ratings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Community.PostReply", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AuthorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("author_id"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)") + .HasColumnName("content"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsByExpert") + .HasColumnType("bit") + .HasColumnName("is_by_expert"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale"); + + b.Property("ParentReplyId") + .HasColumnType("uniqueidentifier") + .HasColumnName("parent_reply_id"); + + b.Property("PostId") + .HasColumnType("uniqueidentifier") + .HasColumnName("post_id"); + + b.HasKey("Id") + .HasName("pk_post_replies"); + + b.HasIndex("ParentReplyId") + .HasDatabaseName("ix_post_reply_parent_id"); + + b.HasIndex("PostId") + .HasDatabaseName("ix_post_reply_post_id"); + + b.ToTable("post_replies", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Community.Topic", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IconUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("icon_url"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("ParentId") + .HasColumnType("uniqueidentifier") + .HasColumnName("parent_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_topics"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ux_topic_slug_active") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("topics", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Community.TopicFollow", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("FollowedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("followed_on"); + + b.Property("TopicId") + .HasColumnType("uniqueidentifier") + .HasColumnName("topic_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_topic_follows"); + + b.HasIndex("TopicId", "UserId") + .IsUnique() + .HasDatabaseName("ux_topic_follow_topic_user"); + + b.ToTable("topic_follows", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Community.UserFollow", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("FollowedId") + .HasColumnType("uniqueidentifier") + .HasColumnName("followed_id"); + + b.Property("FollowedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("followed_on"); + + b.Property("FollowerId") + .HasColumnType("uniqueidentifier") + .HasColumnName("follower_id"); + + b.HasKey("Id") + .HasName("pk_user_follows"); + + b.HasIndex("FollowerId", "FollowedId") + .IsUnique() + .HasDatabaseName("ux_user_follow_follower_followed"); + + b.ToTable("user_follows", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.AssetFile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("MimeType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("mime_type"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("original_file_name"); + + b.Property("ScannedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("scanned_on"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size_bytes"); + + b.Property("UploadedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("uploaded_by_id"); + + b.Property("UploadedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("uploaded_on"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("url"); + + b.Property("VirusScanStatus") + .HasColumnType("int") + .HasColumnName("virus_scan_status"); + + b.HasKey("Id") + .HasName("pk_asset_files"); + + b.HasIndex("VirusScanStatus") + .HasDatabaseName("ix_asset_file_scan_status"); + + b.ToTable("asset_files", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.Event", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("EndsOn") + .HasColumnType("datetimeoffset") + .HasColumnName("ends_on"); + + b.Property("FeaturedImageUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("featured_image_url"); + + b.Property("ICalUid") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("i_cal_uid"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LocationAr") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("location_ar"); + + b.Property("LocationEn") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("location_en"); + + b.Property("OnlineMeetingUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("online_meeting_url"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("StartsOn") + .HasColumnType("datetimeoffset") + .HasColumnName("starts_on"); + + b.Property("TitleAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_en"); + + b.HasKey("Id") + .HasName("pk_events"); + + b.HasIndex("ICalUid") + .IsUnique() + .HasDatabaseName("ux_event_ical_uid"); + + b.HasIndex("StartsOn") + .HasDatabaseName("ix_event_starts_on"); + + b.ToTable("events", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.HomepageSection", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ContentAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_ar"); + + b.Property("ContentEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("SectionType") + .HasColumnType("int") + .HasColumnName("section_type"); + + b.HasKey("Id") + .HasName("pk_homepage_sections"); + + b.HasIndex("IsActive", "OrderIndex") + .HasDatabaseName("ix_homepage_section_active_order"); + + b.ToTable("homepage_sections", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.News", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AuthorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("author_id"); + + b.Property("ContentAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_ar"); + + b.Property("ContentEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("FeaturedImageUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("featured_image_url"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("IsFeatured") + .HasColumnType("bit") + .HasColumnName("is_featured"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("PublishedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("published_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("slug"); + + b.Property("TitleAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_en"); + + b.HasKey("Id") + .HasName("pk_news"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_news_published_on"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ux_news_slug_active") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("news", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.NewsletterSubscription", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ConfirmationToken") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("confirmation_token"); + + b.Property("ConfirmedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("confirmed_on"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("nvarchar(320)") + .HasColumnName("email"); + + b.Property("IsConfirmed") + .HasColumnType("bit") + .HasColumnName("is_confirmed"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LocalePreference") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale_preference"); + + b.Property("UnsubscribedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("unsubscribed_on"); + + b.HasKey("Id") + .HasName("pk_newsletter_subscriptions"); + + b.HasIndex("ConfirmationToken") + .HasDatabaseName("ix_newsletter_token"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ux_newsletter_email"); + + b.ToTable("newsletter_subscriptions", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.Page", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ContentAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_ar"); + + b.Property("ContentEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("PageType") + .HasColumnType("int") + .HasColumnName("page_type"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("slug"); + + b.Property("TitleAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_en"); + + b.HasKey("Id") + .HasName("pk_pages"); + + b.HasIndex("PageType", "Slug") + .IsUnique() + .HasDatabaseName("ux_page_type_slug_active") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("pages", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.Resource", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssetFileId") + .HasColumnType("uniqueidentifier") + .HasColumnName("asset_file_id"); + + b.Property("CategoryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("category_id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("PublishedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("published_on"); + + b.Property("ResourceType") + .HasColumnType("int") + .HasColumnName("resource_type"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("TitleAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("title_en"); + + b.Property("UploadedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("uploaded_by_id"); + + b.Property("ViewCount") + .HasColumnType("bigint") + .HasColumnName("view_count"); + + b.HasKey("Id") + .HasName("pk_resources"); + + b.HasIndex("AssetFileId") + .HasDatabaseName("ix_resource_asset_file_id"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_resource_country_id"); + + b.HasIndex("CategoryId", "PublishedOn") + .HasDatabaseName("ix_resource_category_published"); + + b.ToTable("resources", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.ResourceCategory", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("ParentId") + .HasColumnType("uniqueidentifier") + .HasColumnName("parent_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_resource_categories"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_resource_category_parent_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ux_resource_category_slug"); + + b.ToTable("resource_categories", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Country.Country", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("FlagUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("flag_url"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("IsoAlpha2") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("iso_alpha2"); + + b.Property("IsoAlpha3") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)") + .HasColumnName("iso_alpha3"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LatestKapsarcSnapshotId") + .HasColumnType("uniqueidentifier") + .HasColumnName("latest_kapsarc_snapshot_id"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("RegionAr") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("region_ar"); + + b.Property("RegionEn") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("region_en"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasIndex("IsoAlpha2") + .HasDatabaseName("ix_country_iso_alpha2"); + + b.HasIndex("IsoAlpha3") + .IsUnique() + .HasDatabaseName("ux_country_iso_alpha3_active") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("countries", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Country.CountryKapsarcSnapshot", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Classification") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("classification"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("PerformanceScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)") + .HasColumnName("performance_score"); + + b.Property("SnapshotTakenOn") + .HasColumnType("datetimeoffset") + .HasColumnName("snapshot_taken_on"); + + b.Property("SourceVersion") + .HasMaxLength(32) + .HasColumnType("nvarchar(32)") + .HasColumnName("source_version"); + + b.Property("TotalIndex") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)") + .HasColumnName("total_index"); + + b.HasKey("Id") + .HasName("pk_country_kapsarc_snapshots"); + + b.HasIndex("CountryId", "SnapshotTakenOn") + .HasDatabaseName("ix_kapsarc_snapshot_country_taken"); + + b.ToTable("country_kapsarc_snapshots", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Country.CountryProfile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ContactInfoAr") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("contact_info_ar"); + + b.Property("ContactInfoEn") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("contact_info_en"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("KeyInitiativesAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("key_initiatives_ar"); + + b.Property("KeyInitiativesEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("key_initiatives_en"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.HasKey("Id") + .HasName("pk_country_profiles"); + + b.HasIndex("CountryId") + .IsUnique() + .HasDatabaseName("ux_country_profile_country_id"); + + b.ToTable("country_profiles", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Country.CountryResourceRequest", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AdminNotesAr") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("admin_notes_ar"); + + b.Property("AdminNotesEn") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("admin_notes_en"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("ProcessedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("processed_by_id"); + + b.Property("ProcessedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("processed_on"); + + b.Property("ProposedAssetFileId") + .HasColumnType("uniqueidentifier") + .HasColumnName("proposed_asset_file_id"); + + b.Property("ProposedDescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("proposed_description_ar"); + + b.Property("ProposedDescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("proposed_description_en"); + + b.Property("ProposedResourceType") + .HasColumnType("int") + .HasColumnName("proposed_resource_type"); + + b.Property("ProposedTitleAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("proposed_title_ar"); + + b.Property("ProposedTitleEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("proposed_title_en"); + + b.Property("RequestedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("requested_by_id"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.HasKey("Id") + .HasName("pk_country_resource_requests"); + + b.HasIndex("CountryId", "Status") + .HasDatabaseName("ix_country_request_country_status"); + + b.ToTable("country_resource_requests", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertProfile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AcademicTitleAr") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("academic_title_ar"); + + b.Property("AcademicTitleEn") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("academic_title_en"); + + b.Property("ApprovedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("approved_by_id"); + + b.Property("ApprovedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("approved_on"); + + b.Property("BioAr") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("bio_ar"); + + b.Property("BioEn") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("bio_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.PrimitiveCollection("ExpertiseTags") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("expertise_tags"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_expert_profiles"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("ux_expert_profile_active_user") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("expert_profiles", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRegistrationRequest", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("ProcessedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("processed_by_id"); + + b.Property("ProcessedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("processed_on"); + + b.Property("RejectionReasonAr") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("rejection_reason_ar"); + + b.Property("RejectionReasonEn") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("rejection_reason_en"); + + b.Property("RequestedBioAr") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("requested_bio_ar"); + + b.Property("RequestedBioEn") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("requested_bio_en"); + + b.Property("RequestedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("requested_by_id"); + + b.PrimitiveCollection("RequestedTags") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("requested_tags"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.HasKey("Id") + .HasName("pk_expert_registration_requests"); + + b.HasIndex("RequestedById") + .HasDatabaseName("ix_expert_request_requested_by"); + + b.HasIndex("Status") + .HasDatabaseName("ix_expert_request_status"); + + b.ToTable("expert_registration_requests", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRequestAttachment", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssetFileId") + .HasColumnType("uniqueidentifier") + .HasColumnName("asset_file_id"); + + b.Property("AttachmentType") + .HasColumnType("int") + .HasColumnName("attachment_type"); + + b.Property("ExpertRequestId") + .HasColumnType("uniqueidentifier") + .HasColumnName("expert_request_id"); + + b.Property("UploadedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("uploaded_at"); + + b.HasKey("Id") + .HasName("pk_expert_request_attachments"); + + b.HasIndex("ExpertRequestId") + .HasDatabaseName("ix_expert_request_attachments_expert_request_id"); + + b.ToTable("expert_request_attachments", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset") + .HasColumnName("created_at_utc"); + + b.Property("CreatedByIp") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("created_by_ip"); + + b.Property("ExpiresAtUtc") + .HasColumnType("datetimeoffset") + .HasColumnName("expires_at_utc"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("replaced_by_token_hash"); + + b.Property("RevokedAtUtc") + .HasColumnType("datetimeoffset") + .HasColumnName("revoked_at_utc"); + + b.Property("RevokedByIp") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("revoked_by_ip"); + + b.Property("TokenFamilyId") + .HasColumnType("uniqueidentifier") + .HasColumnName("token_family_id"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("token_hash"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("user_agent"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("TokenFamilyId") + .HasDatabaseName("ix_refresh_tokens_token_family_id"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ux_refresh_tokens_token_hash"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_refresh_tokens_user_id"); + + b.ToTable("refresh_tokens", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("normalized_name"); + + b.HasKey("Id") + .HasName("pk_asp_net_roles"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[normalized_name] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.StateRepresentativeAssignment", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssignedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("assigned_by_id"); + + b.Property("AssignedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("assigned_on"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RevokedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("revoked_by_id"); + + b.Property("RevokedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("revoked_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_state_representative_assignments"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_state_rep_country_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_state_rep_user_id"); + + b.HasIndex("UserId", "CountryId") + .IsUnique() + .HasDatabaseName("ux_state_rep_active_user_country") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("state_representative_assignments", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AccessFailedCount") + .HasColumnType("int") + .HasColumnName("access_failed_count"); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("avatar_url"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)") + .HasColumnName("concurrency_stamp"); + + b.Property("CountryCodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_code_id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("bit") + .HasColumnName("email_confirmed"); + + b.Property("EntraIdObjectId") + .HasColumnType("uniqueidentifier") + .HasColumnName("entra_id_object_id"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("first_name"); + + b.PrimitiveCollection("Interests") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("interests"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("job_title"); + + b.Property("KnowledgeLevel") + .HasColumnType("int") + .HasColumnName("knowledge_level"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("last_name"); + + b.Property("LocalePreference") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale_preference"); + + b.Property("LockoutEnabled") + .HasColumnType("bit") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("normalized_user_name"); + + b.Property("OrganizationName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("organization_name"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)") + .HasColumnName("password_hash"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit") + .HasColumnName("phone_number_confirmed"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)") + .HasColumnName("security_stamp"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit") + .HasColumnName("two_factor_enabled"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("pk_asp_net_users"); + + b.HasIndex("CountryCodeId") + .HasDatabaseName("ix_users_country_code_id"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_users_country_id"); + + b.HasIndex("EntraIdObjectId") + .IsUnique() + .HasDatabaseName("ix_asp_net_users_entra_id_object_id") + .HasFilter("[entra_id_object_id] IS NOT NULL"); + + b.HasIndex("NormalizedEmail") + .IsUnique() + .HasDatabaseName("ix_users_normalized_email_unique") + .HasFilter("[normalized_email] IS NOT NULL"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[normalized_user_name] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityScenario", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CityType") + .HasColumnType("int") + .HasColumnName("city_type"); + + b.Property("ConfigurationJson") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("configuration_json"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("TargetYear") + .HasColumnType("int") + .HasColumnName("target_year"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_city_scenarios"); + + b.HasIndex("UserId", "LastModifiedOn") + .HasDatabaseName("ix_city_scenario_user_modified"); + + b.ToTable("city_scenarios", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityScenarioResult", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ComputedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("computed_at"); + + b.Property("ComputedCarbonNeutralityYear") + .HasColumnType("int") + .HasColumnName("computed_carbon_neutrality_year"); + + b.Property("ComputedTotalCostUsd") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("computed_total_cost_usd"); + + b.Property("EngineVersion") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("engine_version"); + + b.Property("ScenarioId") + .HasColumnType("uniqueidentifier") + .HasColumnName("scenario_id"); + + b.HasKey("Id") + .HasName("pk_city_scenario_results"); + + b.HasIndex("ScenarioId", "ComputedAt") + .HasDatabaseName("ix_city_result_scenario_at"); + + b.ToTable("city_scenario_results", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityTechnology", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CarbonImpactKgPerYear") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("carbon_impact_kg_per_year"); + + b.Property("CategoryAr") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("category_ar"); + + b.Property("CategoryEn") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("category_en"); + + b.Property("CostUsd") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("cost_usd"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IconUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("icon_url"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.HasKey("Id") + .HasName("pk_city_technologies"); + + b.HasIndex("IsActive") + .HasDatabaseName("ix_city_tech_is_active"); + + b.ToTable("city_technologies", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMap", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_knowledge_maps"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ux_knowledge_map_slug_active") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("knowledge_maps", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMapAssociation", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssociatedId") + .HasColumnType("uniqueidentifier") + .HasColumnName("associated_id"); + + b.Property("AssociatedType") + .HasColumnType("int") + .HasColumnName("associated_type"); + + b.Property("NodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("node_id"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_knowledge_map_associations"); + + b.HasIndex("NodeId", "AssociatedType", "AssociatedId") + .IsUnique() + .HasDatabaseName("ux_km_assoc_node_type_id"); + + b.ToTable("knowledge_map_associations", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMapEdge", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("FromNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("from_node_id"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("map_id"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("RelationshipType") + .HasColumnType("int") + .HasColumnName("relationship_type"); + + b.Property("ToNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("to_node_id"); + + b.HasKey("Id") + .HasName("pk_knowledge_map_edges"); + + b.HasIndex("FromNodeId") + .HasDatabaseName("ix_km_edge_from_node"); + + b.HasIndex("ToNodeId") + .HasDatabaseName("ix_km_edge_to_node"); + + b.HasIndex("MapId", "FromNodeId", "ToNodeId", "RelationshipType") + .IsUnique() + .HasDatabaseName("ux_km_edge_map_from_to_relation"); + + b.ToTable("knowledge_map_edges", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMapNode", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("DescriptionAr") + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IconUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("icon_url"); + + b.Property("LayoutX") + .HasColumnType("float") + .HasColumnName("layout_x"); + + b.Property("LayoutY") + .HasColumnType("float") + .HasColumnName("layout_y"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("map_id"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("NodeType") + .HasColumnType("int") + .HasColumnName("node_type"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_knowledge_map_nodes"); + + b.HasIndex("MapId", "OrderIndex") + .HasDatabaseName("ix_km_node_map_order"); + + b.ToTable("knowledge_map_nodes", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Lookups.CountryCode", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DialCode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("nvarchar(16)") + .HasColumnName("dial_code"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.HasKey("Id") + .HasName("pk_country_codes"); + + b.HasIndex("DialCode") + .HasDatabaseName("ix_country_code_dial_code"); + + b.ToTable("country_codes", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Media.MediaFile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AltTextAr") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("alt_text_ar"); + + b.Property("AltTextEn") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("alt_text_en"); + + b.Property("DescriptionAr") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_en"); + + b.Property("MimeType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("mime_type"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("original_file_name"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size_bytes"); + + b.Property("StorageKey") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("storage_key"); + + b.Property("TitleAr") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("title_en"); + + b.Property("UploadedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("uploaded_by_id"); + + b.Property("UploadedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("uploaded_on"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("pk_media_files"); + + b.ToTable("media_files", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.NotificationLog", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AttemptCount") + .HasColumnType("int") + .HasColumnName("attempt_count"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("correlation_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FailedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("failed_on"); + + b.Property("PayloadJson") + .HasColumnType("nvarchar(max)") + .HasColumnName("payload_json"); + + b.Property("ProviderMessageId") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("provider_message_id"); + + b.Property("RecipientUserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("recipient_user_id"); + + b.Property("SentOn") + .HasColumnType("datetimeoffset") + .HasColumnName("sent_on"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("TemplateCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("template_code"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier") + .HasColumnName("template_id"); + + b.HasKey("Id") + .HasName("pk_notification_logs"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_notification_log_correlation_id"); + + b.HasIndex("TemplateCode", "Channel") + .HasDatabaseName("ix_notification_log_template_channel"); + + b.HasIndex("RecipientUserId", "Status", "CreatedOn") + .HasDatabaseName("ix_notification_log_recipient_status_created"); + + b.ToTable("notification_logs", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.NotificationTemplate", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("BodyAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("body_ar"); + + b.Property("BodyEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("body_en"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("code"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("SubjectAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("subject_ar"); + + b.Property("SubjectEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("subject_en"); + + b.Property("VariableSchemaJson") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("variable_schema_json"); + + b.HasKey("Id") + .HasName("pk_notification_templates"); + + b.HasIndex("Code", "Channel") + .IsUnique() + .HasDatabaseName("ux_notification_template_code_channel"); + + b.ToTable("notification_templates", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.UserNotification", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("ReadOn") + .HasColumnType("datetimeoffset") + .HasColumnName("read_on"); + + b.Property("RenderedBody") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("rendered_body"); + + b.Property("RenderedLocale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("rendered_locale"); + + b.Property("RenderedSubjectAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("rendered_subject_ar"); + + b.Property("RenderedSubjectEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("rendered_subject_en"); + + b.Property("SentOn") + .HasColumnType("datetimeoffset") + .HasColumnName("sent_on"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier") + .HasColumnName("template_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_notifications"); + + b.HasIndex("UserId", "Status") + .HasDatabaseName("ix_user_notification_user_status"); + + b.ToTable("user_notifications", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.UserNotificationSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("EventCode") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("event_code"); + + b.Property("IsEnabled") + .HasColumnType("bit") + .HasColumnName("is_enabled"); + + b.Property("UpdatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("updated_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_notification_settings"); + + b.HasIndex("UserId", "Channel", "EventCode") + .IsUnique() + .HasDatabaseName("ux_user_notification_settings_user_channel_event") + .HasFilter("[event_code] IS NOT NULL"); + + b.ToTable("user_notification_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.AboutSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("HowToUseVideoUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("how_to_use_video_url"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.HasKey("Id") + .HasName("pk_about_settings"); + + b.ToTable("about_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.GlossaryEntry", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AboutSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("about_settings_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_glossary_entries"); + + b.HasIndex("AboutSettingsId") + .HasDatabaseName("ix_glossary_entries_about_settings_id"); + + b.ToTable("glossary_entries", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageCountry", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("HomepageSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("homepage_settings_id"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_homepage_countries"); + + b.HasIndex("HomepageSettingsId", "CountryId") + .IsUnique() + .HasDatabaseName("ix_homepage_country_settings_country"); + + b.ToTable("homepage_countries", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CceConceptsAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("cce_concepts_ar"); + + b.Property("CceConceptsEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("cce_concepts_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("VideoUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("video_url"); + + b.HasKey("Id") + .HasName("pk_homepage_settings"); + + b.ToTable("homepage_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.KnowledgePartner", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AboutSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("about_settings_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LogoUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("logo_url"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("WebsiteUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("website_url"); + + b.HasKey("Id") + .HasName("pk_knowledge_partners"); + + b.HasIndex("AboutSettingsId") + .HasDatabaseName("ix_knowledge_partners_about_settings_id"); + + b.ToTable("knowledge_partners", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PoliciesSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.HasKey("Id") + .HasName("pk_policies_settings"); + + b.ToTable("policies_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PolicySection", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("PoliciesSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("policies_settings_id"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_policy_sections"); + + b.HasIndex("PoliciesSettingsId") + .HasDatabaseName("ix_policy_sections_policies_settings_id"); + + b.ToTable("policy_sections", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Surveys.SearchQueryLog", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale"); + + b.Property("QueryText") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("query_text"); + + b.Property("ResponseTimeMs") + .HasColumnType("int") + .HasColumnName("response_time_ms"); + + b.Property("ResultsCount") + .HasColumnType("int") + .HasColumnName("results_count"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_search_query_logs"); + + b.HasIndex("SubmittedOn") + .HasDatabaseName("ix_search_query_log_submitted_on"); + + b.ToTable("search_query_logs", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Surveys.ServiceRating", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CommentAr") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("comment_ar"); + + b.Property("CommentEn") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("comment_en"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale"); + + b.Property("Page") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("page"); + + b.Property("Rating") + .HasColumnType("int") + .HasColumnName("rating"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_service_ratings"); + + b.HasIndex("SubmittedOn") + .HasDatabaseName("ix_service_rating_submitted_on"); + + b.ToTable("service_ratings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Verification.OtpVerification", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AttemptCount") + .HasColumnType("int") + .HasColumnName("attempt_count"); + + b.Property("CodeHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("code_hash"); + + b.Property("Contact") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("contact"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset") + .HasColumnName("expires_at"); + + b.Property("ExtraData") + .HasColumnType("nvarchar(max)") + .HasColumnName("extra_data"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("IsInvalidated") + .HasColumnType("bit") + .HasColumnName("is_invalidated"); + + b.Property("IsVerified") + .HasColumnType("bit") + .HasColumnName("is_verified"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LastSentAt") + .HasColumnType("datetimeoffset") + .HasColumnName("last_sent_at"); + + b.Property("TypeId") + .HasColumnType("int") + .HasColumnName("type_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_otp_verifications"); + + b.HasIndex("Contact", "TypeId") + .HasDatabaseName("ix_otp_verifications_contact_type_id"); + + b.HasIndex("UserId", "Contact", "TypeId") + .HasDatabaseName("ix_otp_verifications_user_contact_type"); + + b.ToTable("otp_verifications", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Verification.UserVerification", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Contact") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("contact"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("IsVerified") + .HasColumnType("bit") + .HasColumnName("is_verified"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("TypeId") + .HasColumnType("int") + .HasColumnName("type_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("VerifiedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_user_verifications"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_verifications_user_id"); + + b.HasIndex("Contact", "TypeId") + .IsUnique() + .HasDatabaseName("ix_user_verifications_contact_type_id"); + + b.ToTable("user_verifications", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_asp_net_role_claims"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_asp_net_role_claims_role_id"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_asp_net_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_asp_net_user_claims_user_id"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_asp_net_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_asp_net_user_logins_user_id"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier") + .HasColumnName("role_id"); + + b.HasKey("UserId", "RoleId") + .HasName("pk_asp_net_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_asp_net_user_roles_role_id"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("nvarchar(450)") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("nvarchar(max)") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_asp_net_user_tokens"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRequestAttachment", b => + { + b.HasOne("CCE.Domain.Identity.ExpertRegistrationRequest", null) + .WithMany("Attachments") + .HasForeignKey("ExpertRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_expert_request_attachments_expert_registration_requests_expert_request_id"); + }); + + modelBuilder.Entity("CCE.Domain.Identity.RefreshToken", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_asp_net_users_user_id"); + }); + + modelBuilder.Entity("CCE.Domain.Lookups.CountryCode", b => + { + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Name", b1 => + { + b1.Property("CountryCodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b1.HasKey("CountryCodeId"); + + b1.ToTable("country_codes"); + + b1.WithOwner() + .HasForeignKey("CountryCodeId") + .HasConstraintName("fk_country_codes_country_codes_id"); + }); + + b.Navigation("Name") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.AboutSettings", b => + { + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Description", b1 => + { + b1.Property("AboutSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_en"); + + b1.HasKey("AboutSettingsId"); + + b1.ToTable("about_settings"); + + b1.WithOwner() + .HasForeignKey("AboutSettingsId") + .HasConstraintName("fk_about_settings_about_settings_id"); + }); + + b.Navigation("Description") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.GlossaryEntry", b => + { + b.HasOne("CCE.Domain.PlatformSettings.AboutSettings", null) + .WithMany("GlossaryEntries") + .HasForeignKey("AboutSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_glossary_entries_about_settings_about_settings_id"); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Definition", b1 => + { + b1.Property("GlossaryEntryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("definition_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("definition_en"); + + b1.HasKey("GlossaryEntryId"); + + b1.ToTable("glossary_entries"); + + b1.WithOwner() + .HasForeignKey("GlossaryEntryId") + .HasConstraintName("fk_glossary_entries_glossary_entries_id"); + }); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Term", b1 => + { + b1.Property("GlossaryEntryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("term_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("term_en"); + + b1.HasKey("GlossaryEntryId"); + + b1.ToTable("glossary_entries"); + + b1.WithOwner() + .HasForeignKey("GlossaryEntryId") + .HasConstraintName("fk_glossary_entries_glossary_entries_id"); + }); + + b.Navigation("Definition") + .IsRequired(); + + b.Navigation("Term") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageCountry", b => + { + b.HasOne("CCE.Domain.PlatformSettings.HomepageSettings", null) + .WithMany("Countries") + .HasForeignKey("HomepageSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_homepage_countries_homepage_settings_homepage_settings_id"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageSettings", b => + { + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Objective", b1 => + { + b1.Property("HomepageSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("objective_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("objective_en"); + + b1.HasKey("HomepageSettingsId"); + + b1.ToTable("homepage_settings"); + + b1.WithOwner() + .HasForeignKey("HomepageSettingsId") + .HasConstraintName("fk_homepage_settings_homepage_settings_id"); + }); + + b.Navigation("Objective") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.KnowledgePartner", b => + { + b.HasOne("CCE.Domain.PlatformSettings.AboutSettings", null) + .WithMany("KnowledgePartners") + .HasForeignKey("AboutSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_knowledge_partners_about_settings_about_settings_id"); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Description", b1 => + { + b1.Property("KnowledgePartnerId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_en"); + + b1.HasKey("KnowledgePartnerId"); + + b1.ToTable("knowledge_partners"); + + b1.WithOwner() + .HasForeignKey("KnowledgePartnerId") + .HasConstraintName("fk_knowledge_partners_knowledge_partners_id"); + }); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Name", b1 => + { + b1.Property("KnowledgePartnerId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("name_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("name_en"); + + b1.HasKey("KnowledgePartnerId"); + + b1.ToTable("knowledge_partners"); + + b1.WithOwner() + .HasForeignKey("KnowledgePartnerId") + .HasConstraintName("fk_knowledge_partners_knowledge_partners_id"); + }); + + b.Navigation("Description"); + + b.Navigation("Name") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PolicySection", b => + { + b.HasOne("CCE.Domain.PlatformSettings.PoliciesSettings", null) + .WithMany("Sections") + .HasForeignKey("PoliciesSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_policy_sections_policies_settings_policies_settings_id"); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Content", b1 => + { + b1.Property("PolicySectionId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_ar"); + + b1.Property("En") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_en"); + + b1.HasKey("PolicySectionId"); + + b1.ToTable("policy_sections"); + + b1.WithOwner() + .HasForeignKey("PolicySectionId") + .HasConstraintName("fk_policy_sections_policy_sections_id"); + }); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Title", b1 => + { + b1.Property("PolicySectionId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("title_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("title_en"); + + b1.HasKey("PolicySectionId"); + + b1.ToTable("policy_sections"); + + b1.WithOwner() + .HasForeignKey("PolicySectionId") + .HasConstraintName("fk_policy_sections_policy_sections_id"); + }); + + b.Navigation("Content") + .IsRequired(); + + b.Navigation("Title") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.Verification.UserVerification", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_user_verifications_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("CCE.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_role_claims_asp_net_roles_role_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_claims_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_logins_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("CCE.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_roles_asp_net_roles_role_id"); + + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_roles_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_tokens_asp_net_users_user_id"); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRegistrationRequest", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.AboutSettings", b => + { + b.Navigation("GlossaryEntries"); + + b.Navigation("KnowledgePartners"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageSettings", b => + { + b.Navigation("Countries"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PoliciesSettings", b => + { + b.Navigation("Sections"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/20260526095022_AddOtpVerificationUserId.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260526095022_AddOtpVerificationUserId.cs new file mode 100644 index 00000000..b06ae53f --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260526095022_AddOtpVerificationUserId.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CCE.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddOtpVerificationUserId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "EmailIndex", + table: "AspNetUsers"); + + migrationBuilder.AddColumn( + name: "user_id", + table: "otp_verifications", + type: "uniqueidentifier", + nullable: true); + + migrationBuilder.CreateIndex( + name: "ix_otp_verifications_user_contact_type", + table: "otp_verifications", + columns: new[] { "user_id", "contact", "type_id" }); + + migrationBuilder.CreateIndex( + name: "ix_users_normalized_email_unique", + table: "AspNetUsers", + column: "normalized_email", + unique: true, + filter: "[normalized_email] IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_otp_verifications_user_contact_type", + table: "otp_verifications"); + + migrationBuilder.DropIndex( + name: "ix_users_normalized_email_unique", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "user_id", + table: "otp_verifications"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "normalized_email"); + } + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/CceDbContextModelSnapshot.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/CceDbContextModelSnapshot.cs index 7b8b61e2..9749ece7 100644 --- a/backend/src/CCE.Infrastructure/Persistence/Migrations/CceDbContextModelSnapshot.cs +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/CceDbContextModelSnapshot.cs @@ -1936,7 +1936,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasFilter("[entra_id_object_id] IS NOT NULL"); b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); + .IsUnique() + .HasDatabaseName("ix_users_normalized_email_unique") + .HasFilter("[normalized_email] IS NOT NULL"); b.HasIndex("NormalizedUserName") .IsUnique() @@ -3177,6 +3179,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetimeoffset") .HasColumnName("expires_at"); + b.Property("ExtraData") + .HasColumnType("nvarchar(max)") + .HasColumnName("extra_data"); + b.Property("IsDeleted") .HasColumnType("bit") .HasColumnName("is_deleted"); @@ -3205,12 +3211,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int") .HasColumnName("type_id"); + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + b.HasKey("Id") .HasName("pk_otp_verifications"); b.HasIndex("Contact", "TypeId") .HasDatabaseName("ix_otp_verifications_contact_type_id"); + b.HasIndex("UserId", "Contact", "TypeId") + .HasDatabaseName("ix_otp_verifications_user_contact_type"); + b.ToTable("otp_verifications", (string)null); }); @@ -3455,7 +3468,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("CountryCodeId"); - b1.ToTable("country_codes"); + b1.ToTable("country_codes", (string)null); b1.WithOwner() .HasForeignKey("CountryCodeId") @@ -3488,7 +3501,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("AboutSettingsId"); - b1.ToTable("about_settings"); + b1.ToTable("about_settings", (string)null); b1.WithOwner() .HasForeignKey("AboutSettingsId") @@ -3528,7 +3541,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("GlossaryEntryId"); - b1.ToTable("glossary_entries"); + b1.ToTable("glossary_entries", (string)null); b1.WithOwner() .HasForeignKey("GlossaryEntryId") @@ -3555,7 +3568,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("GlossaryEntryId"); - b1.ToTable("glossary_entries"); + b1.ToTable("glossary_entries", (string)null); b1.WithOwner() .HasForeignKey("GlossaryEntryId") @@ -3601,7 +3614,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("HomepageSettingsId"); - b1.ToTable("homepage_settings"); + b1.ToTable("homepage_settings", (string)null); b1.WithOwner() .HasForeignKey("HomepageSettingsId") @@ -3641,7 +3654,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("KnowledgePartnerId"); - b1.ToTable("knowledge_partners"); + b1.ToTable("knowledge_partners", (string)null); b1.WithOwner() .HasForeignKey("KnowledgePartnerId") @@ -3668,7 +3681,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("KnowledgePartnerId"); - b1.ToTable("knowledge_partners"); + b1.ToTable("knowledge_partners", (string)null); b1.WithOwner() .HasForeignKey("KnowledgePartnerId") @@ -3708,7 +3721,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("PolicySectionId"); - b1.ToTable("policy_sections"); + b1.ToTable("policy_sections", (string)null); b1.WithOwner() .HasForeignKey("PolicySectionId") @@ -3735,7 +3748,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("PolicySectionId"); - b1.ToTable("policy_sections"); + b1.ToTable("policy_sections", (string)null); b1.WithOwner() .HasForeignKey("PolicySectionId") diff --git a/backend/src/CCE.Infrastructure/Persistence/Repositories/OtpVerificationRepository.cs b/backend/src/CCE.Infrastructure/Persistence/Repositories/OtpVerificationRepository.cs index 22b3bc83..0c5ece88 100644 --- a/backend/src/CCE.Infrastructure/Persistence/Repositories/OtpVerificationRepository.cs +++ b/backend/src/CCE.Infrastructure/Persistence/Repositories/OtpVerificationRepository.cs @@ -11,13 +11,24 @@ public OtpVerificationRepository(CceDbContext db) : base(db) { } public async Task FindActiveAsync( string contact, OtpVerificationType typeId, DateTimeOffset now, CancellationToken ct) - => await Db.OtpVerifications + => await FindActiveAsync(contact, typeId, now, null, ct).ConfigureAwait(false); + + public async Task FindActiveAsync( + string contact, OtpVerificationType typeId, DateTimeOffset now, Guid? userId, CancellationToken ct) + { + var query = Db.OtpVerifications .Where(o => o.Contact == contact && o.TypeId == typeId && !o.IsVerified && !o.IsInvalidated - && o.ExpiresAt > now) + && o.ExpiresAt > now); + + if (userId.HasValue) + query = query.Where(o => o.UserId == userId.Value); + + return await query .OrderByDescending(o => o.CreatedAt) .FirstOrDefaultAsync(ct) .ConfigureAwait(false); + } } diff --git a/backend/src/CCE.Infrastructure/Persistence/Repositories/UserRepository.cs b/backend/src/CCE.Infrastructure/Persistence/Repositories/UserRepository.cs index 3fdd7310..653658a7 100644 --- a/backend/src/CCE.Infrastructure/Persistence/Repositories/UserRepository.cs +++ b/backend/src/CCE.Infrastructure/Persistence/Repositories/UserRepository.cs @@ -29,6 +29,24 @@ public sealed class UserRepository : IUserRepository }; } + public async Task IsContactTakenAsync(string contact, OtpVerificationType type, Guid excludeUserId, CancellationToken ct) + { + if (type == OtpVerificationType.Email) + { + var normalized = contact.ToUpperInvariant(); + return await _db.Users + .AnyAsync(u => u.NormalizedEmail == normalized && u.Id != excludeUserId, ct) + .ConfigureAwait(false); + } + if (type == OtpVerificationType.Sms) + { + return await _db.Users + .AnyAsync(u => u.PhoneNumber == contact && u.Id != excludeUserId, ct) + .ConfigureAwait(false); + } + return false; + } + public async Task StampConfirmedAsync(Guid userId, OtpVerificationType type, CancellationToken ct) { var stamp = await _db.Users diff --git a/backend/src/CCE.Seeder/Seeders/ReferenceDataSeeder.cs b/backend/src/CCE.Seeder/Seeders/ReferenceDataSeeder.cs index c8cebbfe..a4e23ae0 100644 --- a/backend/src/CCE.Seeder/Seeders/ReferenceDataSeeder.cs +++ b/backend/src/CCE.Seeder/Seeders/ReferenceDataSeeder.cs @@ -263,6 +263,18 @@ private static readonly (string Code, string SubjectAr, string SubjectEn, "Your code: {{Code}}", CCE.Domain.Notifications.NotificationChannel.Sms), + // EMAIL_CHANGE_OTP + ("EMAIL_CHANGE_OTP", "تأكيد تغيير البريد الإلكتروني", "Confirm Email Change", + "رمز التحقق لتغيير بريدك الإلكتروني هو: {{Code}}. صالح لمدة 5 دقائق.", + "Your email change verification code is: {{Code}}. Valid for 5 minutes.", + CCE.Domain.Notifications.NotificationChannel.Email), + + // PHONE_CHANGE_OTP + ("PHONE_CHANGE_OTP", "تأكيد تغيير رقم الجوال", "Confirm Phone Change", + "رمز التحقق لتغيير رقم جوالك هو: {{Code}}. صالح لمدة 5 دقائق.", + "Your phone change verification code is: {{Code}}. Valid for 5 minutes.", + CCE.Domain.Notifications.NotificationChannel.Sms), + // PASSWORD_RESET ("PASSWORD_RESET", "استعادة كلمة المرور", "Reset your password", "مرحباً {{Name}}، استخدم الرابط التالي لإعادة تعيين كلمة المرور: {{ResetUrl}}",