From e49876e20d52721f5b46b1135476e51d23e50b44 Mon Sep 17 00:00:00 2001 From: ahmed Date: Sun, 17 May 2026 14:33:29 +0300 Subject: [PATCH 1/5] feat: implement toggle behavior for user interests --- .../Localization/Resources.yaml | 8 ++++ .../Endpoints/UserInterestEndpoints.cs | 36 ++++++++++++++++ backend/src/CCE.Api.External/Program.cs | 1 + .../UserInterest/UpsertUserInterestCommand.cs | 8 ++++ .../UpsertUserInterestCommandHandler.cs | 42 +++++++++++++++++++ .../UserInterest/UpsertUserInterestResult.cs | 5 +++ .../Messages/MessageFactory.cs | 1 + .../CCE.Application/Messages/SystemCode.cs | 2 + .../CCE.Application/Messages/SystemCodeMap.cs | 2 + backend/src/CCE.Domain/Identity/User.cs | 15 +++++++ 10 files changed, 120 insertions(+) create mode 100644 backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommand.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs create mode 100644 backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestResult.cs diff --git a/backend/src/CCE.Api.Common/Localization/Resources.yaml b/backend/src/CCE.Api.Common/Localization/Resources.yaml index 3bac7e4a..31e84ffe 100644 --- a/backend/src/CCE.Api.Common/Localization/Resources.yaml +++ b/backend/src/CCE.Api.Common/Localization/Resources.yaml @@ -335,3 +335,11 @@ SCENARIO_NOT_FOUND: TECHNOLOGY_NOT_FOUND: ar: "التقنية غير موجودة" en: "Technology not found" + +INTEREST_NOT_FOUND: + ar: "الاهتمام غير موجود" + en: "Interest not found" + +INTEREST_UPSERTED: + ar: "تم تحديث الاهتمامات بنجاح" + en: "Interests updated successfully" diff --git a/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs new file mode 100644 index 00000000..93524005 --- /dev/null +++ b/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs @@ -0,0 +1,36 @@ +using CCE.Api.Common.Extensions; +using CCE.Application.Common.Interfaces; +using CCE.Application.Identity.Public.Commands.UserInterest; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace CCE.Api.External.Endpoints; + +public static class UserInterestEndpoints +{ + public static IEndpointRouteBuilder MapUserInterestEndpoints(this IEndpointRouteBuilder app) + { + var me = app.MapGroup("/api/me").WithTags("User Interests").RequireAuthorization(); + + me.MapPatch("/interests", async ( + UpsertUserInterestRequest 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 UpsertUserInterestCommand(userId, body.Interest), ct).ConfigureAwait(false); + return result.ToHttpResult(); + }) + .WithName("UpsertUserInterest"); + + return app; + } +} + +public sealed record UpsertUserInterestRequest(string Interest); diff --git a/backend/src/CCE.Api.External/Program.cs b/backend/src/CCE.Api.External/Program.cs index f2439ee3..3ebb9b7e 100644 --- a/backend/src/CCE.Api.External/Program.cs +++ b/backend/src/CCE.Api.External/Program.cs @@ -103,6 +103,7 @@ app.MapAssistantEndpoints(); app.MapKapsarcEndpoints(); app.MapSurveysEndpoints(); +app.MapUserInterestEndpoints(); app.MapGet("/health", async (IMediator mediator) => { diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommand.cs b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommand.cs new file mode 100644 index 00000000..77091a40 --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommand.cs @@ -0,0 +1,8 @@ +using CCE.Application.Common; +using MediatR; + +namespace CCE.Application.Identity.Public.Commands.UserInterest; + +public sealed record UpsertUserInterestCommand( + System.Guid UserId, + string Interest) : IRequest>; diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs new file mode 100644 index 00000000..b8410f36 --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs @@ -0,0 +1,42 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Messages; +using MediatR; + +namespace CCE.Application.Identity.Public.Commands.UserInterest; + +public sealed class UpsertUserInterestCommandHandler + : IRequestHandler> +{ + private readonly IUserProfileRepository _service; + private readonly ICceDbContext _db; + private readonly MessageFactory _msg; + + public UpsertUserInterestCommandHandler( + IUserProfileRepository service, + ICceDbContext db, + MessageFactory msg) + { + _service = service; + _db = db; + _msg = msg; + } + + public async Task> Handle( + UpsertUserInterestCommand request, + CancellationToken cancellationToken) + { + var user = await _service.FindAsync(request.UserId, cancellationToken).ConfigureAwait(false); + if (user is null) + return _msg.UserNotFound(); + + var added = user.ToggleInterest(request.Interest); + + _service.Update(user); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return _msg.InterestUpserted(new UpsertUserInterestResult( + user.Interests, + added)); + } +} diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestResult.cs b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestResult.cs new file mode 100644 index 00000000..bf1816dd --- /dev/null +++ b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestResult.cs @@ -0,0 +1,5 @@ +namespace CCE.Application.Identity.Public.Commands.UserInterest; + +public sealed record UpsertUserInterestResult( + IReadOnlyList Interests, + bool Added); diff --git a/backend/src/CCE.Application/Messages/MessageFactory.cs b/backend/src/CCE.Application/Messages/MessageFactory.cs index 6ba4868a..b1742ced 100644 --- a/backend/src/CCE.Application/Messages/MessageFactory.cs +++ b/backend/src/CCE.Application/Messages/MessageFactory.cs @@ -68,6 +68,7 @@ public FieldError Field(string fieldName, string domainKey) // ─── Convenience shortcuts (Identity domain) ─── public Response UserNotFound() => NotFound("USER_NOT_FOUND"); + public Response InterestUpserted(T data) => Ok(data, "INTEREST_UPSERTED"); public Response EmailExists() => Conflict("EMAIL_EXISTS"); public Response InvalidCredentials() => Unauthorized("INVALID_CREDENTIALS"); public Response NotAuthenticated() => Unauthorized("NOT_AUTHENTICATED"); diff --git a/backend/src/CCE.Application/Messages/SystemCode.cs b/backend/src/CCE.Application/Messages/SystemCode.cs index beb7aabc..da99ec15 100644 --- a/backend/src/CCE.Application/Messages/SystemCode.cs +++ b/backend/src/CCE.Application/Messages/SystemCode.cs @@ -21,6 +21,7 @@ public static class SystemCode public const string ERR002 = "ERR002"; // Resource download failure (appendix) public const string ERR003 = "ERR003"; // Resource share failure (appendix) + public const string ERR018 = "ERR018"; // Interest not found public const string ERR019 = "ERR019"; // Email already exists / Account creation failure (appendix) public const string ERR020 = "ERR020"; // Invalid credentials (appendix) public const string ERR021 = "ERR021"; // Login system error (appendix) @@ -115,6 +116,7 @@ public static class SystemCode public const string CON007 = "CON007"; // Admin notified of expert request (appendix) public const string CON008 = "CON008"; // Service evaluation submitted (appendix) public const string CON009 = "CON009"; // Personalized suggestions submitted (appendix) + public const string CON018 = "CON018"; // Interest upserted public const string CON010 = "CON010"; // Topic follow success (appendix) public const string CON011 = "CON011"; // Post created (appendix) public const string CON012 = "CON012"; // Post follow success (appendix) diff --git a/backend/src/CCE.Application/Messages/SystemCodeMap.cs b/backend/src/CCE.Application/Messages/SystemCodeMap.cs index f53ae1a5..c5f4d60b 100644 --- a/backend/src/CCE.Application/Messages/SystemCodeMap.cs +++ b/backend/src/CCE.Application/Messages/SystemCodeMap.cs @@ -14,6 +14,7 @@ public static class SystemCodeMap ["INVALID_CREDENTIALS"] = SystemCode.ERR020, ["PASSWORD_RECOVERY_FAILED"] = SystemCode.ERR023, ["LOGOUT_FAILED"] = SystemCode.ERR024, + ["INTEREST_NOT_FOUND"] = SystemCode.ERR018, // ─── Backend-only Identity Errors (moved to free appendix numbers) ─── ["EXPERT_REQUEST_NOT_FOUND"] = SystemCode.ERR400, @@ -90,6 +91,7 @@ public static class SystemCodeMap ["PASSWORD_RESET"] = SystemCode.CON014, ["LOGOUT_SUCCESS"] = SystemCode.CON015, ["REGISTER_SUCCESS"] = SystemCode.CON017, + ["INTEREST_UPSERTED"] = SystemCode.CON018, // ─── Backend-only Identity Success (appendix numbers already taken) ─── ["EXPERT_REQUEST_APPROVED"] = SystemCode.CON050, diff --git a/backend/src/CCE.Domain/Identity/User.cs b/backend/src/CCE.Domain/Identity/User.cs index 5cdd1e0d..4c95b18f 100644 --- a/backend/src/CCE.Domain/Identity/User.cs +++ b/backend/src/CCE.Domain/Identity/User.cs @@ -144,6 +144,21 @@ public void UpdateInterests(IEnumerable interests) .ToList(); } + /// + /// Toggles an interest. If it exists it is removed; otherwise it is added. + /// Returns true if added, false if removed. + /// + public bool ToggleInterest(string interest) + { + if (string.IsNullOrWhiteSpace(interest)) + throw new DomainException("Interest cannot be null or empty."); + var trimmed = interest.Trim(); + if (Interests.Remove(trimmed)) + return false; + Interests.Add(trimmed); + return true; + } + public void AssignCountry(System.Guid countryId) => CountryId = countryId; public void ClearCountry() => CountryId = null; From ed2e61f3eac2bd9b924ec0ba868cf7c19d9ac973 Mon Sep 17 00:00:00 2001 From: ahmed Date: Sun, 17 May 2026 18:32:24 +0300 Subject: [PATCH 2/5] feat: add user interest toggle endpoint --- .../Endpoints/UserInterestEndpoints.cs | 4 ++-- .../UserInterest/UpsertUserInterestCommand.cs | 2 +- .../UpsertUserInterestCommandHandler.cs | 15 ++++++++++++--- .../UserInterest/UpsertUserInterestResult.cs | 3 ++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs index 93524005..817ecab0 100644 --- a/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs @@ -24,7 +24,7 @@ public static IEndpointRouteBuilder MapUserInterestEndpoints(this IEndpointRoute if (userId == System.Guid.Empty) return Results.Unauthorized(); var result = await mediator.Send( - new UpsertUserInterestCommand(userId, body.Interest), ct).ConfigureAwait(false); + new UpsertUserInterestCommand(userId, body.Interests), ct).ConfigureAwait(false); return result.ToHttpResult(); }) .WithName("UpsertUserInterest"); @@ -33,4 +33,4 @@ public static IEndpointRouteBuilder MapUserInterestEndpoints(this IEndpointRoute } } -public sealed record UpsertUserInterestRequest(string Interest); +public sealed record UpsertUserInterestRequest(IReadOnlyList Interests); diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommand.cs b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommand.cs index 77091a40..10d74bba 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommand.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommand.cs @@ -5,4 +5,4 @@ namespace CCE.Application.Identity.Public.Commands.UserInterest; public sealed record UpsertUserInterestCommand( System.Guid UserId, - string Interest) : IRequest>; + IReadOnlyList Interests) : IRequest>; diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs index b8410f36..1bcde5dc 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs @@ -1,3 +1,4 @@ +using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; @@ -30,13 +31,21 @@ public async Task> Handle( if (user is null) return _msg.UserNotFound(); - var added = user.ToggleInterest(request.Interest); + var oldInterests = user.Interests.ToList(); + var newList = request.Interests ?? System.Array.Empty(); + + user.UpdateInterests(newList); + + var newInterests = user.Interests; + var added = newInterests.Except(oldInterests).ToList(); + var removed = oldInterests.Except(newInterests).ToList(); _service.Update(user); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); return _msg.InterestUpserted(new UpsertUserInterestResult( - user.Interests, - added)); + newInterests, + added, + removed)); } } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestResult.cs b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestResult.cs index bf1816dd..8e553105 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestResult.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestResult.cs @@ -2,4 +2,5 @@ namespace CCE.Application.Identity.Public.Commands.UserInterest; public sealed record UpsertUserInterestResult( IReadOnlyList Interests, - bool Added); + IReadOnlyList Added, + IReadOnlyList Removed); From fce5967b265acdec85087794d4d1e05fe0e95502 Mon Sep 17 00:00:00 2001 From: ahmed Date: Mon, 18 May 2026 12:13:05 +0300 Subject: [PATCH 3/5] delete Toggle Interests user --- backend/src/CCE.Domain/Identity/User.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/backend/src/CCE.Domain/Identity/User.cs b/backend/src/CCE.Domain/Identity/User.cs index 4c95b18f..5cdd1e0d 100644 --- a/backend/src/CCE.Domain/Identity/User.cs +++ b/backend/src/CCE.Domain/Identity/User.cs @@ -144,21 +144,6 @@ public void UpdateInterests(IEnumerable interests) .ToList(); } - /// - /// Toggles an interest. If it exists it is removed; otherwise it is added. - /// Returns true if added, false if removed. - /// - public bool ToggleInterest(string interest) - { - if (string.IsNullOrWhiteSpace(interest)) - throw new DomainException("Interest cannot be null or empty."); - var trimmed = interest.Trim(); - if (Interests.Remove(trimmed)) - return false; - Interests.Add(trimmed); - return true; - } - public void AssignCountry(System.Guid countryId) => CountryId = countryId; public void ClearCountry() => CountryId = null; From f7864f559992c6b77168e4f0a984d5405c8e037a Mon Sep 17 00:00:00 2001 From: ahmed Date: Mon, 18 May 2026 12:34:14 +0300 Subject: [PATCH 4/5] UpsertUserInterest update hte unnessesary wrote in db --- .../UpsertUserInterestCommandHandler.cs | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs index 1bcde5dc..6acd7348 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs @@ -32,19 +32,35 @@ public async Task> Handle( return _msg.UserNotFound(); var oldInterests = user.Interests.ToList(); - var newList = request.Interests ?? System.Array.Empty(); + var rawList = request.Interests ?? System.Array.Empty(); - user.UpdateInterests(newList); + var normalizedNew = rawList + .Select(static s => s?.Trim() ?? string.Empty) + .Where(static s => s.Length > 0) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); - var newInterests = user.Interests; - var added = newInterests.Except(oldInterests).ToList(); - var removed = oldInterests.Except(newInterests).ToList(); + var oldSet = new HashSet(oldInterests, StringComparer.OrdinalIgnoreCase); + var newSet = new HashSet(normalizedNew, StringComparer.OrdinalIgnoreCase); + + if (oldSet.SetEquals(newSet)) + { + return _msg.InterestUpserted(new UpsertUserInterestResult( + user.Interests, + System.Array.Empty(), + System.Array.Empty())); + } + + user.UpdateInterests(normalizedNew); + + var added = normalizedNew.Except(oldInterests, StringComparer.OrdinalIgnoreCase).ToList(); + var removed = oldInterests.Except(normalizedNew, StringComparer.OrdinalIgnoreCase).ToList(); _service.Update(user); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); return _msg.InterestUpserted(new UpsertUserInterestResult( - newInterests, + user.Interests, added, removed)); } From 818770411e31328d7a86a430fc22196267c3fb31 Mon Sep 17 00:00:00 2001 From: ahmed Date: Tue, 19 May 2026 18:31:40 +0300 Subject: [PATCH 5/5] Add Interest Topics feature with user interest management --- .../Localization/Resources.yaml | 16 + .../Endpoints/InterestTopicPublicEndpoints.cs | 24 + .../Endpoints/UserInterestEndpoints.cs | 4 +- backend/src/CCE.Api.External/Program.cs | 1 + .../Endpoints/InterestTopicEndpoints.cs | 78 + backend/src/CCE.Api.Internal/Program.cs | 1 + .../Common/Interfaces/ICceDbContext.cs | 1 + .../Identity/Dtos/UserDetailDto.cs | 3 +- .../UpdateMyProfile/UpdateMyProfileCommand.cs | 1 - .../UpdateMyProfileCommandHandler.cs | 12 +- .../UpdateMyProfileCommandValidator.cs | 2 - .../UpdateMyProfile/UpdateMyProfileRequest.cs | 1 - .../UserInterest/UpsertUserInterestCommand.cs | 2 +- .../UpsertUserInterestCommandHandler.cs | 62 +- .../UserInterest/UpsertUserInterestResult.cs | 8 +- .../Identity/Public/Dtos/UserProfileDto.cs | 3 +- .../GetMyProfile/GetMyProfileQueryHandler.cs | 11 +- .../GetUserById/GetUserByIdQueryHandler.cs | 22 +- .../CreateInterestTopicCommand.cs | 9 + .../CreateInterestTopicCommandHandler.cs | 28 + .../DeleteInterestTopicCommand.cs | 6 + .../DeleteInterestTopicCommandHandler.cs | 29 + .../UpdateInterestTopicCommand.cs | 10 + .../UpdateInterestTopicCommandHandler.cs | 31 + .../Dtos/InterestTopicDto.cs | 7 + .../IInterestTopicRepository.cs | 11 + .../GetInterestTopicByIdQuery.cs | 7 + .../GetInterestTopicByIdQueryHandler.cs | 36 + .../ListInterestTopicsQuery.cs | 7 + .../ListInterestTopicsQueryHandler.cs | 33 + .../Messages/MessageFactory.cs | 1 + .../CCE.Application/Messages/SystemCode.cs | 9 +- .../CCE.Application/Messages/SystemCodeMap.cs | 9 +- .../src/CCE.Domain/Identity/InterestTopic.cs | 44 + backend/src/CCE.Domain/Identity/User.cs | 19 +- .../CCE.Domain/Identity/UserInterestTopic.cs | 12 + .../CCE.Infrastructure/DependencyInjection.cs | 5 + .../Identity/UserProfileRepository.cs | 6 +- .../InterestTopicRepository.cs | 34 + .../Persistence/CceDbContext.cs | 3 + .../Identity/InterestTopicConfiguration.cs | 16 + .../Identity/UserConfiguration.cs | 1 - .../UserInterestTopicConfiguration.cs | 23 + ...15121258_StandardizeCountryProfileAudit.cs | 14 +- ...260518133355_AddInterestTopics.Designer.cs | 2772 +++++++++++++++++ .../20260518133355_AddInterestTopics.cs | 79 + .../Migrations/CceDbContextModelSnapshot.cs | 348 ++- .../src/CCE.Infrastructure/dotnet-tools.json | 5 + 48 files changed, 3784 insertions(+), 82 deletions(-) create mode 100644 backend/src/CCE.Api.External/Endpoints/InterestTopicPublicEndpoints.cs create mode 100644 backend/src/CCE.Api.Internal/Endpoints/InterestTopicEndpoints.cs create mode 100644 backend/src/CCE.Application/InterestManagement/Commands/CreateInterestTopic/CreateInterestTopicCommand.cs create mode 100644 backend/src/CCE.Application/InterestManagement/Commands/CreateInterestTopic/CreateInterestTopicCommandHandler.cs create mode 100644 backend/src/CCE.Application/InterestManagement/Commands/DeleteInterestTopic/DeleteInterestTopicCommand.cs create mode 100644 backend/src/CCE.Application/InterestManagement/Commands/DeleteInterestTopic/DeleteInterestTopicCommandHandler.cs create mode 100644 backend/src/CCE.Application/InterestManagement/Commands/UpdateInterestTopic/UpdateInterestTopicCommand.cs create mode 100644 backend/src/CCE.Application/InterestManagement/Commands/UpdateInterestTopic/UpdateInterestTopicCommandHandler.cs create mode 100644 backend/src/CCE.Application/InterestManagement/Dtos/InterestTopicDto.cs create mode 100644 backend/src/CCE.Application/InterestManagement/IInterestTopicRepository.cs create mode 100644 backend/src/CCE.Application/InterestManagement/Queries/GetInterestTopicById/GetInterestTopicByIdQuery.cs create mode 100644 backend/src/CCE.Application/InterestManagement/Queries/GetInterestTopicById/GetInterestTopicByIdQueryHandler.cs create mode 100644 backend/src/CCE.Application/InterestManagement/Queries/ListInterestTopics/ListInterestTopicsQuery.cs create mode 100644 backend/src/CCE.Application/InterestManagement/Queries/ListInterestTopics/ListInterestTopicsQueryHandler.cs create mode 100644 backend/src/CCE.Domain/Identity/InterestTopic.cs create mode 100644 backend/src/CCE.Domain/Identity/UserInterestTopic.cs create mode 100644 backend/src/CCE.Infrastructure/InterestManagement/InterestTopicRepository.cs create mode 100644 backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/InterestTopicConfiguration.cs create mode 100644 backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/UserInterestTopicConfiguration.cs create mode 100644 backend/src/CCE.Infrastructure/Persistence/Migrations/20260518133355_AddInterestTopics.Designer.cs create mode 100644 backend/src/CCE.Infrastructure/Persistence/Migrations/20260518133355_AddInterestTopics.cs create mode 100644 backend/src/CCE.Infrastructure/dotnet-tools.json diff --git a/backend/src/CCE.Api.Common/Localization/Resources.yaml b/backend/src/CCE.Api.Common/Localization/Resources.yaml index 31e84ffe..05ddbff0 100644 --- a/backend/src/CCE.Api.Common/Localization/Resources.yaml +++ b/backend/src/CCE.Api.Common/Localization/Resources.yaml @@ -343,3 +343,19 @@ INTEREST_NOT_FOUND: INTEREST_UPSERTED: ar: "تم تحديث الاهتمامات بنجاح" en: "Interests updated successfully" + +INTEREST_TOPIC_NOT_FOUND: + ar: "موضوع الاهتمام غير موجود" + en: "Interest topic not found" + +INTEREST_TOPIC_CREATED: + ar: "تم إنشاء موضوع الاهتمام بنجاح" + en: "Interest topic created successfully" + +INTEREST_TOPIC_UPDATED: + ar: "تم تحديث موضوع الاهتمام بنجاح" + en: "Interest topic updated successfully" + +INTEREST_TOPIC_DELETED: + ar: "تم حذف موضوع الاهتمام بنجاح" + en: "Interest topic deleted successfully" diff --git a/backend/src/CCE.Api.External/Endpoints/InterestTopicPublicEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/InterestTopicPublicEndpoints.cs new file mode 100644 index 00000000..0e761f3a --- /dev/null +++ b/backend/src/CCE.Api.External/Endpoints/InterestTopicPublicEndpoints.cs @@ -0,0 +1,24 @@ +using CCE.Api.Common.Extensions; +using CCE.Application.InterestManagement.Queries.ListInterestTopics; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace CCE.Api.External.Endpoints; + +public static class InterestTopicPublicEndpoints +{ + public static IEndpointRouteBuilder MapInterestTopicPublicEndpoints(this IEndpointRouteBuilder app) + { + app.MapGet("/api/interest-topics", async ( + IMediator mediator, CancellationToken cancellationToken) => + { + var result = await mediator.Send(new ListInterestTopicsQuery(), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); + }) + .WithName("ListInterestTopicsPublic"); + + return app; + } +} diff --git a/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs index 817ecab0..5ef79f41 100644 --- a/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs @@ -24,7 +24,7 @@ public static IEndpointRouteBuilder MapUserInterestEndpoints(this IEndpointRoute if (userId == System.Guid.Empty) return Results.Unauthorized(); var result = await mediator.Send( - new UpsertUserInterestCommand(userId, body.Interests), ct).ConfigureAwait(false); + new UpsertUserInterestCommand(userId, body.InterestTopicIds ?? System.Array.Empty()), ct).ConfigureAwait(false); return result.ToHttpResult(); }) .WithName("UpsertUserInterest"); @@ -33,4 +33,4 @@ public static IEndpointRouteBuilder MapUserInterestEndpoints(this IEndpointRoute } } -public sealed record UpsertUserInterestRequest(IReadOnlyList Interests); +public sealed record UpsertUserInterestRequest(IReadOnlyList InterestTopicIds); diff --git a/backend/src/CCE.Api.External/Program.cs b/backend/src/CCE.Api.External/Program.cs index 3ebb9b7e..c207664c 100644 --- a/backend/src/CCE.Api.External/Program.cs +++ b/backend/src/CCE.Api.External/Program.cs @@ -104,6 +104,7 @@ app.MapKapsarcEndpoints(); app.MapSurveysEndpoints(); app.MapUserInterestEndpoints(); +app.MapInterestTopicPublicEndpoints(); app.MapGet("/health", async (IMediator mediator) => { diff --git a/backend/src/CCE.Api.Internal/Endpoints/InterestTopicEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/InterestTopicEndpoints.cs new file mode 100644 index 00000000..80ac9c7f --- /dev/null +++ b/backend/src/CCE.Api.Internal/Endpoints/InterestTopicEndpoints.cs @@ -0,0 +1,78 @@ +using CCE.Api.Common.Extensions; +using CCE.Application.InterestManagement.Commands.CreateInterestTopic; +using CCE.Application.InterestManagement.Commands.DeleteInterestTopic; +using CCE.Application.InterestManagement.Commands.UpdateInterestTopic; +using CCE.Application.InterestManagement.Queries.GetInterestTopicById; +using CCE.Application.InterestManagement.Queries.ListInterestTopics; +using CCE.Domain; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace CCE.Api.Internal.Endpoints; + +public static class InterestTopicEndpoints +{ + public static IEndpointRouteBuilder MapInterestTopicEndpoints(this IEndpointRouteBuilder app) + { + var topics = app.MapGroup("/api/admin/interest-topics").WithTags("Interest Topics"); + + topics.MapGet("", async ( + IMediator mediator, CancellationToken cancellationToken) => + { + var result = await mediator.Send(new ListInterestTopicsQuery(), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.InterestTopic_Manage) + .WithName("ListInterestTopics"); + + topics.MapGet("/{id:guid}", async ( + System.Guid id, + IMediator mediator, CancellationToken cancellationToken) => + { + var result = await mediator.Send(new GetInterestTopicByIdQuery(id), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.InterestTopic_Manage) + .WithName("GetInterestTopicById"); + + topics.MapPost("", async ( + CreateInterestTopicRequest body, + IMediator mediator, CancellationToken cancellationToken) => + { + var result = await mediator.Send( + new CreateInterestTopicCommand(body.NameAr, body.NameEn), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.InterestTopic_Manage) + .WithName("CreateInterestTopic"); + + topics.MapPut("/{id:guid}", async ( + System.Guid id, + UpdateInterestTopicRequest body, + IMediator mediator, CancellationToken cancellationToken) => + { + var result = await mediator.Send( + new UpdateInterestTopicCommand(id, body.NameAr, body.NameEn), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.InterestTopic_Manage) + .WithName("UpdateInterestTopic"); + + topics.MapDelete("/{id:guid}", async ( + System.Guid id, + IMediator mediator, CancellationToken cancellationToken) => + { + var result = await mediator.Send(new DeleteInterestTopicCommand(id), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.InterestTopic_Manage) + .WithName("DeleteInterestTopic"); + + return app; + } +} + +public sealed record CreateInterestTopicRequest(string NameAr, string NameEn); +public sealed record UpdateInterestTopicRequest(string NameAr, string NameEn); diff --git a/backend/src/CCE.Api.Internal/Program.cs b/backend/src/CCE.Api.Internal/Program.cs index 159a1a42..88ab57b5 100644 --- a/backend/src/CCE.Api.Internal/Program.cs +++ b/backend/src/CCE.Api.Internal/Program.cs @@ -75,6 +75,7 @@ app.MapTopicEndpoints(); app.MapCommunityModerationEndpoints(); app.MapNotificationTemplateEndpoints(); +app.MapInterestTopicEndpoints(); app.MapReportEndpoints(); app.MapAuditEndpoints(); diff --git a/backend/src/CCE.Application/Common/Interfaces/ICceDbContext.cs b/backend/src/CCE.Application/Common/Interfaces/ICceDbContext.cs index 08baabcf..b9f9dce6 100644 --- a/backend/src/CCE.Application/Common/Interfaces/ICceDbContext.cs +++ b/backend/src/CCE.Application/Common/Interfaces/ICceDbContext.cs @@ -58,6 +58,7 @@ public interface ICceDbContext IQueryable CityScenarios { get; } IQueryable CityTechnologies { get; } IQueryable CityScenarioResults { get; } + IQueryable InterestTopics { get; } Task SaveChangesAsync(CancellationToken cancellationToken = default); } diff --git a/backend/src/CCE.Application/Identity/Dtos/UserDetailDto.cs b/backend/src/CCE.Application/Identity/Dtos/UserDetailDto.cs index 9a400931..f0db1c16 100644 --- a/backend/src/CCE.Application/Identity/Dtos/UserDetailDto.cs +++ b/backend/src/CCE.Application/Identity/Dtos/UserDetailDto.cs @@ -1,3 +1,4 @@ +using CCE.Application.InterestManagement.Dtos; using CCE.Domain.Identity; namespace CCE.Application.Identity.Dtos; @@ -11,7 +12,7 @@ public sealed record UserDetailDto( string? UserName, string LocalePreference, KnowledgeLevel KnowledgeLevel, - IReadOnlyList Interests, + IReadOnlyList InterestTopics, System.Guid? CountryId, string? AvatarUrl, IReadOnlyList Roles, diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommand.cs b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommand.cs index 542635c0..8596db2e 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommand.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommand.cs @@ -9,6 +9,5 @@ public sealed record UpdateMyProfileCommand( System.Guid UserId, string LocalePreference, KnowledgeLevel KnowledgeLevel, - IReadOnlyList Interests, string? AvatarUrl, System.Guid? CountryId) : IRequest>; diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandHandler.cs index 9d75a3f0..9494aabc 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandHandler.cs @@ -1,6 +1,7 @@ using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Identity.Public.Dtos; +using CCE.Application.InterestManagement.Dtos; using CCE.Application.Messages; using MediatR; @@ -29,7 +30,6 @@ public async Task> Handle(UpdateMyProfileCommand reques user.SetLocalePreference(request.LocalePreference); user.SetKnowledgeLevel(request.KnowledgeLevel); - user.UpdateInterests(request.Interests); user.SetAvatarUrl(request.AvatarUrl); if (request.CountryId is null) @@ -44,13 +44,21 @@ public async Task> Handle(UpdateMyProfileCommand reques _service.Update(user); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + var interestTopics = user.UserInterestTopics + .Select(uit => new InterestTopicDto( + uit.InterestTopic.Id, + uit.InterestTopic.NameAr, + uit.InterestTopic.NameEn, + uit.InterestTopic.IsActive)) + .ToList(); + return _msg.Ok(new UserProfileDto( user.Id, user.Email, user.UserName, user.LocalePreference, user.KnowledgeLevel, - user.Interests, + interestTopics, user.CountryId, user.AvatarUrl), "PROFILE_UPDATED"); } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandValidator.cs b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandValidator.cs index 4fa41f15..c8c081ef 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandValidator.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandValidator.cs @@ -11,8 +11,6 @@ public UpdateMyProfileCommandValidator() .Must(l => l == "ar" || l == "en") .WithMessage("LocalePreference must be 'ar' or 'en'."); - RuleFor(x => x.Interests).NotNull(); - RuleFor(x => x.AvatarUrl) .Must(url => url is null || url.StartsWith("https://", System.StringComparison.OrdinalIgnoreCase)) .WithMessage("AvatarUrl must be null or start with 'https://'."); diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileRequest.cs b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileRequest.cs index b7d47780..fde00b9a 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileRequest.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileRequest.cs @@ -3,6 +3,5 @@ namespace CCE.Application.Identity.Public.Commands.UpdateMyProfile; public sealed record UpdateMyProfileRequest( string LocalePreference, Domain.Identity.KnowledgeLevel KnowledgeLevel, - IReadOnlyList? Interests, string? AvatarUrl, System.Guid? CountryId); diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommand.cs b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommand.cs index 10d74bba..cfcff952 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommand.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommand.cs @@ -5,4 +5,4 @@ namespace CCE.Application.Identity.Public.Commands.UserInterest; public sealed record UpsertUserInterestCommand( System.Guid UserId, - IReadOnlyList Interests) : IRequest>; + IReadOnlyList InterestTopicIds) : IRequest>; diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs index 6acd7348..25f26511 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs @@ -1,8 +1,10 @@ -using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; +using CCE.Application.InterestManagement.Dtos; using CCE.Application.Messages; +using CCE.Domain.Identity; using MediatR; +using Microsoft.EntityFrameworkCore; namespace CCE.Application.Identity.Public.Commands.UserInterest; @@ -31,37 +33,49 @@ public async Task> Handle( if (user is null) return _msg.UserNotFound(); - var oldInterests = user.Interests.ToList(); - var rawList = request.Interests ?? System.Array.Empty(); + var newIds = (request.InterestTopicIds ?? System.Array.Empty()) + .Distinct() + .ToHashSet(); - var normalizedNew = rawList - .Select(static s => s?.Trim() ?? string.Empty) - .Where(static s => s.Length > 0) - .Distinct(StringComparer.OrdinalIgnoreCase) + var oldIds = user.UserInterestTopics + .Select(uit => uit.InterestTopicId) + .ToHashSet(); + + var toRemove = user.UserInterestTopics + .Where(uit => !newIds.Contains(uit.InterestTopicId)) .ToList(); - var oldSet = new HashSet(oldInterests, StringComparer.OrdinalIgnoreCase); - var newSet = new HashSet(normalizedNew, StringComparer.OrdinalIgnoreCase); + var toAddIds = newIds + .Where(id => !oldIds.Contains(id)) + .ToList(); - if (oldSet.SetEquals(newSet)) - { - return _msg.InterestUpserted(new UpsertUserInterestResult( - user.Interests, - System.Array.Empty(), - System.Array.Empty())); - } + foreach (var remove in toRemove) + user.UserInterestTopics.Remove(remove); - user.UpdateInterests(normalizedNew); + foreach (var id in toAddIds) + user.UserInterestTopics.Add(new UserInterestTopic + { + UserId = user.Id, + InterestTopicId = id + }); - var added = normalizedNew.Except(oldInterests, StringComparer.OrdinalIgnoreCase).ToList(); - var removed = oldInterests.Except(normalizedNew, StringComparer.OrdinalIgnoreCase).ToList(); + if (toRemove.Count > 0 || toAddIds.Count > 0) + { + _service.Update(user); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } - _service.Update(user); - await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + var currentTopicIds = user.UserInterestTopics + .Select(uit => uit.InterestTopicId) + .ToHashSet(); + var currentTopics = await _db.InterestTopics + .Where(t => currentTopicIds.Contains(t.Id)) + .Select(t => new InterestTopicDto(t.Id, t.NameAr, t.NameEn, t.IsActive)) + .ToListAsync(cancellationToken); return _msg.InterestUpserted(new UpsertUserInterestResult( - user.Interests, - added, - removed)); + currentTopics, + toAddIds, + toRemove.Select(r => r.InterestTopicId).ToList())); } } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestResult.cs b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestResult.cs index 8e553105..48fed008 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestResult.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestResult.cs @@ -1,6 +1,8 @@ +using CCE.Application.InterestManagement.Dtos; + namespace CCE.Application.Identity.Public.Commands.UserInterest; public sealed record UpsertUserInterestResult( - IReadOnlyList Interests, - IReadOnlyList Added, - IReadOnlyList Removed); + IReadOnlyList InterestTopics, + IReadOnlyList Added, + IReadOnlyList Removed); diff --git a/backend/src/CCE.Application/Identity/Public/Dtos/UserProfileDto.cs b/backend/src/CCE.Application/Identity/Public/Dtos/UserProfileDto.cs index e8b8685e..961f1686 100644 --- a/backend/src/CCE.Application/Identity/Public/Dtos/UserProfileDto.cs +++ b/backend/src/CCE.Application/Identity/Public/Dtos/UserProfileDto.cs @@ -1,3 +1,4 @@ +using CCE.Application.InterestManagement.Dtos; using CCE.Domain.Identity; namespace CCE.Application.Identity.Public.Dtos; @@ -8,6 +9,6 @@ public sealed record UserProfileDto( string? UserName, string LocalePreference, KnowledgeLevel KnowledgeLevel, - IReadOnlyList Interests, + IReadOnlyList InterestTopics, System.Guid? CountryId, string? AvatarUrl); diff --git a/backend/src/CCE.Application/Identity/Public/Queries/GetMyProfile/GetMyProfileQueryHandler.cs b/backend/src/CCE.Application/Identity/Public/Queries/GetMyProfile/GetMyProfileQueryHandler.cs index 7fa15a57..485f5868 100644 --- a/backend/src/CCE.Application/Identity/Public/Queries/GetMyProfile/GetMyProfileQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Queries/GetMyProfile/GetMyProfileQueryHandler.cs @@ -1,5 +1,6 @@ using CCE.Application.Common; using CCE.Application.Identity.Public.Dtos; +using CCE.Application.InterestManagement.Dtos; using CCE.Application.Messages; using MediatR; @@ -24,13 +25,21 @@ public async Task> Handle(GetMyProfileQuery request, Ca return _msg.UserNotFound(); } + var interestTopics = user.UserInterestTopics + .Select(uit => new InterestTopicDto( + uit.InterestTopic.Id, + uit.InterestTopic.NameAr, + uit.InterestTopic.NameEn, + uit.InterestTopic.IsActive)) + .ToList(); + return _msg.Ok(new UserProfileDto( user.Id, user.Email, user.UserName, user.LocalePreference, user.KnowledgeLevel, - user.Interests, + interestTopics, user.CountryId, user.AvatarUrl), "SUCCESS_OPERATION"); } diff --git a/backend/src/CCE.Application/Identity/Queries/GetUserById/GetUserByIdQueryHandler.cs b/backend/src/CCE.Application/Identity/Queries/GetUserById/GetUserByIdQueryHandler.cs index 8435576d..cac83d35 100644 --- a/backend/src/CCE.Application/Identity/Queries/GetUserById/GetUserByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Queries/GetUserById/GetUserByIdQueryHandler.cs @@ -1,9 +1,12 @@ +using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Dtos; +using CCE.Application.InterestManagement.Dtos; using CCE.Application.Messages; using MediatR; +using Microsoft.EntityFrameworkCore; namespace CCE.Application.Identity.Queries.GetUserById; @@ -20,8 +23,13 @@ public GetUserByIdQueryHandler(ICceDbContext db, MessageFactory msg) public async Task> Handle(GetUserByIdQuery request, CancellationToken cancellationToken) { - var user = (await _db.Users.Where(u => u.Id == request.Id).ToListAsyncEither(cancellationToken).ConfigureAwait(false)) - .SingleOrDefault(); + var users = await _db.Users + .Where(u => u.Id == request.Id) + .Include(u => u.UserInterestTopics) + .ThenInclude(uit => uit.InterestTopic) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var user = users.SingleOrDefault(); if (user is null) { return _msg.UserNotFound(); @@ -37,13 +45,21 @@ join r in _db.Roles on ur.RoleId equals r.Id var now = DateTimeOffset.UtcNow; var isActive = !user.LockoutEnabled || user.LockoutEnd is null || user.LockoutEnd < now; + var interestTopics = user.UserInterestTopics + .Select(uit => new InterestTopicDto( + uit.InterestTopic.Id, + uit.InterestTopic.NameAr, + uit.InterestTopic.NameEn, + uit.InterestTopic.IsActive)) + .ToList(); + return _msg.Ok(new UserDetailDto( user.Id, user.Email, user.UserName, user.LocalePreference, user.KnowledgeLevel, - user.Interests, + interestTopics, user.CountryId, user.AvatarUrl, roles, diff --git a/backend/src/CCE.Application/InterestManagement/Commands/CreateInterestTopic/CreateInterestTopicCommand.cs b/backend/src/CCE.Application/InterestManagement/Commands/CreateInterestTopic/CreateInterestTopicCommand.cs new file mode 100644 index 00000000..c3f72509 --- /dev/null +++ b/backend/src/CCE.Application/InterestManagement/Commands/CreateInterestTopic/CreateInterestTopicCommand.cs @@ -0,0 +1,9 @@ +using CCE.Application.Common; +using CCE.Application.InterestManagement.Dtos; +using MediatR; + +namespace CCE.Application.InterestManagement.Commands.CreateInterestTopic; + +public sealed record CreateInterestTopicCommand( + string NameAr, + string NameEn) : IRequest>; diff --git a/backend/src/CCE.Application/InterestManagement/Commands/CreateInterestTopic/CreateInterestTopicCommandHandler.cs b/backend/src/CCE.Application/InterestManagement/Commands/CreateInterestTopic/CreateInterestTopicCommandHandler.cs new file mode 100644 index 00000000..ef07c91e --- /dev/null +++ b/backend/src/CCE.Application/InterestManagement/Commands/CreateInterestTopic/CreateInterestTopicCommandHandler.cs @@ -0,0 +1,28 @@ +using CCE.Application.Common; +using CCE.Application.InterestManagement.Dtos; +using CCE.Application.Messages; +using CCE.Domain.Identity; +using MediatR; + +namespace CCE.Application.InterestManagement.Commands.CreateInterestTopic; + +public sealed class CreateInterestTopicCommandHandler + : IRequestHandler> +{ + private readonly IInterestTopicRepository _repo; + private readonly MessageFactory _msg; + + public CreateInterestTopicCommandHandler(IInterestTopicRepository repo, MessageFactory msg) + { + _repo = repo; + _msg = msg; + } + + public async Task> Handle( + CreateInterestTopicCommand request, CancellationToken cancellationToken) + { + var topic = InterestTopic.Create(request.NameAr, request.NameEn); + await _repo.AddAsync(topic, cancellationToken).ConfigureAwait(false); + return _msg.Ok(new InterestTopicDto(topic.Id, topic.NameAr, topic.NameEn, topic.IsActive), "INTEREST_TOPIC_CREATED"); + } +} diff --git a/backend/src/CCE.Application/InterestManagement/Commands/DeleteInterestTopic/DeleteInterestTopicCommand.cs b/backend/src/CCE.Application/InterestManagement/Commands/DeleteInterestTopic/DeleteInterestTopicCommand.cs new file mode 100644 index 00000000..8facda5f --- /dev/null +++ b/backend/src/CCE.Application/InterestManagement/Commands/DeleteInterestTopic/DeleteInterestTopicCommand.cs @@ -0,0 +1,6 @@ +using CCE.Application.Common; +using MediatR; + +namespace CCE.Application.InterestManagement.Commands.DeleteInterestTopic; + +public sealed record DeleteInterestTopicCommand(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/InterestManagement/Commands/DeleteInterestTopic/DeleteInterestTopicCommandHandler.cs b/backend/src/CCE.Application/InterestManagement/Commands/DeleteInterestTopic/DeleteInterestTopicCommandHandler.cs new file mode 100644 index 00000000..548d50a9 --- /dev/null +++ b/backend/src/CCE.Application/InterestManagement/Commands/DeleteInterestTopic/DeleteInterestTopicCommandHandler.cs @@ -0,0 +1,29 @@ +using CCE.Application.Common; +using CCE.Application.Messages; +using MediatR; + +namespace CCE.Application.InterestManagement.Commands.DeleteInterestTopic; + +public sealed class DeleteInterestTopicCommandHandler + : IRequestHandler> +{ + private readonly IInterestTopicRepository _repo; + private readonly MessageFactory _msg; + + public DeleteInterestTopicCommandHandler(IInterestTopicRepository repo, MessageFactory msg) + { + _repo = repo; + _msg = msg; + } + + public async Task> Handle( + DeleteInterestTopicCommand request, CancellationToken cancellationToken) + { + var topic = await _repo.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); + if (topic is null) + return _msg.NotFound("INTEREST_TOPIC_NOT_FOUND"); + + await _repo.Delete(topic).ConfigureAwait(false); + return _msg.Ok("INTEREST_TOPIC_DELETED"); + } +} diff --git a/backend/src/CCE.Application/InterestManagement/Commands/UpdateInterestTopic/UpdateInterestTopicCommand.cs b/backend/src/CCE.Application/InterestManagement/Commands/UpdateInterestTopic/UpdateInterestTopicCommand.cs new file mode 100644 index 00000000..b79193c2 --- /dev/null +++ b/backend/src/CCE.Application/InterestManagement/Commands/UpdateInterestTopic/UpdateInterestTopicCommand.cs @@ -0,0 +1,10 @@ +using CCE.Application.Common; +using CCE.Application.InterestManagement.Dtos; +using MediatR; + +namespace CCE.Application.InterestManagement.Commands.UpdateInterestTopic; + +public sealed record UpdateInterestTopicCommand( + System.Guid Id, + string NameAr, + string NameEn) : IRequest>; diff --git a/backend/src/CCE.Application/InterestManagement/Commands/UpdateInterestTopic/UpdateInterestTopicCommandHandler.cs b/backend/src/CCE.Application/InterestManagement/Commands/UpdateInterestTopic/UpdateInterestTopicCommandHandler.cs new file mode 100644 index 00000000..70f2d618 --- /dev/null +++ b/backend/src/CCE.Application/InterestManagement/Commands/UpdateInterestTopic/UpdateInterestTopicCommandHandler.cs @@ -0,0 +1,31 @@ +using CCE.Application.Common; +using CCE.Application.InterestManagement.Dtos; +using CCE.Application.Messages; +using MediatR; + +namespace CCE.Application.InterestManagement.Commands.UpdateInterestTopic; + +public sealed class UpdateInterestTopicCommandHandler + : IRequestHandler> +{ + private readonly IInterestTopicRepository _repo; + private readonly MessageFactory _msg; + + public UpdateInterestTopicCommandHandler(IInterestTopicRepository repo, MessageFactory msg) + { + _repo = repo; + _msg = msg; + } + + public async Task> Handle( + UpdateInterestTopicCommand request, CancellationToken cancellationToken) + { + var topic = await _repo.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); + if (topic is null) + return _msg.InterestTopicNotFound(); + + topic.UpdateNames(request.NameAr, request.NameEn); + await _repo.Update(topic).ConfigureAwait(false); + return _msg.Ok(new InterestTopicDto(topic.Id, topic.NameAr, topic.NameEn, topic.IsActive), "INTEREST_TOPIC_UPDATED"); + } +} diff --git a/backend/src/CCE.Application/InterestManagement/Dtos/InterestTopicDto.cs b/backend/src/CCE.Application/InterestManagement/Dtos/InterestTopicDto.cs new file mode 100644 index 00000000..f6805e74 --- /dev/null +++ b/backend/src/CCE.Application/InterestManagement/Dtos/InterestTopicDto.cs @@ -0,0 +1,7 @@ +namespace CCE.Application.InterestManagement.Dtos; + +public sealed record InterestTopicDto( + System.Guid Id, + string NameAr, + string NameEn, + bool IsActive); diff --git a/backend/src/CCE.Application/InterestManagement/IInterestTopicRepository.cs b/backend/src/CCE.Application/InterestManagement/IInterestTopicRepository.cs new file mode 100644 index 00000000..206b0a65 --- /dev/null +++ b/backend/src/CCE.Application/InterestManagement/IInterestTopicRepository.cs @@ -0,0 +1,11 @@ +using CCE.Domain.Identity; + +namespace CCE.Application.InterestManagement; + +public interface IInterestTopicRepository +{ + Task AddAsync(InterestTopic topic, CancellationToken ct); + Task FindAsync(System.Guid id, CancellationToken ct); + Task Update(InterestTopic topic); + Task Delete(InterestTopic topic); +} diff --git a/backend/src/CCE.Application/InterestManagement/Queries/GetInterestTopicById/GetInterestTopicByIdQuery.cs b/backend/src/CCE.Application/InterestManagement/Queries/GetInterestTopicById/GetInterestTopicByIdQuery.cs new file mode 100644 index 00000000..bb41b807 --- /dev/null +++ b/backend/src/CCE.Application/InterestManagement/Queries/GetInterestTopicById/GetInterestTopicByIdQuery.cs @@ -0,0 +1,7 @@ +using CCE.Application.Common; +using CCE.Application.InterestManagement.Dtos; +using MediatR; + +namespace CCE.Application.InterestManagement.Queries.GetInterestTopicById; + +public sealed record GetInterestTopicByIdQuery(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/InterestManagement/Queries/GetInterestTopicById/GetInterestTopicByIdQueryHandler.cs b/backend/src/CCE.Application/InterestManagement/Queries/GetInterestTopicById/GetInterestTopicByIdQueryHandler.cs new file mode 100644 index 00000000..61f9ff21 --- /dev/null +++ b/backend/src/CCE.Application/InterestManagement/Queries/GetInterestTopicById/GetInterestTopicByIdQueryHandler.cs @@ -0,0 +1,36 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; +using CCE.Application.InterestManagement.Dtos; +using CCE.Application.Messages; +using MediatR; + +namespace CCE.Application.InterestManagement.Queries.GetInterestTopicById; + +public sealed class GetInterestTopicByIdQueryHandler + : IRequestHandler> +{ + private readonly ICceDbContext _db; + private readonly MessageFactory _msg; + + public GetInterestTopicByIdQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } + + public async Task> Handle( + GetInterestTopicByIdQuery request, CancellationToken cancellationToken) + { + var topics = await _db.InterestTopics + .Where(t => t.Id == request.Id) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var topic = topics.SingleOrDefault(); + + if (topic is null) + return _msg.NotFound("INTEREST_TOPIC_NOT_FOUND"); + + return _msg.Ok(new InterestTopicDto(topic.Id, topic.NameAr, topic.NameEn, topic.IsActive), "SUCCESS_OPERATION"); + } +} diff --git a/backend/src/CCE.Application/InterestManagement/Queries/ListInterestTopics/ListInterestTopicsQuery.cs b/backend/src/CCE.Application/InterestManagement/Queries/ListInterestTopics/ListInterestTopicsQuery.cs new file mode 100644 index 00000000..ce420fc0 --- /dev/null +++ b/backend/src/CCE.Application/InterestManagement/Queries/ListInterestTopics/ListInterestTopicsQuery.cs @@ -0,0 +1,7 @@ +using CCE.Application.Common; +using CCE.Application.InterestManagement.Dtos; +using MediatR; + +namespace CCE.Application.InterestManagement.Queries.ListInterestTopics; + +public sealed record ListInterestTopicsQuery : IRequest>>; \ No newline at end of file diff --git a/backend/src/CCE.Application/InterestManagement/Queries/ListInterestTopics/ListInterestTopicsQueryHandler.cs b/backend/src/CCE.Application/InterestManagement/Queries/ListInterestTopics/ListInterestTopicsQueryHandler.cs new file mode 100644 index 00000000..264afd22 --- /dev/null +++ b/backend/src/CCE.Application/InterestManagement/Queries/ListInterestTopics/ListInterestTopicsQueryHandler.cs @@ -0,0 +1,33 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; +using CCE.Application.InterestManagement.Dtos; +using CCE.Application.Messages; +using MediatR; + +namespace CCE.Application.InterestManagement.Queries.ListInterestTopics; + +public sealed class ListInterestTopicsQueryHandler + : IRequestHandler>> +{ + private readonly ICceDbContext _db; + private readonly MessageFactory _msg; + + public ListInterestTopicsQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } + + public async Task>> Handle( + ListInterestTopicsQuery request, CancellationToken cancellationToken) + { + var topics = await _db.InterestTopics + .OrderBy(t => t.NameEn) + .Select(t => new InterestTopicDto(t.Id, t.NameAr, t.NameEn, t.IsActive)) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + + return _msg.Ok>(topics, "SUCCESS_OPERATION"); + } +} diff --git a/backend/src/CCE.Application/Messages/MessageFactory.cs b/backend/src/CCE.Application/Messages/MessageFactory.cs index b1742ced..6b194e35 100644 --- a/backend/src/CCE.Application/Messages/MessageFactory.cs +++ b/backend/src/CCE.Application/Messages/MessageFactory.cs @@ -77,6 +77,7 @@ public FieldError Field(string fieldName, string domainKey) public Response NewsNotFound() => NotFound("NEWS_NOT_FOUND"); public Response EventNotFound() => NotFound("EVENT_NOT_FOUND"); + public Response InterestTopicNotFound() => NotFound("INTEREST_TOPIC_NOT_FOUND"); public Response PageNotFound() => NotFound("PAGE_NOT_FOUND"); public Response CategoryNotFound() => NotFound("CATEGORY_NOT_FOUND"); diff --git a/backend/src/CCE.Application/Messages/SystemCode.cs b/backend/src/CCE.Application/Messages/SystemCode.cs index da99ec15..4e36381a 100644 --- a/backend/src/CCE.Application/Messages/SystemCode.cs +++ b/backend/src/CCE.Application/Messages/SystemCode.cs @@ -21,7 +21,6 @@ public static class SystemCode public const string ERR002 = "ERR002"; // Resource download failure (appendix) public const string ERR003 = "ERR003"; // Resource share failure (appendix) - public const string ERR018 = "ERR018"; // Interest not found public const string ERR019 = "ERR019"; // Email already exists / Account creation failure (appendix) public const string ERR020 = "ERR020"; // Invalid credentials (appendix) public const string ERR021 = "ERR021"; // Login system error (appendix) @@ -91,6 +90,9 @@ public static class SystemCode public const string ERR100 = "ERR100"; // Scenario not found public const string ERR101 = "ERR101"; // Technology not found + // ─── InterestTopic Errors ─── + public const string ERR110 = "ERR110"; // Interest topic not found + // ─── General Errors ─── public const string ERR900 = "ERR900"; // Internal server error public const string ERR901 = "ERR901"; // Unauthorized access @@ -133,6 +135,11 @@ public static class SystemCode public const string CON053 = "CON053"; // State rep assignment revoked public const string CON054 = "CON054"; // Roles assigned + // ─── InterestTopic Success ─── + public const string CON055 = "CON055"; // Interest topic created + public const string CON056 = "CON056"; // Interest topic updated + public const string CON057 = "CON057"; // Interest topic deleted + // ─── Content Success ─── public const string CON020 = "CON020"; // Content created public const string CON021 = "CON021"; // Content updated diff --git a/backend/src/CCE.Application/Messages/SystemCodeMap.cs b/backend/src/CCE.Application/Messages/SystemCodeMap.cs index c5f4d60b..0175c069 100644 --- a/backend/src/CCE.Application/Messages/SystemCodeMap.cs +++ b/backend/src/CCE.Application/Messages/SystemCodeMap.cs @@ -14,7 +14,6 @@ public static class SystemCodeMap ["INVALID_CREDENTIALS"] = SystemCode.ERR020, ["PASSWORD_RECOVERY_FAILED"] = SystemCode.ERR023, ["LOGOUT_FAILED"] = SystemCode.ERR024, - ["INTEREST_NOT_FOUND"] = SystemCode.ERR018, // ─── Backend-only Identity Errors (moved to free appendix numbers) ─── ["EXPERT_REQUEST_NOT_FOUND"] = SystemCode.ERR400, @@ -72,6 +71,9 @@ public static class SystemCodeMap ["SCENARIO_NOT_FOUND"] = SystemCode.ERR100, ["TECHNOLOGY_NOT_FOUND"] = SystemCode.ERR101, + // ─── InterestTopic Errors ─── + ["INTEREST_TOPIC_NOT_FOUND"] = SystemCode.ERR110, + // ─── General Errors ─── ["INTERNAL_ERROR"] = SystemCode.ERR900, ["UNAUTHORIZED_ACCESS"] = SystemCode.ERR901, @@ -100,6 +102,11 @@ public static class SystemCodeMap ["STATE_REP_ASSIGNMENT_REVOKED"] = SystemCode.CON053, ["ROLES_ASSIGNED"] = SystemCode.CON054, + // ─── InterestTopic Success ─── + ["INTEREST_TOPIC_CREATED"] = SystemCode.CON055, + ["INTEREST_TOPIC_UPDATED"] = SystemCode.CON056, + ["INTEREST_TOPIC_DELETED"] = SystemCode.CON057, + // ─── Content Success ─── ["CONTENT_CREATED"] = SystemCode.CON020, ["CONTENT_UPDATED"] = SystemCode.CON021, diff --git a/backend/src/CCE.Domain/Identity/InterestTopic.cs b/backend/src/CCE.Domain/Identity/InterestTopic.cs new file mode 100644 index 00000000..da9538aa --- /dev/null +++ b/backend/src/CCE.Domain/Identity/InterestTopic.cs @@ -0,0 +1,44 @@ +using CCE.Domain.Common; + +namespace CCE.Domain.Identity; + +public sealed class InterestTopic : Entity +{ + private InterestTopic(System.Guid id, string nameAr, string nameEn) : base(id) + { + NameAr = nameAr; + NameEn = nameEn; + IsActive = true; + } + + public string NameAr { get; private set; } + + public string NameEn { get; private set; } + + public bool IsActive { get; private set; } + + public static InterestTopic Create(string nameAr, string nameEn) + { + if (string.IsNullOrWhiteSpace(nameAr)) + throw new DomainException("NameAr is required."); + if (string.IsNullOrWhiteSpace(nameEn)) + throw new DomainException("NameEn is required."); + + return new InterestTopic(System.Guid.NewGuid(), nameAr.Trim(), nameEn.Trim()); + } + + public void UpdateNames(string nameAr, string nameEn) + { + if (string.IsNullOrWhiteSpace(nameAr)) + throw new DomainException("NameAr is required."); + if (string.IsNullOrWhiteSpace(nameEn)) + throw new DomainException("NameEn is required."); + + NameAr = nameAr.Trim(); + NameEn = nameEn.Trim(); + } + + public void Deactivate() => IsActive = false; + + public void Activate() => IsActive = true; +} diff --git a/backend/src/CCE.Domain/Identity/User.cs b/backend/src/CCE.Domain/Identity/User.cs index 5cdd1e0d..b3a7be25 100644 --- a/backend/src/CCE.Domain/Identity/User.cs +++ b/backend/src/CCE.Domain/Identity/User.cs @@ -25,8 +25,7 @@ public class User : IdentityUser /// Self-declared knowledge level. Default . public KnowledgeLevel KnowledgeLevel { get; private set; } = KnowledgeLevel.Beginner; - /// User-selected topic interests (free-text PascalCase tags). EF maps as JSON column. - public List Interests { get; private set; } = new(); + public ICollection UserInterestTopics { get; private set; } = new List(); /// Optional user country (FK to Country); only set for state-rep / community users with a profile. public System.Guid? CountryId { get; set; } @@ -128,22 +127,6 @@ public void SetLocalePreference(string locale) public void SetKnowledgeLevel(KnowledgeLevel level) => KnowledgeLevel = level; - /// - /// Replaces the interests list. Trims whitespace, deduplicates, and removes empty entries. - /// - public void UpdateInterests(IEnumerable interests) - { - if (interests is null) - { - throw new DomainException("interests collection cannot be null."); - } - Interests = interests - .Select(static s => s?.Trim() ?? string.Empty) - .Where(static s => s.Length > 0) - .Distinct() - .ToList(); - } - public void AssignCountry(System.Guid countryId) => CountryId = countryId; public void ClearCountry() => CountryId = null; diff --git a/backend/src/CCE.Domain/Identity/UserInterestTopic.cs b/backend/src/CCE.Domain/Identity/UserInterestTopic.cs new file mode 100644 index 00000000..207f9aa9 --- /dev/null +++ b/backend/src/CCE.Domain/Identity/UserInterestTopic.cs @@ -0,0 +1,12 @@ +namespace CCE.Domain.Identity; + +public sealed class UserInterestTopic +{ + public System.Guid UserId { get; init; } + + public User User { get; init; } = null!; + + public System.Guid InterestTopicId { get; init; } + + public InterestTopic InterestTopic { get; init; } = null!; +} diff --git a/backend/src/CCE.Infrastructure/DependencyInjection.cs b/backend/src/CCE.Infrastructure/DependencyInjection.cs index 145f86c7..96744ea5 100644 --- a/backend/src/CCE.Infrastructure/DependencyInjection.cs +++ b/backend/src/CCE.Infrastructure/DependencyInjection.cs @@ -1,6 +1,7 @@ using CCE.Application.Assistant; using CCE.Application.Common.CountryScope; using CCE.Application.Common.Interfaces; +using CCE.Application.InterestManagement; using CCE.Application.Common.Sanitization; using CCE.Application.Community; using CCE.Application.Content; @@ -19,6 +20,7 @@ using CCE.Infrastructure.Community; using CCE.Infrastructure.Content; using CCE.Infrastructure.InteractiveCity; +using CCE.Infrastructure.InterestManagement; using CCE.Infrastructure.Sanitization; using CCE.Infrastructure.Country; using CCE.Infrastructure.Notifications; @@ -184,6 +186,9 @@ public static IServiceCollection AddInfrastructure( // Interactive City services.AddScoped(); + // Interest Management + services.AddScoped(); + // Search services.AddScoped(); services.AddScoped(); diff --git a/backend/src/CCE.Infrastructure/Identity/UserProfileRepository.cs b/backend/src/CCE.Infrastructure/Identity/UserProfileRepository.cs index 8ceeb478..7d125b48 100644 --- a/backend/src/CCE.Infrastructure/Identity/UserProfileRepository.cs +++ b/backend/src/CCE.Infrastructure/Identity/UserProfileRepository.cs @@ -3,6 +3,7 @@ using CCE.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; + namespace CCE.Infrastructure.Identity; public sealed class UserProfileRepository : IUserProfileRepository @@ -15,7 +16,10 @@ public UserProfileRepository(CceDbContext db) } public async Task FindAsync(System.Guid userId, CancellationToken ct) - => await _db.Users.FirstOrDefaultAsync(u => u.Id == userId, ct).ConfigureAwait(false); + => await _db.Users + .Include(u => u.UserInterestTopics) + .ThenInclude(uit => uit.InterestTopic) + .FirstOrDefaultAsync(u => u.Id == userId, ct).ConfigureAwait(false); public void Update(User user) => _db.Users.Update(user); diff --git a/backend/src/CCE.Infrastructure/InterestManagement/InterestTopicRepository.cs b/backend/src/CCE.Infrastructure/InterestManagement/InterestTopicRepository.cs new file mode 100644 index 00000000..7e679405 --- /dev/null +++ b/backend/src/CCE.Infrastructure/InterestManagement/InterestTopicRepository.cs @@ -0,0 +1,34 @@ +using CCE.Application.InterestManagement; +using CCE.Domain.Identity; +using CCE.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace CCE.Infrastructure.InterestManagement; + +public sealed class InterestTopicRepository : IInterestTopicRepository +{ + private readonly CceDbContext _db; + + public InterestTopicRepository(CceDbContext db) => _db = db; + + public async Task AddAsync(InterestTopic topic, CancellationToken ct) + { + await _db.InterestTopics.AddAsync(topic, ct).ConfigureAwait(false); + await _db.SaveChangesAsync(ct).ConfigureAwait(false); + } + + public Task FindAsync(System.Guid id, CancellationToken ct) + => _db.InterestTopics.FirstOrDefaultAsync(t => t.Id == id, ct); + + public async Task Update(InterestTopic topic) + { + _db.InterestTopics.Update(topic); + await _db.SaveChangesAsync().ConfigureAwait(false); + } + + public async Task Delete(InterestTopic topic) + { + _db.InterestTopics.Remove(topic); + await _db.SaveChangesAsync().ConfigureAwait(false); + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/CceDbContext.cs b/backend/src/CCE.Infrastructure/Persistence/CceDbContext.cs index 7198d594..a128dff1 100644 --- a/backend/src/CCE.Infrastructure/Persistence/CceDbContext.cs +++ b/backend/src/CCE.Infrastructure/Persistence/CceDbContext.cs @@ -36,6 +36,8 @@ public CceDbContext(DbContextOptions options) : base(options) { } public DbSet ExpertProfiles => Set(); public DbSet ExpertRegistrationRequests => Set(); public DbSet RefreshTokens => Set(); + public DbSet InterestTopics => Set(); + public DbSet UserInterestTopics => Set(); // ─── Content ─── public DbSet AssetFiles => Set(); @@ -116,6 +118,7 @@ public CceDbContext(DbContextOptions options) : base(options) { } IQueryable ICceDbContext.KnowledgeMapEdges => KnowledgeMapEdges.AsNoTracking(); IQueryable ICceDbContext.KnowledgeMapAssociations => KnowledgeMapAssociations.AsNoTracking(); IQueryable ICceDbContext.CityScenarios => CityScenarios.AsNoTracking(); + IQueryable ICceDbContext.InterestTopics => InterestTopics.AsNoTracking(); IQueryable ICceDbContext.CityTechnologies => CityTechnologies.AsNoTracking(); IQueryable ICceDbContext.CityScenarioResults => CityScenarioResults.AsNoTracking(); diff --git a/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/InterestTopicConfiguration.cs b/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/InterestTopicConfiguration.cs new file mode 100644 index 00000000..1f5a9a60 --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/InterestTopicConfiguration.cs @@ -0,0 +1,16 @@ +using CCE.Domain.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CCE.Infrastructure.Persistence.Configurations.Identity; + +internal sealed class InterestTopicConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(t => t.Id); + builder.Property(t => t.Id).ValueGeneratedNever(); + builder.Property(t => t.NameAr).HasMaxLength(256).IsRequired(); + builder.Property(t => t.NameEn).HasMaxLength(256).IsRequired(); + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/UserConfiguration.cs b/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/UserConfiguration.cs index 05db4019..3f86ebe9 100644 --- a/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/UserConfiguration.cs +++ b/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/UserConfiguration.cs @@ -14,7 +14,6 @@ public void Configure(EntityTypeBuilder builder) builder.Property(u => u.OrganizationName).HasMaxLength(100).IsRequired(); builder.Property(u => u.LocalePreference).HasMaxLength(2).IsRequired(); builder.Property(u => u.AvatarUrl).HasMaxLength(2048); - builder.Property(u => u.Interests).HasColumnType("nvarchar(max)"); builder.Property(u => u.KnowledgeLevel).HasConversion(); builder.HasIndex(u => u.CountryId).HasDatabaseName("ix_users_country_id"); diff --git a/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/UserInterestTopicConfiguration.cs b/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/UserInterestTopicConfiguration.cs new file mode 100644 index 00000000..4122dfb1 --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Configurations/Identity/UserInterestTopicConfiguration.cs @@ -0,0 +1,23 @@ +using CCE.Domain.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CCE.Infrastructure.Persistence.Configurations.Identity; + +internal sealed class UserInterestTopicConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(uit => new { uit.UserId, uit.InterestTopicId }); + + builder.HasOne(uit => uit.User) + .WithMany(u => u.UserInterestTopics) + .HasForeignKey(uit => uit.UserId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(uit => uit.InterestTopic) + .WithMany() + .HasForeignKey(uit => uit.InterestTopicId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/20260515121258_StandardizeCountryProfileAudit.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260515121258_StandardizeCountryProfileAudit.cs index ff7cb93d..f4380600 100644 --- a/backend/src/CCE.Infrastructure/Persistence/Migrations/20260515121258_StandardizeCountryProfileAudit.cs +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260515121258_StandardizeCountryProfileAudit.cs @@ -11,15 +11,13 @@ public partial class StandardizeCountryProfileAudit : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.RenameColumn( - name: "last_updated_on", - table: "country_profiles", - newName: "created_on"); + migrationBuilder.Sql(@" +IF COL_LENGTH('[dbo].[country_profiles]', 'last_updated_on') IS NOT NULL + EXEC sp_rename N'[dbo].[country_profiles].[last_updated_on]', N'created_on', 'COLUMN';"); - migrationBuilder.RenameColumn( - name: "last_updated_by_id", - table: "country_profiles", - newName: "created_by_id"); + migrationBuilder.Sql(@" +IF COL_LENGTH('[dbo].[country_profiles]', 'last_updated_by_id') IS NOT NULL + EXEC sp_rename N'[dbo].[country_profiles].[last_updated_by_id]', N'created_by_id', 'COLUMN';"); migrationBuilder.AddColumn( name: "created_by_id", diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/20260518133355_AddInterestTopics.Designer.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260518133355_AddInterestTopics.Designer.cs new file mode 100644 index 00000000..6fa7d4d8 --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260518133355_AddInterestTopics.Designer.cs @@ -0,0 +1,2772 @@ +// +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("20260518133355_AddInterestTopics")] + partial class AddInterestTopics + { + /// + 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.InterestTopic", 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.HasKey("Id") + .HasName("pk_interest_topics"); + + b.ToTable("interest_topics", (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("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + 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.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("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("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.Identity.UserInterestTopic", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("InterestTopicId") + .HasColumnType("uniqueidentifier") + .HasColumnName("interest_topic_id"); + + b.HasKey("UserId", "InterestTopicId") + .HasName("pk_user_interest_topics"); + + b.HasIndex("InterestTopicId") + .HasDatabaseName("ix_user_interest_topics_interest_topic_id"); + + b.ToTable("user_interest_topics", (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.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") + .IsUnique() + .HasDatabaseName("ux_notification_template_code"); + + 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.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("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.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.Identity.UserInterestTopic", b => + { + b.HasOne("CCE.Domain.Identity.InterestTopic", "InterestTopic") + .WithMany() + .HasForeignKey("InterestTopicId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_interest_topics_interest_topics_interest_topic_id"); + + b.HasOne("CCE.Domain.Identity.User", "User") + .WithMany("UserInterestTopics") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_interest_topics_users_user_id"); + + b.Navigation("InterestTopic"); + + b.Navigation("User"); + }); + + 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.User", b => + { + b.Navigation("UserInterestTopics"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/20260518133355_AddInterestTopics.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260518133355_AddInterestTopics.cs new file mode 100644 index 00000000..c0d071d2 --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260518133355_AddInterestTopics.cs @@ -0,0 +1,79 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CCE.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddInterestTopics : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "interests", + table: "AspNetUsers"); + + migrationBuilder.CreateTable( + name: "interest_topics", + columns: table => new + { + id = table.Column(type: "uniqueidentifier", nullable: false), + name_ar = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + name_en = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + is_active = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_interest_topics", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "user_interest_topics", + columns: table => new + { + user_id = table.Column(type: "uniqueidentifier", nullable: false), + interest_topic_id = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_user_interest_topics", x => new { x.user_id, x.interest_topic_id }); + table.ForeignKey( + name: "fk_user_interest_topics_interest_topics_interest_topic_id", + column: x => x.interest_topic_id, + principalTable: "interest_topics", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_user_interest_topics_users_user_id", + column: x => x.user_id, + principalTable: "AspNetUsers", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_user_interest_topics_interest_topic_id", + table: "user_interest_topics", + column: "interest_topic_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "user_interest_topics"); + + migrationBuilder.DropTable( + name: "interest_topics"); + + migrationBuilder.AddColumn( + name: "interests", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/CceDbContextModelSnapshot.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/CceDbContextModelSnapshot.cs index 8f671e33..d7b687eb 100644 --- a/backend/src/CCE.Infrastructure/Persistence/Migrations/CceDbContextModelSnapshot.cs +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/CceDbContextModelSnapshot.cs @@ -95,6 +95,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)") .HasColumnName("content"); + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + b.Property("CreatedOn") .HasColumnType("datetimeoffset") .HasColumnName("created_on"); @@ -115,6 +119,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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) @@ -213,6 +225,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)") .HasColumnName("content"); + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + b.Property("CreatedOn") .HasColumnType("datetimeoffset") .HasColumnName("created_on"); @@ -233,6 +249,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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) @@ -265,6 +289,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -296,6 +328,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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) @@ -448,6 +488,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -485,6 +533,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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)") @@ -552,6 +608,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -568,6 +632,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -605,6 +677,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -626,6 +706,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -685,6 +773,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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) @@ -695,6 +799,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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) @@ -734,6 +850,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -746,6 +870,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -804,6 +936,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -826,6 +966,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -931,6 +1079,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -965,6 +1121,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -1071,6 +1235,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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)") @@ -1091,13 +1263,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)") .HasColumnName("key_initiatives_en"); - b.Property("LastUpdatedById") + b.Property("LastModifiedById") .HasColumnType("uniqueidentifier") - .HasColumnName("last_updated_by_id"); + .HasColumnName("last_modified_by_id"); - b.Property("LastUpdatedOn") + b.Property("LastModifiedOn") .HasColumnType("datetimeoffset") - .HasColumnName("last_updated_on"); + .HasColumnName("last_modified_on"); b.Property("RowVersion") .IsConcurrencyToken() @@ -1136,6 +1308,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -1148,6 +1328,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -1245,6 +1433,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -1262,6 +1458,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -1283,6 +1487,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -1295,6 +1507,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -1354,6 +1574,34 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("expert_registration_requests", (string)null); }); + modelBuilder.Entity("CCE.Domain.Identity.InterestTopic", 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.HasKey("Id") + .HasName("pk_interest_topics"); + + b.ToTable("interest_topics", (string)null); + }); + modelBuilder.Entity("CCE.Domain.Identity.RefreshToken", b => { b.Property("Id") @@ -1473,6 +1721,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -1485,6 +1741,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -1558,11 +1822,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(50)") .HasColumnName("first_name"); - b.PrimitiveCollection("Interests") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("interests"); - b.Property("JobTitle") .IsRequired() .HasMaxLength(50) @@ -1656,6 +1915,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUsers", (string)null); }); + modelBuilder.Entity("CCE.Domain.Identity.UserInterestTopic", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("InterestTopicId") + .HasColumnType("uniqueidentifier") + .HasColumnName("interest_topic_id"); + + b.HasKey("UserId", "InterestTopicId") + .HasName("pk_user_interest_topics"); + + b.HasIndex("InterestTopicId") + .HasDatabaseName("ix_user_interest_topics_interest_topic_id"); + + b.ToTable("user_interest_topics", (string)null); + }); + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityScenario", b => { b.Property("Id") @@ -1671,6 +1949,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)") .HasColumnName("configuration_json"); + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + b.Property("CreatedOn") .HasColumnType("datetimeoffset") .HasColumnName("created_on"); @@ -1687,7 +1969,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit") .HasColumnName("is_deleted"); - b.Property("LastModifiedOn") + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") .HasColumnType("datetimeoffset") .HasColumnName("last_modified_on"); @@ -1832,6 +2118,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -1858,6 +2152,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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) @@ -2379,6 +2681,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasConstraintName("fk_refresh_tokens_asp_net_users_user_id"); }); + modelBuilder.Entity("CCE.Domain.Identity.UserInterestTopic", b => + { + b.HasOne("CCE.Domain.Identity.InterestTopic", "InterestTopic") + .WithMany() + .HasForeignKey("InterestTopicId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_interest_topics_interest_topics_interest_topic_id"); + + b.HasOne("CCE.Domain.Identity.User", "User") + .WithMany("UserInterestTopics") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_interest_topics_users_user_id"); + + b.Navigation("InterestTopic"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("CCE.Domain.Identity.Role", null) @@ -2435,6 +2758,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasConstraintName("fk_asp_net_user_tokens_asp_net_users_user_id"); }); + + modelBuilder.Entity("CCE.Domain.Identity.User", b => + { + b.Navigation("UserInterestTopics"); + }); #pragma warning restore 612, 618 } } diff --git a/backend/src/CCE.Infrastructure/dotnet-tools.json b/backend/src/CCE.Infrastructure/dotnet-tools.json new file mode 100644 index 00000000..b0e38abd --- /dev/null +++ b/backend/src/CCE.Infrastructure/dotnet-tools.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "isRoot": true, + "tools": {} +} \ No newline at end of file