diff --git a/backend/src/CCE.Api.Common/Localization/Resources.yaml b/backend/src/CCE.Api.Common/Localization/Resources.yaml index f40e5a01..a5ce2845 100644 --- a/backend/src/CCE.Api.Common/Localization/Resources.yaml +++ b/backend/src/CCE.Api.Common/Localization/Resources.yaml @@ -476,3 +476,49 @@ EVALUATION_NOT_FOUND: EVALUATION_SUBMITTED: ar: "تم تقديم التقييم بنجاح" en: "Evaluation submitted successfully" + +# ─── Resource-specific messages (BRD Sprint 04) ─── + +RESOURCE_CREATED: + ar: "تم رفع المصدر بنجاح!" + en: "Resource uploaded successfully!" + +RESOURCE_DELETED: + ar: "تم حذف المصدر بنجاح!" + en: "Resource deleted successfully!" + +CONTENT_UPDATED: + ar: "تم التحديث بنجاح" + en: "Content updated successfully" + +CONTENT_DELETED: + ar: "تم الحذف بنجاح" + en: "Content deleted successfully" + +RESOURCE_DOWNLOAD_FAILED: + ar: "حدث خطأ أثناء محاولة تحميل المصدر. يرجى المحاولة مرة أخرى." + en: "An error occurred while downloading the resource. Please try again." + +RESOURCE_UPLOAD_FAILED: + ar: "عذراً، حدثت مشكلة أثناء رفع المصدر." + en: "Sorry, a problem occurred while uploading the resource." + +RESOURCE_DELETE_FAILED: + ar: "عذراً، حدثت مشكلة أثناء حذف المصدر." + en: "Sorry, a problem occurred while deleting the resource." + +RESOURCE_NOT_FOUND_ALT: + ar: "عذراً، لا توجد مصادر حالياً." + en: "Sorry, there are no resources currently." + +RESOURCE_DOWNLOAD_SUCCESS: + ar: "تم تحميل المصدر بنجاح! يمكنك الآن الوصول إلى المرفق من جهازك." + en: "Resource downloaded successfully! You can now access the attachment from your device." + +RESOURCE_SHARE_SUCCESS: + ar: "تمت مشاركة المصدر بنجاح!" + en: "Resource shared successfully!" + +RESOURCE_SHARE_FAILED: + ar: "حدث خطأ أثناء محاولة مشاركة المصدر. يرجى المحاولة مرة أخرى لاحقاً." + en: "An error occurred while trying to share the resource. Please try again later." diff --git a/backend/src/CCE.Api.External/Endpoints/EventsPublicEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/EventsPublicEndpoints.cs index f69751eb..5f308d76 100644 --- a/backend/src/CCE.Api.External/Endpoints/EventsPublicEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/EventsPublicEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Content.Public; using CCE.Application.Content.Public.Queries.GetPublicEventById; using CCE.Application.Content.Public.Queries.ListPublicEvents; @@ -17,15 +18,17 @@ public static IEndpointRouteBuilder MapEventsPublicEndpoints(this IEndpointRoute events.MapGet("", async ( int? page, int? pageSize, System.DateTimeOffset? from, System.DateTimeOffset? to, + string? topicSlug, IMediator mediator, CancellationToken cancellationToken) => { var query = new ListPublicEventsQuery( Page: page ?? 1, PageSize: pageSize ?? 20, From: from, - To: to); - var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + To: to, + TopicSlug: topicSlug); + var response = await mediator.Send(query, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .AllowAnonymous() .WithName("ListPublicEvents"); @@ -34,8 +37,8 @@ public static IEndpointRouteBuilder MapEventsPublicEndpoints(this IEndpointRoute System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - var dto = await mediator.Send(new GetPublicEventByIdQuery(id), cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var response = await mediator.Send(new GetPublicEventByIdQuery(id), cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .AllowAnonymous() .WithName("GetPublicEventById"); @@ -44,10 +47,10 @@ public static IEndpointRouteBuilder MapEventsPublicEndpoints(this IEndpointRoute System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - var dto = await mediator.Send(new GetPublicEventByIdQuery(id), cancellationToken).ConfigureAwait(false); - if (dto is null) - return Results.NotFound(); - var ics = IcsBuilder.ToIcs(dto); + var response = await mediator.Send(new GetPublicEventByIdQuery(id), cancellationToken).ConfigureAwait(false); + if (!response.Success) + return response.ToHttpResult(); + var ics = IcsBuilder.ToIcs(response.Data!); return Results.Text(ics, "text/calendar; charset=utf-8"); }) .AllowAnonymous() diff --git a/backend/src/CCE.Api.External/Endpoints/NewsPublicEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/NewsPublicEndpoints.cs index 410288a0..b68a3389 100644 --- a/backend/src/CCE.Api.External/Endpoints/NewsPublicEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/NewsPublicEndpoints.cs @@ -1,8 +1,8 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Content.Public.Queries.GetPublicNewsBySlug; using CCE.Application.Content.Public.Queries.ListPublicNews; using MediatR; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace CCE.Api.External.Endpoints; @@ -14,15 +14,16 @@ public static IEndpointRouteBuilder MapNewsPublicEndpoints(this IEndpointRouteBu var news = app.MapGroup("/api/news").WithTags("News"); news.MapGet("", async ( - int? page, int? pageSize, bool? isFeatured, + int? page, int? pageSize, bool? isFeatured, string? topicSlug, IMediator mediator, CancellationToken cancellationToken) => { var query = new ListPublicNewsQuery( Page: page ?? 1, PageSize: pageSize ?? 20, - IsFeatured: isFeatured); - var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + IsFeatured: isFeatured, + TopicSlug: topicSlug); + var response = await mediator.Send(query, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .AllowAnonymous() .WithName("ListPublicNews"); @@ -31,8 +32,8 @@ public static IEndpointRouteBuilder MapNewsPublicEndpoints(this IEndpointRouteBu string slug, IMediator mediator, CancellationToken cancellationToken) => { - var dto = await mediator.Send(new GetPublicNewsBySlugQuery(slug), cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var response = await mediator.Send(new GetPublicNewsBySlugQuery(slug), cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .AllowAnonymous() .WithName("GetPublicNewsBySlug"); diff --git a/backend/src/CCE.Api.External/Endpoints/ResourcesPublicEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/ResourcesPublicEndpoints.cs index 80ecd9e6..3d63231d 100644 --- a/backend/src/CCE.Api.External/Endpoints/ResourcesPublicEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/ResourcesPublicEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Common.Interfaces; using CCE.Application.Content; using CCE.Application.Content.Public; @@ -19,15 +20,16 @@ public static IEndpointRouteBuilder MapResourcesPublicEndpoints(this IEndpointRo var resources = app.MapGroup("/api/resources").WithTags("Resources"); resources.MapGet("", async ( - int? page, int? pageSize, + int? page, int? pageSize, string? search, System.Guid? categoryId, System.Guid? countryId, ResourceType? resourceType, IMediator mediator, CancellationToken cancellationToken) => { var query = new ListPublicResourcesQuery( Page: page ?? 1, PageSize: pageSize ?? 20, + Search: search, CategoryId: categoryId, CountryId: countryId, ResourceType: resourceType); - var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + var response = await mediator.Send(query, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .AllowAnonymous() .WithName("ListPublicResources"); @@ -36,8 +38,8 @@ public static IEndpointRouteBuilder MapResourcesPublicEndpoints(this IEndpointRo System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - var dto = await mediator.Send(new GetPublicResourceByIdQuery(id), cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var response = await mediator.Send(new GetPublicResourceByIdQuery(id), cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .AllowAnonymous() .WithName("GetPublicResourceById"); @@ -50,21 +52,15 @@ public static IEndpointRouteBuilder MapResourcesPublicEndpoints(this IEndpointRo IResourceViewCountRepository viewCounter, CancellationToken cancellationToken) => { - // Load resource + asset metadata in a single round trip. var resource = await db.Resources.FirstOrDefaultAsync(r => r.Id == id, cancellationToken).ConfigureAwait(false); if (resource is null || resource.PublishedOn is null) - { return Results.NotFound(); - } + var asset = await db.AssetFiles.FirstOrDefaultAsync(a => a.Id == resource.AssetFileId, cancellationToken).ConfigureAwait(false); if (asset is null) - { return Results.NotFound(); - } if (asset.VirusScanStatus != VirusScanStatus.Clean) - { return Results.StatusCode(StatusCodes.Status403Forbidden); - } httpContext.Response.ContentType = asset.MimeType; httpContext.Response.Headers.ContentDisposition = @@ -73,7 +69,6 @@ public static IEndpointRouteBuilder MapResourcesPublicEndpoints(this IEndpointRo await using var stream = await storage.OpenReadAsync(asset.Url, cancellationToken).ConfigureAwait(false); await stream.CopyToAsync(httpContext.Response.Body, cancellationToken).ConfigureAwait(false); - // Fire-and-forget view-count bump (don't await; don't propagate exceptions). _ = Task.Run(async () => { try { await viewCounter.IncrementAsync(id, CancellationToken.None).ConfigureAwait(false); } diff --git a/backend/src/CCE.Api.External/appsettings.Production.json b/backend/src/CCE.Api.External/appsettings.Production.json index e1a2febb..72fb3431 100644 --- a/backend/src/CCE.Api.External/appsettings.Production.json +++ b/backend/src/CCE.Api.External/appsettings.Production.json @@ -76,11 +76,11 @@ }, "ExternalApis": { "CommunicationGateway": { - "BaseUrl": "https://cce-mocks.bonto.run", + "BaseUrl": "https://cce-mock.bonto.run", "TimeoutSeconds": 30 }, "AdminAuthGateway": { - "BaseUrl": "https://cce-mocks.bonto.run", + "BaseUrl": "https://cce-mock.bonto.run", "TimeoutSeconds": 30 } }, diff --git a/backend/src/CCE.Api.External/backend/uploads/uploads/2026/05/63d703a56d034ca2a565344a1f5110f4.pdf b/backend/src/CCE.Api.External/backend/uploads/uploads/2026/05/63d703a56d034ca2a565344a1f5110f4.pdf new file mode 100644 index 00000000..986229f8 Binary files /dev/null and b/backend/src/CCE.Api.External/backend/uploads/uploads/2026/05/63d703a56d034ca2a565344a1f5110f4.pdf differ diff --git a/backend/src/CCE.Api.External/backend/uploads/uploads/2026/05/94448ac812bd4db397140dc7bb2907b9.pdf b/backend/src/CCE.Api.External/backend/uploads/uploads/2026/05/94448ac812bd4db397140dc7bb2907b9.pdf new file mode 100644 index 00000000..986229f8 Binary files /dev/null and b/backend/src/CCE.Api.External/backend/uploads/uploads/2026/05/94448ac812bd4db397140dc7bb2907b9.pdf differ diff --git a/backend/src/CCE.Api.External/backend/uploads/uploads/2026/05/c8c645d9029c46a3964f76a6fe7f8dd3.pdf b/backend/src/CCE.Api.External/backend/uploads/uploads/2026/05/c8c645d9029c46a3964f76a6fe7f8dd3.pdf new file mode 100644 index 00000000..986229f8 Binary files /dev/null and b/backend/src/CCE.Api.External/backend/uploads/uploads/2026/05/c8c645d9029c46a3964f76a6fe7f8dd3.pdf differ diff --git a/backend/src/CCE.Api.Internal/Endpoints/CreateEventRequest.cs b/backend/src/CCE.Api.Internal/Endpoints/CreateEventRequest.cs new file mode 100644 index 00000000..0aa577a6 --- /dev/null +++ b/backend/src/CCE.Api.Internal/Endpoints/CreateEventRequest.cs @@ -0,0 +1,12 @@ +namespace CCE.Api.Internal.Endpoints; + +public sealed record CreateEventRequest( + string TitleAr, string TitleEn, + string DescriptionAr, string DescriptionEn, + System.DateTimeOffset StartsOn, + System.DateTimeOffset EndsOn, + string? LocationAr, + string? LocationEn, + string? OnlineMeetingUrl, + string? FeaturedImageUrl, + System.Guid TopicId); diff --git a/backend/src/CCE.Api.Internal/Endpoints/CreateNewsRequest.cs b/backend/src/CCE.Api.Internal/Endpoints/CreateNewsRequest.cs new file mode 100644 index 00000000..b6df382c --- /dev/null +++ b/backend/src/CCE.Api.Internal/Endpoints/CreateNewsRequest.cs @@ -0,0 +1,5 @@ +namespace CCE.Api.Internal.Endpoints; + +public sealed record CreateNewsRequest( + string TitleAr, string TitleEn, string ContentAr, string ContentEn, + System.Guid TopicId, string? FeaturedImageUrl); diff --git a/backend/src/CCE.Api.Internal/Endpoints/CreateResourceRequest.cs b/backend/src/CCE.Api.Internal/Endpoints/CreateResourceRequest.cs new file mode 100644 index 00000000..d86d8a60 --- /dev/null +++ b/backend/src/CCE.Api.Internal/Endpoints/CreateResourceRequest.cs @@ -0,0 +1,14 @@ +using CCE.Domain.Content; + +namespace CCE.Api.Internal.Endpoints; + +public sealed record CreateResourceRequest( + string TitleAr, + string TitleEn, + string DescriptionAr, + string DescriptionEn, + ResourceType ResourceType, + System.Guid CategoryId, + System.Guid? CountryId, + System.Guid AssetFileId, + List CountryIds); diff --git a/backend/src/CCE.Api.Internal/Endpoints/EventEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/EventEndpoints.cs index 51177c6e..56a6c2c2 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/EventEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/EventEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Content.Commands.CreateEvent; using CCE.Application.Content.Commands.DeleteEvent; using CCE.Application.Content.Commands.RescheduleEvent; @@ -7,7 +8,6 @@ using CCE.Domain; using MediatR; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace CCE.Api.Internal.Endpoints; @@ -20,33 +20,38 @@ public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder events.MapGet("", async ( int? page, int? pageSize, string? search, - System.DateTimeOffset? fromDate, System.DateTimeOffset? toDate, + System.DateTimeOffset? fromDate, System.DateTimeOffset? toDate, System.Guid? topicId, IMediator mediator, CancellationToken cancellationToken) => { - var query = new ListEventsQuery(page ?? 1, pageSize ?? 20, search, fromDate, toDate); - var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + var query = new ListEventsQuery(page ?? 1, pageSize ?? 20, search, fromDate, toDate, topicId); + var response = await mediator.Send(query, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.Event_Manage) .WithName("ListEvents"); - events.MapGet("/{id:guid}", async (System.Guid id, IMediator mediator, CancellationToken cancellationToken) => + events.MapGet("/{id:guid}", async ( + System.Guid id, + IMediator mediator, CancellationToken cancellationToken) => { - var dto = await mediator.Send(new GetEventByIdQuery(id), cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var response = await mediator.Send(new GetEventByIdQuery(id), cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.Event_Manage) .WithName("GetEventById"); - events.MapPost("", async (CreateEventRequest body, IMediator mediator, CancellationToken cancellationToken) => + events.MapPost("", async ( + CreateEventRequest body, + IMediator mediator, CancellationToken cancellationToken) => { var cmd = new CreateEventCommand( body.TitleAr, body.TitleEn, body.DescriptionAr, body.DescriptionEn, body.StartsOn, body.EndsOn, body.LocationAr, body.LocationEn, - body.OnlineMeetingUrl, body.FeaturedImageUrl); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return Results.Created($"/api/admin/events/{dto.Id}", dto); + body.OnlineMeetingUrl, body.FeaturedImageUrl, + body.TopicId); + var response = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.Event_Manage) .WithName("CreateEvent"); @@ -56,16 +61,15 @@ public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder UpdateEventRequest body, IMediator mediator, CancellationToken cancellationToken) => { - var rowVersion = string.IsNullOrEmpty(body.RowVersion) ? System.Array.Empty() : System.Convert.FromBase64String(body.RowVersion); var cmd = new UpdateEventCommand( id, body.TitleAr, body.TitleEn, body.DescriptionAr, body.DescriptionEn, body.LocationAr, body.LocationEn, body.OnlineMeetingUrl, body.FeaturedImageUrl, - rowVersion); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + body.TopicId); + var response = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.Event_Manage) .WithName("UpdateEvent"); @@ -75,10 +79,9 @@ public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder RescheduleEventRequest body, IMediator mediator, CancellationToken cancellationToken) => { - var rowVersion = string.IsNullOrEmpty(body.RowVersion) ? System.Array.Empty() : System.Convert.FromBase64String(body.RowVersion); - var cmd = new RescheduleEventCommand(id, body.StartsOn, body.EndsOn, rowVersion); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var cmd = new RescheduleEventCommand(id, body.StartsOn, body.EndsOn); + var response = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.Event_Manage) .WithName("RescheduleEvent"); @@ -87,8 +90,8 @@ public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - await mediator.Send(new DeleteEventCommand(id), cancellationToken).ConfigureAwait(false); - return Results.NoContent(); + var response = await mediator.Send(new DeleteEventCommand(id), cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.Event_Manage) .WithName("DeleteEvent"); @@ -96,25 +99,3 @@ public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder return app; } } - -public sealed record CreateEventRequest( - string TitleAr, string TitleEn, - string DescriptionAr, string DescriptionEn, - System.DateTimeOffset StartsOn, - System.DateTimeOffset EndsOn, - string? LocationAr, - string? LocationEn, - string? OnlineMeetingUrl, - string? FeaturedImageUrl); - -public sealed record UpdateEventRequest( - string TitleAr, string TitleEn, - string DescriptionAr, string DescriptionEn, - string? LocationAr, string? LocationEn, - string? OnlineMeetingUrl, string? FeaturedImageUrl, - string RowVersion); - -public sealed record RescheduleEventRequest( - System.DateTimeOffset StartsOn, - System.DateTimeOffset EndsOn, - string RowVersion); diff --git a/backend/src/CCE.Api.Internal/Endpoints/NewsEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/NewsEndpoints.cs index 931f410b..53fa8c11 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/NewsEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/NewsEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Content.Commands.CreateNews; using CCE.Application.Content.Commands.DeleteNews; using CCE.Application.Content.Commands.PublishNews; @@ -7,7 +8,6 @@ using CCE.Domain; using MediatR; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace CCE.Api.Internal.Endpoints; @@ -19,7 +19,7 @@ public static IEndpointRouteBuilder MapNewsEndpoints(this IEndpointRouteBuilder var news = app.MapGroup("/api/admin/news").WithTags("News"); news.MapGet("", async ( - int? page, int? pageSize, string? search, bool? isPublished, bool? isFeatured, + int? page, int? pageSize, string? search, bool? isPublished, bool? isFeatured, System.Guid? topicId, IMediator mediator, CancellationToken cancellationToken) => { var query = new ListNewsQuery( @@ -27,9 +27,10 @@ public static IEndpointRouteBuilder MapNewsEndpoints(this IEndpointRouteBuilder PageSize: pageSize ?? 20, Search: search, IsPublished: isPublished, - IsFeatured: isFeatured); - var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + IsFeatured: isFeatured, + TopicId: topicId); + var response = await mediator.Send(query, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.News_Update) .WithName("ListNews"); @@ -38,8 +39,8 @@ public static IEndpointRouteBuilder MapNewsEndpoints(this IEndpointRouteBuilder System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - var dto = await mediator.Send(new GetNewsByIdQuery(id), cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var response = await mediator.Send(new GetNewsByIdQuery(id), cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.News_Update) .WithName("GetNewsById"); @@ -48,9 +49,9 @@ public static IEndpointRouteBuilder MapNewsEndpoints(this IEndpointRouteBuilder CreateNewsRequest body, IMediator mediator, CancellationToken cancellationToken) => { - var cmd = new CreateNewsCommand(body.TitleAr, body.TitleEn, body.ContentAr, body.ContentEn, body.Slug, body.FeaturedImageUrl); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return Results.Created($"/api/admin/news/{dto.Id}", dto); + var cmd = new CreateNewsCommand(body.TitleAr, body.TitleEn, body.ContentAr, body.ContentEn, body.TopicId, body.FeaturedImageUrl); + var response = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.News_Update) .WithName("CreateNews"); @@ -60,10 +61,9 @@ public static IEndpointRouteBuilder MapNewsEndpoints(this IEndpointRouteBuilder UpdateNewsRequest body, IMediator mediator, CancellationToken cancellationToken) => { - var rowVersion = string.IsNullOrEmpty(body.RowVersion) ? System.Array.Empty() : System.Convert.FromBase64String(body.RowVersion); - var cmd = new UpdateNewsCommand(id, body.TitleAr, body.TitleEn, body.ContentAr, body.ContentEn, body.Slug, body.FeaturedImageUrl, rowVersion); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var cmd = new UpdateNewsCommand(id, body.TitleAr, body.TitleEn, body.ContentAr, body.ContentEn, body.TopicId, body.FeaturedImageUrl); + var response = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.News_Update) .WithName("UpdateNews"); @@ -72,8 +72,8 @@ public static IEndpointRouteBuilder MapNewsEndpoints(this IEndpointRouteBuilder System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - await mediator.Send(new DeleteNewsCommand(id), cancellationToken).ConfigureAwait(false); - return Results.NoContent(); + var response = await mediator.Send(new DeleteNewsCommand(id), cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.News_Delete) .WithName("DeleteNews"); @@ -82,8 +82,8 @@ public static IEndpointRouteBuilder MapNewsEndpoints(this IEndpointRouteBuilder System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - var dto = await mediator.Send(new PublishNewsCommand(id), cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var response = await mediator.Send(new PublishNewsCommand(id), cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.News_Publish) .WithName("PublishNews"); @@ -91,11 +91,3 @@ public static IEndpointRouteBuilder MapNewsEndpoints(this IEndpointRouteBuilder return app; } } - -public sealed record CreateNewsRequest( - string TitleAr, string TitleEn, string ContentAr, string ContentEn, - string Slug, string? FeaturedImageUrl); - -public sealed record UpdateNewsRequest( - string TitleAr, string TitleEn, string ContentAr, string ContentEn, - string Slug, string? FeaturedImageUrl, string RowVersion); diff --git a/backend/src/CCE.Api.Internal/Endpoints/RescheduleEventRequest.cs b/backend/src/CCE.Api.Internal/Endpoints/RescheduleEventRequest.cs new file mode 100644 index 00000000..b3baca9a --- /dev/null +++ b/backend/src/CCE.Api.Internal/Endpoints/RescheduleEventRequest.cs @@ -0,0 +1,5 @@ +namespace CCE.Api.Internal.Endpoints; + +public sealed record RescheduleEventRequest( + System.DateTimeOffset StartsOn, + System.DateTimeOffset EndsOn); diff --git a/backend/src/CCE.Api.Internal/Endpoints/ResourceEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/ResourceEndpoints.cs index 459dab3c..ac743e26 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/ResourceEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/ResourceEndpoints.cs @@ -1,12 +1,14 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Content.Commands.CreateResource; +using CCE.Application.Content.Commands.DeleteResource; using CCE.Application.Content.Commands.PublishResource; using CCE.Application.Content.Commands.UpdateResource; +using CCE.Application.Content.Queries.GetResourceById; using CCE.Application.Content.Queries.ListResources; using CCE.Domain; using CCE.Domain.Content; using MediatR; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace CCE.Api.Internal.Endpoints; @@ -29,12 +31,22 @@ public static IEndpointRouteBuilder MapResourceEndpoints(this IEndpointRouteBuil CategoryId: categoryId, CountryId: countryId, IsPublished: isPublished); - var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + var response = await mediator.Send(query, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.Resource_Center_Upload) .WithName("ListResources"); + resources.MapGet("/{id:guid}", async ( + System.Guid id, + IMediator mediator, CancellationToken cancellationToken) => + { + var response = await mediator.Send(new GetResourceByIdQuery(id), cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); + }) + .RequireAuthorization(Permissions.Resource_Center_Upload) + .WithName("GetResourceById"); + resources.MapPost("", async ( CreateResourceRequest body, IMediator mediator, CancellationToken cancellationToken) => @@ -42,9 +54,10 @@ public static IEndpointRouteBuilder MapResourceEndpoints(this IEndpointRouteBuil var cmd = new CreateResourceCommand( body.TitleAr, body.TitleEn, body.DescriptionAr, body.DescriptionEn, - body.ResourceType, body.CategoryId, body.CountryId, body.AssetFileId); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return Results.Created($"/api/admin/resources/{dto.Id}", dto); + body.ResourceType, body.CategoryId, body.CountryId, body.AssetFileId, + body.CountryIds); + var response = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.Resource_Center_Upload) .WithName("CreateResource"); @@ -54,17 +67,14 @@ public static IEndpointRouteBuilder MapResourceEndpoints(this IEndpointRouteBuil UpdateResourceRequest body, IMediator mediator, CancellationToken cancellationToken) => { - var rowVersion = string.IsNullOrEmpty(body.RowVersion) - ? System.Array.Empty() - : System.Convert.FromBase64String(body.RowVersion); var cmd = new UpdateResourceCommand( id, body.TitleAr, body.TitleEn, body.DescriptionAr, body.DescriptionEn, body.ResourceType, body.CategoryId, - rowVersion); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + body.CountryIds); + var response = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.Resource_Center_Update) .WithName("UpdateResource"); @@ -73,31 +83,24 @@ public static IEndpointRouteBuilder MapResourceEndpoints(this IEndpointRouteBuil System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - var dto = await mediator.Send(new PublishResourceCommand(id), cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var response = await mediator.Send(new PublishResourceCommand(id), cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); }) .RequireAuthorization(Permissions.Resource_Center_Upload) .WithName("PublishResource"); + resources.MapDelete("/{id:guid}", async ( + System.Guid id, + IMediator mediator, CancellationToken cancellationToken) => + { + var response = await mediator.Send(new DeleteResourceCommand(id), cancellationToken).ConfigureAwait(false); + return response.ToHttpResult(); + }) + .RequireAuthorization(Permissions.Resource_Center_Delete) + .WithName("DeleteResource"); + return app; } } -public sealed record CreateResourceRequest( - string TitleAr, - string TitleEn, - string DescriptionAr, - string DescriptionEn, - CCE.Domain.Content.ResourceType ResourceType, - System.Guid CategoryId, - System.Guid? CountryId, - System.Guid AssetFileId); -public sealed record UpdateResourceRequest( - string TitleAr, - string TitleEn, - string DescriptionAr, - string DescriptionEn, - CCE.Domain.Content.ResourceType ResourceType, - System.Guid CategoryId, - string RowVersion); diff --git a/backend/src/CCE.Api.Internal/Endpoints/UpdateEventRequest.cs b/backend/src/CCE.Api.Internal/Endpoints/UpdateEventRequest.cs new file mode 100644 index 00000000..30b1395e --- /dev/null +++ b/backend/src/CCE.Api.Internal/Endpoints/UpdateEventRequest.cs @@ -0,0 +1,8 @@ +namespace CCE.Api.Internal.Endpoints; + +public sealed record UpdateEventRequest( + string TitleAr, string TitleEn, + string DescriptionAr, string DescriptionEn, + string? LocationAr, string? LocationEn, + string? OnlineMeetingUrl, string? FeaturedImageUrl, + System.Guid TopicId); diff --git a/backend/src/CCE.Api.Internal/Endpoints/UpdateNewsRequest.cs b/backend/src/CCE.Api.Internal/Endpoints/UpdateNewsRequest.cs new file mode 100644 index 00000000..4cdc9a6a --- /dev/null +++ b/backend/src/CCE.Api.Internal/Endpoints/UpdateNewsRequest.cs @@ -0,0 +1,5 @@ +namespace CCE.Api.Internal.Endpoints; + +public sealed record UpdateNewsRequest( + string TitleAr, string TitleEn, string ContentAr, string ContentEn, + System.Guid TopicId, string? FeaturedImageUrl); diff --git a/backend/src/CCE.Api.Internal/Endpoints/UpdateResourceRequest.cs b/backend/src/CCE.Api.Internal/Endpoints/UpdateResourceRequest.cs new file mode 100644 index 00000000..9d52205f --- /dev/null +++ b/backend/src/CCE.Api.Internal/Endpoints/UpdateResourceRequest.cs @@ -0,0 +1,13 @@ +using CCE.Domain.Content; + +namespace CCE.Api.Internal.Endpoints; + +public sealed record UpdateResourceRequest( + string TitleAr, + string TitleEn, + string DescriptionAr, + string DescriptionEn, + ResourceType ResourceType, + System.Guid CategoryId, + List CountryIds, + string RowVersion); diff --git a/backend/src/CCE.Api.Internal/appsettings.Production.json b/backend/src/CCE.Api.Internal/appsettings.Production.json index aebea391..ba97ac10 100644 --- a/backend/src/CCE.Api.Internal/appsettings.Production.json +++ b/backend/src/CCE.Api.Internal/appsettings.Production.json @@ -63,11 +63,11 @@ }, "ExternalApis": { "CommunicationGateway": { - "BaseUrl": "https://cce-mocks.bonto.run", + "BaseUrl": "https://cce-mock.bonto.run", "TimeoutSeconds": 30 }, "AdminAuthGateway": { - "BaseUrl": "https://cce-mocks.bonto.run", + "BaseUrl": "https://cce-mock.bonto.run", "TimeoutSeconds": 30 } }, diff --git a/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommand.cs b/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommand.cs index a2a23b94..d58dcada 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; @@ -11,4 +12,5 @@ public sealed record CreateEventCommand( string? LocationAr, string? LocationEn, string? OnlineMeetingUrl, - string? FeaturedImageUrl) : IRequest; + string? FeaturedImageUrl, + System.Guid TopicId) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommandHandler.cs index 194778ba..f196a2aa 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommandHandler.cs @@ -1,24 +1,40 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; using CCE.Application.Content.Queries.ListEvents; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Commands.CreateEvent; -public sealed class CreateEventCommandHandler : IRequestHandler +public sealed class CreateEventCommandHandler : IRequestHandler> { - private readonly IEventRepository _service; + private readonly IRepository _repo; + private readonly ICceDbContext _db; private readonly ISystemClock _clock; + private readonly MessageFactory _messages; - public CreateEventCommandHandler(IEventRepository service, ISystemClock clock) + public CreateEventCommandHandler( + IRepository repo, + ICceDbContext db, + ISystemClock clock, + MessageFactory messages) { - _service = service; + _repo = repo; + _db = db; _clock = clock; + _messages = messages; } - public async Task Handle(CreateEventCommand request, CancellationToken cancellationToken) + public async Task> Handle(CreateEventCommand request, CancellationToken cancellationToken) { + var topicExists = await _db.Topics.Where(t => t.Id == request.TopicId).CountAsyncEither(cancellationToken) > 0; + if (!topicExists) + return _messages.NotFound("TOPIC_NOT_FOUND"); + var ev = Event.Schedule( request.TitleAr, request.TitleEn, @@ -30,10 +46,17 @@ public async Task Handle(CreateEventCommand request, CancellationToken request.LocationEn, request.OnlineMeetingUrl, request.FeaturedImageUrl, + request.TopicId, _clock); - await _service.SaveAsync(ev, cancellationToken).ConfigureAwait(false); + await _repo.AddAsync(ev, cancellationToken).ConfigureAwait(false); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + var topic = await _db.Topics.Where(t => t.Id == request.TopicId) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + var topicNameAr = topic.FirstOrDefault()?.NameAr ?? string.Empty; + var topicNameEn = topic.FirstOrDefault()?.NameEn ?? string.Empty; - return ListEventsQueryHandler.MapToDto(ev); + return _messages.Ok(ListEventsQueryHandler.MapToDto(ev, topicNameAr, topicNameEn), "CONTENT_CREATED"); } } diff --git a/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommandValidator.cs b/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommandValidator.cs index ca10bff9..3cd88131 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommandValidator.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommandValidator.cs @@ -6,10 +6,13 @@ public sealed class CreateEventCommandValidator : AbstractValidator x.TitleAr).NotEmpty().MaximumLength(500); - RuleFor(x => x.TitleEn).NotEmpty().MaximumLength(500); - RuleFor(x => x.DescriptionAr).NotEmpty(); - RuleFor(x => x.DescriptionEn).NotEmpty(); + RuleFor(x => x.TitleAr).NotEmpty().MaximumLength(255); + RuleFor(x => x.TitleEn).NotEmpty().MaximumLength(255); + RuleFor(x => x.DescriptionAr).NotEmpty().MaximumLength(2000); + RuleFor(x => x.DescriptionEn).NotEmpty().MaximumLength(2000); + RuleFor(x => x.LocationAr).MaximumLength(255).When(x => x.LocationAr is not null); + RuleFor(x => x.LocationEn).MaximumLength(255).When(x => x.LocationEn is not null); RuleFor(x => x.EndsOn).GreaterThan(x => x.StartsOn).WithMessage("EndsOn must be after StartsOn."); + RuleFor(x => x.TopicId).NotEmpty(); } } diff --git a/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommand.cs b/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommand.cs index f7b0a23c..90e5965e 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; @@ -6,5 +7,5 @@ namespace CCE.Application.Content.Commands.CreateNews; public sealed record CreateNewsCommand( string TitleAr, string TitleEn, string ContentAr, string ContentEn, - string Slug, - string? FeaturedImageUrl) : IRequest; + System.Guid TopicId, + string? FeaturedImageUrl) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandHandler.cs index 42481895..7a967e10 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandHandler.cs @@ -1,45 +1,65 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; using CCE.Application.Content.Queries.ListNews; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Commands.CreateNews; -public sealed class CreateNewsCommandHandler : IRequestHandler +public sealed class CreateNewsCommandHandler : IRequestHandler> { - private readonly INewsRepository _service; + private readonly IRepository _repo; + private readonly ICceDbContext _db; private readonly ICurrentUserAccessor _currentUser; private readonly ISystemClock _clock; + private readonly MessageFactory _messages; public CreateNewsCommandHandler( - INewsRepository service, + IRepository repo, + ICceDbContext db, ICurrentUserAccessor currentUser, - ISystemClock clock) + ISystemClock clock, + MessageFactory messages) { - _service = service; + _repo = repo; + _db = db; _currentUser = currentUser; _clock = clock; + _messages = messages; } - public async Task Handle(CreateNewsCommand request, CancellationToken cancellationToken) + public async Task> Handle(CreateNewsCommand request, CancellationToken cancellationToken) { - var authorId = _currentUser.GetUserId() - ?? throw new DomainException("Cannot create a news article from a request without a user identity."); + var authorId = _currentUser.GetUserId(); + if (authorId is null) + return _messages.NotAuthenticated(); + + var topicExists = await _db.Topics.Where(t => t.Id == request.TopicId).CountAsyncEither(cancellationToken) > 0; + if (!topicExists) + return _messages.NotFound("TOPIC_NOT_FOUND"); var news = News.Draft( request.TitleAr, request.TitleEn, request.ContentAr, request.ContentEn, - request.Slug, - authorId, + request.TopicId, + authorId.Value, request.FeaturedImageUrl, _clock); - await _service.SaveAsync(news, cancellationToken).ConfigureAwait(false); + await _repo.AddAsync(news, cancellationToken).ConfigureAwait(false); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + var topic = await _db.Topics.Where(t => t.Id == request.TopicId) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + var topicNameAr = topic.FirstOrDefault()?.NameAr ?? string.Empty; + var topicNameEn = topic.FirstOrDefault()?.NameEn ?? string.Empty; - return ListNewsQueryHandler.MapToDto(news); + return _messages.Ok(ListNewsQueryHandler.MapToDto(news, topicNameAr, topicNameEn), "CONTENT_CREATED"); } } diff --git a/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandValidator.cs b/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandValidator.cs index 3f1421c0..e1583e21 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandValidator.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandValidator.cs @@ -6,10 +6,10 @@ public sealed class CreateNewsCommandValidator : AbstractValidator x.TitleAr).NotEmpty().MaximumLength(500); - RuleFor(x => x.TitleEn).NotEmpty().MaximumLength(500); - RuleFor(x => x.ContentAr).NotEmpty(); - RuleFor(x => x.ContentEn).NotEmpty(); - RuleFor(x => x.Slug).NotEmpty().MaximumLength(200); + RuleFor(x => x.TitleAr).NotEmpty().MaximumLength(255); + RuleFor(x => x.TitleEn).NotEmpty().MaximumLength(255); + RuleFor(x => x.ContentAr).NotEmpty().MaximumLength(2000); + RuleFor(x => x.ContentEn).NotEmpty().MaximumLength(2000); + RuleFor(x => x.TopicId).NotEmpty(); } } diff --git a/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommand.cs b/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommand.cs index 87ac3eb3..f423b87e 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using CCE.Domain.Content; using MediatR; @@ -12,4 +13,5 @@ public sealed record CreateResourceCommand( ResourceType ResourceType, System.Guid CategoryId, System.Guid? CountryId, - System.Guid AssetFileId) : IRequest; + System.Guid AssetFileId, + IReadOnlyList CountryIds) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandHandler.cs index fcd767b2..e53b098a 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandHandler.cs @@ -1,45 +1,67 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Content; +using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Commands.CreateResource; -public sealed class CreateResourceCommandHandler : IRequestHandler +public sealed class CreateResourceCommandHandler : IRequestHandler> { - private readonly IResourceRepository _service; - private readonly IAssetRepository _assetService; + private readonly IRepository _repo; + private readonly ICceDbContext _db; private readonly ICurrentUserAccessor _currentUser; private readonly ISystemClock _clock; + private readonly MessageFactory _messages; public CreateResourceCommandHandler( - IResourceRepository service, - IAssetRepository assetService, + IRepository repo, + ICceDbContext db, ICurrentUserAccessor currentUser, - ISystemClock clock) + ISystemClock clock, + MessageFactory messages) { - _service = service; - _assetService = assetService; + _repo = repo; + _db = db; _currentUser = currentUser; _clock = clock; + _messages = messages; } - public async Task Handle(CreateResourceCommand request, CancellationToken cancellationToken) + public async Task> Handle(CreateResourceCommand request, CancellationToken cancellationToken) { - var asset = await _assetService.GetByIdAsync(request.AssetFileId, cancellationToken).ConfigureAwait(false); + var assets = await _db.AssetFiles + .Where(a => a.Id == request.AssetFileId) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var asset = assets.SingleOrDefault(); + if (asset is null) - { - throw new System.Collections.Generic.KeyNotFoundException($"Asset {request.AssetFileId} not found."); - } + return _messages.AssetNotFound(); if (asset.VirusScanStatus != VirusScanStatus.Clean) + return _messages.AssetNotClean(); + + var categoryExists = await ExistsAsync(_db.ResourceCategories.Where(c => c.Id == request.CategoryId), cancellationToken).ConfigureAwait(false); + if (!categoryExists) + return _messages.CategoryNotFound(); + + var countryIds = request.CountryIds.Distinct().ToList(); + if (countryIds.Count > 0) { - throw new DomainException($"Asset {request.AssetFileId} has not passed virus scan ({asset.VirusScanStatus})."); + var existingCountryCount = await _db.Countries + .Where(c => countryIds.Contains(c.Id)) + .CountAsyncEither(cancellationToken) + .ConfigureAwait(false); + if (existingCountryCount != countryIds.Count) + return _messages.NotFound("COUNTRY_NOT_FOUND"); } - var uploadedById = _currentUser.GetUserId() - ?? throw new DomainException("Cannot create a resource from a request without a user identity."); + var uploadedById = _currentUser.GetUserId(); + if (uploadedById is null) + return _messages.NotAuthenticated(); var resource = Resource.Draft( request.TitleAr, @@ -49,27 +71,20 @@ public async Task Handle(CreateResourceCommand request, Cancellatio request.ResourceType, request.CategoryId, request.CountryId, - uploadedById, + uploadedById.Value, request.AssetFileId, + request.CountryIds, _clock); - await _service.SaveAsync(resource, cancellationToken).ConfigureAwait(false); + await _repo.AddAsync(resource, cancellationToken).ConfigureAwait(false); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return _messages.Ok(resource.Id, "RESOURCE_CREATED"); + } - return new ResourceDto( - resource.Id, - resource.TitleAr, - resource.TitleEn, - resource.DescriptionAr, - resource.DescriptionEn, - resource.ResourceType, - resource.CategoryId, - resource.CountryId, - resource.UploadedById, - resource.AssetFileId, - resource.PublishedOn, - resource.ViewCount, - resource.IsCenterManaged, - resource.IsPublished, - System.Convert.ToBase64String(resource.RowVersion)); + private static async Task ExistsAsync(IQueryable query, CancellationToken ct) + { + var list = await query.Take(1).ToListAsyncEither(ct).ConfigureAwait(false); + return list.Count > 0; } } diff --git a/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandValidator.cs b/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandValidator.cs index fc7b7c47..aa38e375 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandValidator.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandValidator.cs @@ -6,11 +6,12 @@ public sealed class CreateResourceCommandValidator : AbstractValidator x.TitleAr).NotEmpty().MaximumLength(500); - RuleFor(x => x.TitleEn).NotEmpty().MaximumLength(500); - RuleFor(x => x.DescriptionAr).NotEmpty().MaximumLength(4000); - RuleFor(x => x.DescriptionEn).NotEmpty().MaximumLength(4000); + RuleFor(x => x.TitleAr).NotEmpty().MaximumLength(255); + RuleFor(x => x.TitleEn).NotEmpty().MaximumLength(255); + RuleFor(x => x.DescriptionAr).NotEmpty().MaximumLength(500); + RuleFor(x => x.DescriptionEn).NotEmpty().MaximumLength(500); RuleFor(x => x.CategoryId).NotEmpty(); RuleFor(x => x.AssetFileId).NotEmpty(); + RuleFor(x => x.CountryIds).NotEmpty().ForEach(x => x.NotEmpty()); } } diff --git a/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommand.cs b/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommand.cs index 20f4bc55..5ca7be7a 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommand.cs @@ -1,5 +1,6 @@ +using CCE.Application.Common; using MediatR; namespace CCE.Application.Content.Commands.DeleteEvent; -public sealed record DeleteEventCommand(System.Guid Id) : IRequest; +public sealed record DeleteEventCommand(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommandHandler.cs index 3c0b2460..7275628f 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommandHandler.cs @@ -1,36 +1,47 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Content; +using CCE.Application.Messages; using CCE.Domain.Common; +using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Commands.DeleteEvent; -public sealed class DeleteEventCommandHandler : IRequestHandler +public sealed class DeleteEventCommandHandler : IRequestHandler> { - private readonly IEventRepository _service; + private readonly IRepository _repo; + private readonly ICceDbContext _db; private readonly ICurrentUserAccessor _currentUser; private readonly ISystemClock _clock; + private readonly MessageFactory _messages; - public DeleteEventCommandHandler(IEventRepository service, ICurrentUserAccessor currentUser, ISystemClock clock) + public DeleteEventCommandHandler( + IRepository repo, + ICceDbContext db, + ICurrentUserAccessor currentUser, + ISystemClock clock, + MessageFactory messages) { - _service = service; + _repo = repo; + _db = db; _currentUser = currentUser; _clock = clock; + _messages = messages; } - public async Task Handle(DeleteEventCommand request, CancellationToken cancellationToken) + public async Task> Handle(DeleteEventCommand request, CancellationToken cancellationToken) { - var ev = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); + var ev = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (ev is null) - { - throw new System.Collections.Generic.KeyNotFoundException($"Event {request.Id} not found."); - } + return _messages.EventNotFound(); - var deletedById = _currentUser.GetUserId() - ?? throw new DomainException("Cannot delete event from a request without a user identity."); + var userId = _currentUser.GetUserId(); + if (userId is null) + return _messages.NotAuthenticated(); - ev.SoftDelete(deletedById, _clock); - await _service.UpdateAsync(ev, ev.RowVersion, cancellationToken).ConfigureAwait(false); - return Unit.Value; + ev.SoftDelete(userId.Value, _clock); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return _messages.Ok("CONTENT_DELETED"); } } diff --git a/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommand.cs b/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommand.cs index 6e318eb0..0f26d3bf 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommand.cs @@ -1,5 +1,6 @@ +using CCE.Application.Common; using MediatR; namespace CCE.Application.Content.Commands.DeleteNews; -public sealed record DeleteNewsCommand(System.Guid Id) : IRequest; +public sealed record DeleteNewsCommand(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommandHandler.cs index ab119842..50b547d2 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommandHandler.cs @@ -1,36 +1,47 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Content; +using CCE.Application.Messages; using CCE.Domain.Common; +using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Commands.DeleteNews; -public sealed class DeleteNewsCommandHandler : IRequestHandler +public sealed class DeleteNewsCommandHandler : IRequestHandler> { - private readonly INewsRepository _service; + private readonly IRepository _repo; + private readonly ICceDbContext _db; private readonly ICurrentUserAccessor _currentUser; private readonly ISystemClock _clock; + private readonly MessageFactory _messages; - public DeleteNewsCommandHandler(INewsRepository service, ICurrentUserAccessor currentUser, ISystemClock clock) + public DeleteNewsCommandHandler( + IRepository repo, + ICceDbContext db, + ICurrentUserAccessor currentUser, + ISystemClock clock, + MessageFactory messages) { - _service = service; + _repo = repo; + _db = db; _currentUser = currentUser; _clock = clock; + _messages = messages; } - public async Task Handle(DeleteNewsCommand request, CancellationToken cancellationToken) + public async Task> Handle(DeleteNewsCommand request, CancellationToken cancellationToken) { - var news = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); + var news = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (news is null) - { - throw new System.Collections.Generic.KeyNotFoundException($"News {request.Id} not found."); - } + return _messages.NewsNotFound(); - var deletedById = _currentUser.GetUserId() - ?? throw new DomainException("Cannot delete news from a request without a user identity."); + var userId = _currentUser.GetUserId(); + if (userId is null) + return _messages.NotAuthenticated(); - news.SoftDelete(deletedById, _clock); - await _service.UpdateAsync(news, news.RowVersion, cancellationToken).ConfigureAwait(false); - return Unit.Value; + news.SoftDelete(userId.Value, _clock); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return _messages.Ok("CONTENT_DELETED"); } } diff --git a/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommand.cs b/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommand.cs new file mode 100644 index 00000000..c9c1ea0e --- /dev/null +++ b/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommand.cs @@ -0,0 +1,6 @@ +using CCE.Application.Common; +using MediatR; + +namespace CCE.Application.Content.Commands.DeleteResource; + +public sealed record DeleteResourceCommand(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommandHandler.cs new file mode 100644 index 00000000..5d48354e --- /dev/null +++ b/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommandHandler.cs @@ -0,0 +1,47 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Messages; +using CCE.Domain.Common; +using CCE.Domain.Content; +using MediatR; + +namespace CCE.Application.Content.Commands.DeleteResource; + +public sealed class DeleteResourceCommandHandler : IRequestHandler> +{ + private readonly IRepository _repo; + private readonly ICceDbContext _db; + private readonly ICurrentUserAccessor _currentUser; + private readonly ISystemClock _clock; + private readonly MessageFactory _messages; + + public DeleteResourceCommandHandler( + IRepository repo, + ICceDbContext db, + ICurrentUserAccessor currentUser, + ISystemClock clock, + MessageFactory messages) + { + _repo = repo; + _db = db; + _currentUser = currentUser; + _clock = clock; + _messages = messages; + } + + public async Task> Handle(DeleteResourceCommand request, CancellationToken cancellationToken) + { + var resource = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); + if (resource is null) + return _messages.ResourceNotFound(); + + var userId = _currentUser.GetUserId(); + if (userId is null) + return _messages.NotAuthenticated(); + + resource.SoftDelete(userId.Value, _clock); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return _messages.Ok("RESOURCE_DELETED"); + } +} diff --git a/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommand.cs b/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommand.cs index 6b2164c4..c8f6b2a1 100644 --- a/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommand.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; namespace CCE.Application.Content.Commands.PublishNews; -public sealed record PublishNewsCommand(System.Guid Id) : IRequest; +public sealed record PublishNewsCommand(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommandHandler.cs index ac711f02..408d7e23 100644 --- a/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommandHandler.cs @@ -1,34 +1,45 @@ -using CCE.Application.Content; +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; using CCE.Application.Content.Dtos; -using CCE.Application.Content.Queries.ListNews; +using CCE.Application.Content.Queries.GetNewsById; +using CCE.Application.Messages; using CCE.Domain.Common; +using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Commands.PublishNews; -public sealed class PublishNewsCommandHandler : IRequestHandler +public sealed class PublishNewsCommandHandler : IRequestHandler> { - private readonly INewsRepository _service; + private readonly IRepository _repo; + private readonly ICceDbContext _db; private readonly ISystemClock _clock; + private readonly MessageFactory _messages; - public PublishNewsCommandHandler(INewsRepository service, ISystemClock clock) + public PublishNewsCommandHandler( + IRepository repo, + ICceDbContext db, + ISystemClock clock, + MessageFactory messages) { - _service = service; + _repo = repo; + _db = db; _clock = clock; + _messages = messages; } - public async Task Handle(PublishNewsCommand request, CancellationToken cancellationToken) + public async Task> Handle(PublishNewsCommand request, CancellationToken cancellationToken) { - var news = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); + var news = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (news is null) - { - return null; - } + return _messages.NewsNotFound(); var expectedRowVersion = news.RowVersion; news.Publish(_clock); - await _service.UpdateAsync(news, expectedRowVersion, cancellationToken).ConfigureAwait(false); - return ListNewsQueryHandler.MapToDto(news); + _db.SetExpectedRowVersion(news, expectedRowVersion); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return _messages.Ok(GetNewsByIdQueryHandler.MapToDto(news), "SUCCESS_OPERATION"); } } diff --git a/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommand.cs b/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommand.cs index 38eccfdd..6682061c 100644 --- a/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommand.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; namespace CCE.Application.Content.Commands.PublishResource; -public sealed record PublishResourceCommand(System.Guid Id) : IRequest; +public sealed record PublishResourceCommand(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommandHandler.cs index a32db6e8..fcdf3505 100644 --- a/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommandHandler.cs @@ -1,65 +1,87 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Content; +using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; using MediatR; +using Microsoft.EntityFrameworkCore; namespace CCE.Application.Content.Commands.PublishResource; -public sealed class PublishResourceCommandHandler : IRequestHandler +public sealed class PublishResourceCommandHandler : IRequestHandler> { - private readonly IResourceRepository _service; - private readonly IAssetRepository _assetService; + private readonly ICceDbContext _db; private readonly ISystemClock _clock; + private readonly MessageFactory _messages; public PublishResourceCommandHandler( - IResourceRepository service, - IAssetRepository assetService, - ISystemClock clock) + ICceDbContext db, + ISystemClock clock, + MessageFactory messages) { - _service = service; - _assetService = assetService; + _db = db; _clock = clock; + _messages = messages; } - public async Task Handle(PublishResourceCommand request, CancellationToken cancellationToken) + public async Task> Handle(PublishResourceCommand request, CancellationToken cancellationToken) { - var resource = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); + var resources = await _db.Resources + .Include(r => r.Countries) + .Where(r => r.Id == request.Id) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var resource = resources.SingleOrDefault(); if (resource is null) - { - return null; - } + return _messages.ResourceNotFound(); - var asset = await _assetService.GetByIdAsync(resource.AssetFileId, cancellationToken).ConfigureAwait(false); + var assets = await _db.AssetFiles + .Where(a => a.Id == resource.AssetFileId) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var asset = assets.SingleOrDefault(); if (asset is null) - { - throw new DomainException($"Asset {resource.AssetFileId} not found for resource {resource.Id}."); - } + return _messages.AssetNotFound(); if (asset.VirusScanStatus != VirusScanStatus.Clean) - { - throw new DomainException($"Cannot publish resource {resource.Id}: asset has not passed virus scan ({asset.VirusScanStatus})."); - } + return _messages.AssetNotClean(); var expectedRowVersion = resource.RowVersion; resource.Publish(_clock); - await _service.UpdateAsync(resource, expectedRowVersion, cancellationToken).ConfigureAwait(false); + + _db.SetExpectedRowVersion(resource, expectedRowVersion); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + var dto = MapToDto(resource); + return _messages.Ok(dto, "SUCCESS_OPERATION"); + } + + private ResourceDto MapToDto(Resource r) + { + var category = _db.ResourceCategories.FirstOrDefault(c => c.Id == r.CategoryId); + var asset = _db.AssetFiles.FirstOrDefault(a => a.Id == r.AssetFileId); + var countryIds = r.Countries.Select(c => c.CountryId).ToList(); + var countries = _db.Countries.Where(c => countryIds.Contains(c.Id)).ToList(); return new ResourceDto( - resource.Id, - resource.TitleAr, - resource.TitleEn, - resource.DescriptionAr, - resource.DescriptionEn, - resource.ResourceType, - resource.CategoryId, - resource.CountryId, - resource.UploadedById, - resource.AssetFileId, - resource.PublishedOn, - resource.ViewCount, - resource.IsCenterManaged, - resource.IsPublished, - System.Convert.ToBase64String(resource.RowVersion)); + r.Id, + r.TitleAr, + r.TitleEn, + r.DescriptionAr, + r.DescriptionEn, + r.ResourceType, + r.CategoryId, + category?.NameAr ?? string.Empty, + category?.NameEn ?? string.Empty, + r.AssetFileId, + asset?.OriginalFileName ?? string.Empty, + countryIds, + countries.Select(c => c.NameAr).ToList(), + r.UploadedById, + r.PublishedOn, + r.ViewCount, + r.IsCenterManaged, + r.IsPublished); } } diff --git a/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommand.cs b/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommand.cs index 52587e73..2cc267af 100644 --- a/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; @@ -6,5 +7,4 @@ namespace CCE.Application.Content.Commands.RescheduleEvent; public sealed record RescheduleEventCommand( System.Guid Id, System.DateTimeOffset StartsOn, - System.DateTimeOffset EndsOn, - byte[] RowVersion) : IRequest; + System.DateTimeOffset EndsOn) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandHandler.cs index 8d87af69..6d33c647 100644 --- a/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandHandler.cs @@ -1,30 +1,42 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; using CCE.Application.Content.Dtos; -using CCE.Application.Content.Queries.ListEvents; +using CCE.Application.Content.Queries.GetEventById; +using CCE.Application.Messages; +using CCE.Domain.Common; +using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Commands.RescheduleEvent; -public sealed class RescheduleEventCommandHandler : IRequestHandler +public sealed class RescheduleEventCommandHandler : IRequestHandler> { - private readonly IEventRepository _service; + private readonly IRepository _repo; + private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public RescheduleEventCommandHandler(IEventRepository service) + public RescheduleEventCommandHandler( + IRepository repo, + ICceDbContext db, + MessageFactory messages) { - _service = service; + _repo = repo; + _db = db; + _messages = messages; } - public async Task Handle(RescheduleEventCommand request, CancellationToken cancellationToken) + public async Task> Handle(RescheduleEventCommand request, CancellationToken cancellationToken) { - var ev = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); + var ev = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (ev is null) - { - return null; - } + return _messages.EventNotFound(); + var expectedRowVersion = ev.RowVersion; ev.Reschedule(request.StartsOn, request.EndsOn); - await _service.UpdateAsync(ev, request.RowVersion, cancellationToken).ConfigureAwait(false); + _db.SetExpectedRowVersion(ev, expectedRowVersion); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return ListEventsQueryHandler.MapToDto(ev); + return _messages.Ok(GetEventByIdQueryHandler.MapToDto(ev), "SUCCESS_OPERATION"); } } diff --git a/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandValidator.cs b/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandValidator.cs index a5baeb9e..f9c57a2b 100644 --- a/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandValidator.cs +++ b/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandValidator.cs @@ -8,6 +8,5 @@ public RescheduleEventCommandValidator() { RuleFor(x => x.Id).NotEmpty(); RuleFor(x => x.EndsOn).GreaterThan(x => x.StartsOn); - RuleFor(x => x.RowVersion).NotNull().Must(rv => rv.Length == 8); } } diff --git a/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommand.cs b/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommand.cs index 6acf7498..05a9cd59 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; @@ -11,4 +12,4 @@ public sealed record UpdateEventCommand( string? LocationEn, string? OnlineMeetingUrl, string? FeaturedImageUrl, - byte[] RowVersion) : IRequest; + System.Guid TopicId) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandHandler.cs index a38f0072..37d006a0 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandHandler.cs @@ -1,26 +1,42 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; -using CCE.Application.Content.Queries.ListEvents; +using CCE.Application.Content.Queries.GetEventById; +using CCE.Application.Messages; +using CCE.Domain.Common; +using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Commands.UpdateEvent; -public sealed class UpdateEventCommandHandler : IRequestHandler +public sealed class UpdateEventCommandHandler : IRequestHandler> { - private readonly IEventRepository _service; + private readonly IRepository _repo; + private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public UpdateEventCommandHandler(IEventRepository service) + public UpdateEventCommandHandler( + IRepository repo, + ICceDbContext db, + MessageFactory messages) { - _service = service; + _repo = repo; + _db = db; + _messages = messages; } - public async Task Handle(UpdateEventCommand request, CancellationToken cancellationToken) + public async Task> Handle(UpdateEventCommand request, CancellationToken cancellationToken) { - var ev = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); + var ev = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (ev is null) - { - return null; - } + return _messages.EventNotFound(); + var topicExists = await _db.Topics.Where(t => t.Id == request.TopicId).CountAsyncEither(cancellationToken) > 0; + if (!topicExists) + return _messages.NotFound("TOPIC_NOT_FOUND"); + + var expectedRowVersion = ev.RowVersion; ev.UpdateContent( request.TitleAr, request.TitleEn, @@ -29,10 +45,17 @@ public UpdateEventCommandHandler(IEventRepository service) request.LocationAr, request.LocationEn, request.OnlineMeetingUrl, - request.FeaturedImageUrl); + request.FeaturedImageUrl, + request.TopicId); + + _db.SetExpectedRowVersion(ev, expectedRowVersion); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - await _service.UpdateAsync(ev, request.RowVersion, cancellationToken).ConfigureAwait(false); + var topic = await _db.Topics.Where(t => t.Id == request.TopicId) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + var topicNameAr = topic.FirstOrDefault()?.NameAr ?? string.Empty; + var topicNameEn = topic.FirstOrDefault()?.NameEn ?? string.Empty; - return ListEventsQueryHandler.MapToDto(ev); + return _messages.Ok(GetEventByIdQueryHandler.MapToDto(ev, topicNameAr, topicNameEn), "SUCCESS_OPERATION"); } } diff --git a/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandValidator.cs b/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandValidator.cs index cab6c277..eb93f60b 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandValidator.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandValidator.cs @@ -11,7 +11,6 @@ public UpdateEventCommandValidator() RuleFor(x => x.TitleEn).NotEmpty().MaximumLength(500); RuleFor(x => x.DescriptionAr).NotEmpty(); RuleFor(x => x.DescriptionEn).NotEmpty(); - RuleFor(x => x.RowVersion).NotNull().Must(rv => rv.Length == 8) - .WithMessage("RowVersion must be exactly 8 bytes."); + RuleFor(x => x.TopicId).NotEmpty(); } } diff --git a/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommand.cs b/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommand.cs index 9054c9b3..53680da1 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; @@ -7,6 +8,5 @@ public sealed record UpdateNewsCommand( System.Guid Id, string TitleAr, string TitleEn, string ContentAr, string ContentEn, - string Slug, - string? FeaturedImageUrl, - byte[] RowVersion) : IRequest; + System.Guid TopicId, + string? FeaturedImageUrl) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandHandler.cs index fcb8ad2f..3bae6718 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandHandler.cs @@ -1,36 +1,58 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; -using CCE.Application.Content.Queries.ListNews; +using CCE.Application.Content.Queries.GetNewsById; +using CCE.Application.Messages; +using CCE.Domain.Common; +using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Commands.UpdateNews; -public sealed class UpdateNewsCommandHandler : IRequestHandler +public sealed class UpdateNewsCommandHandler : IRequestHandler> { - private readonly INewsRepository _service; + private readonly IRepository _repo; + private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public UpdateNewsCommandHandler(INewsRepository service) + public UpdateNewsCommandHandler( + IRepository repo, + ICceDbContext db, + MessageFactory messages) { - _service = service; + _repo = repo; + _db = db; + _messages = messages; } - public async Task Handle(UpdateNewsCommand request, CancellationToken cancellationToken) + public async Task> Handle(UpdateNewsCommand request, CancellationToken cancellationToken) { - var news = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); + var news = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (news is null) - { - return null; - } + return _messages.NewsNotFound(); + var topicExists = await _db.Topics.Where(t => t.Id == request.TopicId).CountAsyncEither(cancellationToken) > 0; + if (!topicExists) + return _messages.NotFound("TOPIC_NOT_FOUND"); + + var expectedRowVersion = news.RowVersion; news.UpdateContent( request.TitleAr, request.TitleEn, request.ContentAr, request.ContentEn, - request.Slug, + request.TopicId, request.FeaturedImageUrl); - await _service.UpdateAsync(news, request.RowVersion, cancellationToken).ConfigureAwait(false); + _db.SetExpectedRowVersion(news, expectedRowVersion); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + var topic = await _db.Topics.Where(t => t.Id == request.TopicId) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + var topicNameAr = topic.FirstOrDefault()?.NameAr ?? string.Empty; + var topicNameEn = topic.FirstOrDefault()?.NameEn ?? string.Empty; - return ListNewsQueryHandler.MapToDto(news); + return _messages.Ok(GetNewsByIdQueryHandler.MapToDto(news, topicNameAr, topicNameEn), "SUCCESS_OPERATION"); } } diff --git a/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandValidator.cs b/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandValidator.cs index e8ba0004..1accfc09 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandValidator.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandValidator.cs @@ -11,9 +11,6 @@ public UpdateNewsCommandValidator() RuleFor(x => x.TitleEn).NotEmpty().MaximumLength(500); RuleFor(x => x.ContentAr).NotEmpty(); RuleFor(x => x.ContentEn).NotEmpty(); - RuleFor(x => x.Slug).NotEmpty().MaximumLength(200) - .Matches("^[a-z0-9]+(-[a-z0-9]+)*$").WithMessage("Slug must be kebab-case."); - RuleFor(x => x.RowVersion).NotNull().Must(rv => rv.Length == 8) - .WithMessage("RowVersion must be exactly 8 bytes."); + RuleFor(x => x.TopicId).NotEmpty(); } } diff --git a/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommand.cs b/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommand.cs index fc74b2af..b16b43ea 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using CCE.Domain.Content; using MediatR; @@ -12,4 +13,4 @@ public sealed record UpdateResourceCommand( string DescriptionEn, ResourceType ResourceType, System.Guid CategoryId, - byte[] RowVersion) : IRequest; + IReadOnlyList CountryIds) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandHandler.cs index 70781688..4bfc3bf0 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandHandler.cs @@ -1,51 +1,102 @@ -using CCE.Application.Content; +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; +using CCE.Application.Messages; +using CCE.Domain.Common; +using CCE.Domain.Content; using MediatR; +using Microsoft.EntityFrameworkCore; namespace CCE.Application.Content.Commands.UpdateResource; -public sealed class UpdateResourceCommandHandler : IRequestHandler +public sealed class UpdateResourceCommandHandler : IRequestHandler> { - private readonly IResourceRepository _service; + private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public UpdateResourceCommandHandler(IResourceRepository service) + public UpdateResourceCommandHandler( + ICceDbContext db, + MessageFactory messages) { - _service = service; + _db = db; + _messages = messages; } - public async Task Handle(UpdateResourceCommand request, CancellationToken cancellationToken) + public async Task> Handle(UpdateResourceCommand request, CancellationToken cancellationToken) { - var resource = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); + var resources = await _db.Resources + .Include(r => r.Countries) + .Where(r => r.Id == request.Id) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var resource = resources.SingleOrDefault(); if (resource is null) + return _messages.ResourceNotFound(); + + var categoryExists = await ExistsAsync(_db.ResourceCategories.Where(c => c.Id == request.CategoryId), cancellationToken).ConfigureAwait(false); + if (!categoryExists) + return _messages.CategoryNotFound(); + + var countryIds = request.CountryIds.Distinct().ToList(); + if (countryIds.Count > 0) { - return null; + var existingCountryCount = await _db.Countries + .Where(c => countryIds.Contains(c.Id)) + .CountAsyncEither(cancellationToken) + .ConfigureAwait(false); + if (existingCountryCount != countryIds.Count) + return _messages.NotFound("COUNTRY_NOT_FOUND"); } + var expectedRowVersion = resource.RowVersion; resource.UpdateContent( request.TitleAr, request.TitleEn, request.DescriptionAr, request.DescriptionEn, request.ResourceType, - request.CategoryId); + request.CategoryId, + request.CountryIds); - await _service.UpdateAsync(resource, request.RowVersion, cancellationToken).ConfigureAwait(false); + _db.SetExpectedRowVersion(resource, expectedRowVersion); + await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + var dto = MapToDto(resource); + return _messages.Ok(dto, "SUCCESS_OPERATION"); + } + + private ResourceDto MapToDto(Resource r) + { + var category = _db.ResourceCategories.FirstOrDefault(c => c.Id == r.CategoryId); + var asset = _db.AssetFiles.FirstOrDefault(a => a.Id == r.AssetFileId); + var countryIds = r.Countries.Select(c => c.CountryId).ToList(); + var countries = _db.Countries.Where(c => countryIds.Contains(c.Id)).ToList(); return new ResourceDto( - resource.Id, - resource.TitleAr, - resource.TitleEn, - resource.DescriptionAr, - resource.DescriptionEn, - resource.ResourceType, - resource.CategoryId, - resource.CountryId, - resource.UploadedById, - resource.AssetFileId, - resource.PublishedOn, - resource.ViewCount, - resource.IsCenterManaged, - resource.IsPublished, - System.Convert.ToBase64String(resource.RowVersion)); + r.Id, + r.TitleAr, + r.TitleEn, + r.DescriptionAr, + r.DescriptionEn, + r.ResourceType, + r.CategoryId, + category?.NameAr ?? string.Empty, + category?.NameEn ?? string.Empty, + r.AssetFileId, + asset?.OriginalFileName ?? string.Empty, + countryIds, + countries.Select(c => c.NameAr).ToList(), + r.UploadedById, + r.PublishedOn, + r.ViewCount, + r.IsCenterManaged, + r.IsPublished); + } + + private static async Task ExistsAsync(IQueryable query, CancellationToken ct) + { + var list = await query.Take(1).ToListAsyncEither(ct).ConfigureAwait(false); + return list.Count > 0; } } diff --git a/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandValidator.cs b/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandValidator.cs index 7272e3d1..896d15ec 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandValidator.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandValidator.cs @@ -7,12 +7,11 @@ public sealed class UpdateResourceCommandValidator : AbstractValidator x.Id).NotEmpty(); - RuleFor(x => x.TitleAr).NotEmpty().MaximumLength(500); - RuleFor(x => x.TitleEn).NotEmpty().MaximumLength(500); - RuleFor(x => x.DescriptionAr).NotEmpty().MaximumLength(4000); - RuleFor(x => x.DescriptionEn).NotEmpty().MaximumLength(4000); + RuleFor(x => x.TitleAr).NotEmpty().MaximumLength(255); + RuleFor(x => x.TitleEn).NotEmpty().MaximumLength(255); + RuleFor(x => x.DescriptionAr).NotEmpty().MaximumLength(500); + RuleFor(x => x.DescriptionEn).NotEmpty().MaximumLength(500); RuleFor(x => x.CategoryId).NotEmpty(); - RuleFor(x => x.RowVersion).NotNull().Must(rv => rv.Length == 8) - .WithMessage("RowVersion must be exactly 8 bytes."); + RuleFor(x => x.CountryIds).NotEmpty().ForEach(x => x.NotEmpty()); } } diff --git a/backend/src/CCE.Application/Content/Dtos/EventDto.cs b/backend/src/CCE.Application/Content/Dtos/EventDto.cs index 2f2820a4..e348ca0d 100644 --- a/backend/src/CCE.Application/Content/Dtos/EventDto.cs +++ b/backend/src/CCE.Application/Content/Dtos/EventDto.cs @@ -10,4 +10,6 @@ public sealed record EventDto( string? OnlineMeetingUrl, string? FeaturedImageUrl, string ICalUid, - string RowVersion); + System.Guid TopicId, + string TopicNameAr, + string TopicNameEn); diff --git a/backend/src/CCE.Application/Content/Dtos/NewsDto.cs b/backend/src/CCE.Application/Content/Dtos/NewsDto.cs index 5704c85d..ba18b79e 100644 --- a/backend/src/CCE.Application/Content/Dtos/NewsDto.cs +++ b/backend/src/CCE.Application/Content/Dtos/NewsDto.cs @@ -6,10 +6,11 @@ public sealed record NewsDto( string TitleEn, string ContentAr, string ContentEn, - string Slug, + System.Guid TopicId, + string TopicNameAr, + string TopicNameEn, System.Guid AuthorId, string? FeaturedImageUrl, System.DateTimeOffset? PublishedOn, bool IsFeatured, - bool IsPublished, - string RowVersion); + bool IsPublished); diff --git a/backend/src/CCE.Application/Content/Dtos/ResourceDto.cs b/backend/src/CCE.Application/Content/Dtos/ResourceDto.cs index d8b62d92..a4805329 100644 --- a/backend/src/CCE.Application/Content/Dtos/ResourceDto.cs +++ b/backend/src/CCE.Application/Content/Dtos/ResourceDto.cs @@ -10,11 +10,14 @@ public sealed record ResourceDto( string DescriptionEn, ResourceType ResourceType, System.Guid CategoryId, - System.Guid? CountryId, - System.Guid UploadedById, + string CategoryNameAr, + string CategoryNameEn, System.Guid AssetFileId, + string AssetFileName, + IReadOnlyList CountryIds, + IReadOnlyList CountryNames, + System.Guid UploadedById, System.DateTimeOffset? PublishedOn, long ViewCount, bool IsCenterManaged, - bool IsPublished, - string RowVersion); + bool IsPublished); diff --git a/backend/src/CCE.Application/Content/Public/Dtos/PublicEventDto.cs b/backend/src/CCE.Application/Content/Public/Dtos/PublicEventDto.cs index 7db26728..5f0cb77f 100644 --- a/backend/src/CCE.Application/Content/Public/Dtos/PublicEventDto.cs +++ b/backend/src/CCE.Application/Content/Public/Dtos/PublicEventDto.cs @@ -12,4 +12,7 @@ public sealed record PublicEventDto( string? LocationEn, string? OnlineMeetingUrl, string? FeaturedImageUrl, - string ICalUid); + string ICalUid, + System.Guid TopicId, + string TopicNameAr, + string TopicNameEn); diff --git a/backend/src/CCE.Application/Content/Public/Dtos/PublicNewsDto.cs b/backend/src/CCE.Application/Content/Public/Dtos/PublicNewsDto.cs index 4cb6c9d5..b4fb0e9b 100644 --- a/backend/src/CCE.Application/Content/Public/Dtos/PublicNewsDto.cs +++ b/backend/src/CCE.Application/Content/Public/Dtos/PublicNewsDto.cs @@ -6,7 +6,9 @@ public sealed record PublicNewsDto( string TitleEn, string ContentAr, string ContentEn, - string Slug, + System.Guid TopicId, + string TopicNameAr, + string TopicNameEn, string? FeaturedImageUrl, System.DateTimeOffset PublishedOn, bool IsFeatured); diff --git a/backend/src/CCE.Application/Content/Public/Dtos/PublicResourceDto.cs b/backend/src/CCE.Application/Content/Public/Dtos/PublicResourceDto.cs index 577736d4..787a49c9 100644 --- a/backend/src/CCE.Application/Content/Public/Dtos/PublicResourceDto.cs +++ b/backend/src/CCE.Application/Content/Public/Dtos/PublicResourceDto.cs @@ -10,7 +10,11 @@ public sealed record PublicResourceDto( string DescriptionEn, ResourceType ResourceType, System.Guid CategoryId, - System.Guid? CountryId, + string CategoryNameAr, + string CategoryNameEn, System.Guid AssetFileId, + string AssetFileName, + IReadOnlyList CountryIds, + IReadOnlyList CountryNames, System.DateTimeOffset PublishedOn, long ViewCount); diff --git a/backend/src/CCE.Application/Content/Public/Queries/GetPublicEventById/GetPublicEventByIdQuery.cs b/backend/src/CCE.Application/Content/Public/Queries/GetPublicEventById/GetPublicEventByIdQuery.cs index 4d0a2f0d..eb010618 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicEventById/GetPublicEventByIdQuery.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicEventById/GetPublicEventByIdQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Content.Public.Dtos; using MediatR; namespace CCE.Application.Content.Public.Queries.GetPublicEventById; -public sealed record GetPublicEventByIdQuery(System.Guid Id) : IRequest; +public sealed record GetPublicEventByIdQuery(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Public/Queries/GetPublicEventById/GetPublicEventByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/GetPublicEventById/GetPublicEventByIdQueryHandler.cs index a11fe47b..e6801557 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicEventById/GetPublicEventByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicEventById/GetPublicEventByIdQueryHandler.cs @@ -1,28 +1,42 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; +using CCE.Application.Messages; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Public.Queries.GetPublicEventById; -public sealed class GetPublicEventByIdQueryHandler : IRequestHandler +public sealed class GetPublicEventByIdQueryHandler : IRequestHandler> { private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public GetPublicEventByIdQueryHandler(ICceDbContext db) => _db = db; + public GetPublicEventByIdQueryHandler(ICceDbContext db, MessageFactory messages) + { + _db = db; + _messages = messages; + } - public async Task Handle(GetPublicEventByIdQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetPublicEventByIdQuery request, CancellationToken cancellationToken) { var list = await _db.Events .Where(e => e.Id == request.Id) .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); var ev = list.SingleOrDefault(); - return ev is null ? null : MapToDto(ev); + if (ev is null) + return _messages.EventNotFound(); + + var topics = await _db.Topics.Where(t => t.Id == ev.TopicId) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + var topic = topics.FirstOrDefault(); + + return _messages.Ok(MapToDto(ev, topic?.NameAr ?? string.Empty, topic?.NameEn ?? string.Empty), "SUCCESS_OPERATION"); } - internal static PublicEventDto MapToDto(Event e) => new( + internal static PublicEventDto MapToDto(Event e, string topicNameAr, string topicNameEn) => new( e.Id, e.TitleAr, e.TitleEn, @@ -34,5 +48,8 @@ public sealed class GetPublicEventByIdQueryHandler : IRequestHandler; +public sealed record GetPublicNewsBySlugQuery(string Slug) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Public/Queries/GetPublicNewsBySlug/GetPublicNewsBySlugQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/GetPublicNewsBySlug/GetPublicNewsBySlugQueryHandler.cs index 19d6616b..0cbfb477 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicNewsBySlug/GetPublicNewsBySlugQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicNewsBySlug/GetPublicNewsBySlugQueryHandler.cs @@ -1,34 +1,50 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; +using CCE.Application.Messages; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Public.Queries.GetPublicNewsBySlug; -public sealed class GetPublicNewsBySlugQueryHandler : IRequestHandler +public sealed class GetPublicNewsBySlugQueryHandler : IRequestHandler> { private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public GetPublicNewsBySlugQueryHandler(ICceDbContext db) => _db = db; + public GetPublicNewsBySlugQueryHandler(ICceDbContext db, MessageFactory messages) + { + _db = db; + _messages = messages; + } - public async Task Handle(GetPublicNewsBySlugQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetPublicNewsBySlugQuery request, CancellationToken cancellationToken) { var list = await _db.News .Where(n => n.Slug == request.Slug && n.PublishedOn != null) .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); var news = list.SingleOrDefault(); - return news is null ? null : MapToDto(news); + if (news is null) + return _messages.NewsNotFound(); + + var topics = await _db.Topics.Where(t => t.Id == news.TopicId) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + var topic = topics.FirstOrDefault(); + + return _messages.Ok(MapToDto(news, topic?.NameAr ?? string.Empty, topic?.NameEn ?? string.Empty), "SUCCESS_OPERATION"); } - internal static PublicNewsDto MapToDto(News n) => new( + internal static PublicNewsDto MapToDto(News n, string topicNameAr, string topicNameEn) => new( n.Id, n.TitleAr, n.TitleEn, n.ContentAr, n.ContentEn, - n.Slug, + n.TopicId, + topicNameAr, + topicNameEn, n.FeaturedImageUrl, n.PublishedOn!.Value, n.IsFeatured); diff --git a/backend/src/CCE.Application/Content/Public/Queries/GetPublicResourceById/GetPublicResourceByIdQuery.cs b/backend/src/CCE.Application/Content/Public/Queries/GetPublicResourceById/GetPublicResourceByIdQuery.cs index 2b0390a6..f3a15a51 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicResourceById/GetPublicResourceByIdQuery.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicResourceById/GetPublicResourceByIdQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Content.Public.Dtos; using MediatR; namespace CCE.Application.Content.Public.Queries.GetPublicResourceById; -public sealed record GetPublicResourceByIdQuery(System.Guid Id) : IRequest; +public sealed record GetPublicResourceByIdQuery(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Public/Queries/GetPublicResourceById/GetPublicResourceByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/GetPublicResourceById/GetPublicResourceByIdQueryHandler.cs index 46589899..05998dee 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicResourceById/GetPublicResourceByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicResourceById/GetPublicResourceByIdQueryHandler.cs @@ -1,41 +1,61 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; +using CCE.Application.Messages; using CCE.Domain.Content; using MediatR; +using Microsoft.EntityFrameworkCore; namespace CCE.Application.Content.Public.Queries.GetPublicResourceById; -public sealed class GetPublicResourceByIdQueryHandler : IRequestHandler +public sealed class GetPublicResourceByIdQueryHandler : IRequestHandler> { private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public GetPublicResourceByIdQueryHandler(ICceDbContext db) => _db = db; + public GetPublicResourceByIdQueryHandler(ICceDbContext db, MessageFactory messages) + { + _db = db; + _messages = messages; + } - public async Task Handle(GetPublicResourceByIdQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetPublicResourceByIdQuery request, CancellationToken cancellationToken) { var list = await _db.Resources + .AsNoTracking() + .Include(r => r.Countries) .Where(r => r.Id == request.Id) .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); var resource = list.SingleOrDefault(); if (resource is null || resource.PublishedOn is null) - { - return null; - } - return MapToDto(resource); + return _messages.ResourceNotFound(); + return _messages.Ok(MapToDto(resource), "SUCCESS_OPERATION"); } - internal static PublicResourceDto MapToDto(Resource r) => new( - r.Id, - r.TitleAr, - r.TitleEn, - r.DescriptionAr, - r.DescriptionEn, - r.ResourceType, - r.CategoryId, - r.CountryId, - r.AssetFileId, - r.PublishedOn!.Value, - r.ViewCount); + private PublicResourceDto MapToDto(Resource r) + { + var category = _db.ResourceCategories.FirstOrDefault(c => c.Id == r.CategoryId); + var asset = _db.AssetFiles.FirstOrDefault(a => a.Id == r.AssetFileId); + var countryIds = r.Countries.Select(c => c.CountryId).ToList(); + var countries = _db.Countries.Where(c => countryIds.Contains(c.Id)).ToList(); + + return new PublicResourceDto( + r.Id, + r.TitleAr, + r.TitleEn, + r.DescriptionAr, + r.DescriptionEn, + r.ResourceType, + r.CategoryId, + category?.NameAr ?? string.Empty, + category?.NameEn ?? string.Empty, + r.AssetFileId, + asset?.OriginalFileName ?? string.Empty, + countryIds, + countries.Select(c => c.NameAr).ToList(), + r.PublishedOn!.Value, + r.ViewCount); + } } diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListPublicEvents/ListPublicEventsQuery.cs b/backend/src/CCE.Application/Content/Public/Queries/ListPublicEvents/ListPublicEventsQuery.cs index 62d0aaec..340400ca 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicEvents/ListPublicEventsQuery.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicEvents/ListPublicEventsQuery.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; using MediatR; @@ -8,4 +9,6 @@ public sealed record ListPublicEventsQuery( int Page = 1, int PageSize = 20, System.DateTimeOffset? From = null, - System.DateTimeOffset? To = null) : IRequest>; + System.DateTimeOffset? To = null, + System.Guid? TopicId = null, + string? TopicSlug = null) : IRequest>>; diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListPublicEvents/ListPublicEventsQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/ListPublicEvents/ListPublicEventsQueryHandler.cs index bbeb6e26..1cd36898 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicEvents/ListPublicEventsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicEvents/ListPublicEventsQueryHandler.cs @@ -1,19 +1,35 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; +using CCE.Application.Messages; +using CCE.Domain.Community; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Public.Queries.ListPublicEvents; -public sealed class ListPublicEventsQueryHandler : IRequestHandler> +public sealed class ListPublicEventsQueryHandler : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public ListPublicEventsQueryHandler(ICceDbContext db) => _db = db; + public ListPublicEventsQueryHandler(ICceDbContext db, MessageFactory messages) + { + _db = db; + _messages = messages; + } - public async Task> Handle(ListPublicEventsQuery request, CancellationToken cancellationToken) + public async Task>> Handle(ListPublicEventsQuery request, CancellationToken cancellationToken) { + System.Guid? topicId = request.TopicId; + if (!string.IsNullOrWhiteSpace(request.TopicSlug) && !topicId.HasValue) + { + var topics = await _db.Topics.Where(t => t.Slug == request.TopicSlug!) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + topicId = topics.FirstOrDefault()?.Id; + } + var query = _db.Events.AsQueryable(); if (request.From.HasValue && request.To.HasValue) @@ -26,13 +42,20 @@ public async Task> Handle(ListPublicEventsQuery requ query = query.Where(e => e.StartsOn >= now); } + query = query.WhereIf(topicId.HasValue, e => e.TopicId == topicId!.Value); query = query.OrderBy(e => e.StartsOn); var result = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); - return result.Map(MapToDto); + + var topicIds = result.Items.Select(e => e.TopicId).Distinct().ToList(); + var topicsList = await _db.Topics.Where(t => topicIds.Contains(t.Id)) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + var topicById = topicsList.ToDictionary(t => t.Id); + + return _messages.Ok(result.Map(e => MapToDto(e, topicById)), "ITEMS_LISTED"); } - internal static PublicEventDto MapToDto(Event e) => new( + internal static PublicEventDto MapToDto(Event e, Dictionary topicById) => new( e.Id, e.TitleAr, e.TitleEn, @@ -44,5 +67,8 @@ public async Task> Handle(ListPublicEventsQuery requ e.LocationEn, e.OnlineMeetingUrl, e.FeaturedImageUrl, - e.ICalUid); + e.ICalUid, + e.TopicId, + topicById.TryGetValue(e.TopicId, out var t) ? t.NameAr : string.Empty, + topicById.TryGetValue(e.TopicId, out t) ? t.NameEn : string.Empty); } diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListPublicNews/ListPublicNewsQuery.cs b/backend/src/CCE.Application/Content/Public/Queries/ListPublicNews/ListPublicNewsQuery.cs index 6a8e014d..8d476530 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicNews/ListPublicNewsQuery.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicNews/ListPublicNewsQuery.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; using MediatR; @@ -7,4 +8,6 @@ namespace CCE.Application.Content.Public.Queries.ListPublicNews; public sealed record ListPublicNewsQuery( int Page = 1, int PageSize = 20, - bool? IsFeatured = null) : IRequest>; + bool? IsFeatured = null, + System.Guid? TopicId = null, + string? TopicSlug = null) : IRequest>>; diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListPublicNews/ListPublicNewsQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/ListPublicNews/ListPublicNewsQueryHandler.cs index 8bfd2e0e..d72c9e8e 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicNews/ListPublicNewsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicNews/ListPublicNewsQueryHandler.cs @@ -1,35 +1,60 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; +using CCE.Application.Messages; +using CCE.Domain.Community; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Public.Queries.ListPublicNews; -public sealed class ListPublicNewsQueryHandler : IRequestHandler> +public sealed class ListPublicNewsQueryHandler : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public ListPublicNewsQueryHandler(ICceDbContext db) => _db = db; + public ListPublicNewsQueryHandler(ICceDbContext db, MessageFactory messages) + { + _db = db; + _messages = messages; + } - public async Task> Handle(ListPublicNewsQuery request, CancellationToken cancellationToken) + public async Task>> Handle(ListPublicNewsQuery request, CancellationToken cancellationToken) { + System.Guid? topicId = request.TopicId; + if (!string.IsNullOrWhiteSpace(request.TopicSlug) && !topicId.HasValue) + { + var topics = await _db.Topics.Where(t => t.Slug == request.TopicSlug!) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + topicId = topics.FirstOrDefault()?.Id; + } + var query = _db.News .Where(n => n.PublishedOn != null) .WhereIf(request.IsFeatured.HasValue, n => n.IsFeatured == request.IsFeatured!.Value) + .WhereIf(topicId.HasValue, n => n.TopicId == topicId!.Value) .OrderByDescending(n => n.PublishedOn); var result = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); - return result.Map(MapToDto); + + var topicIds = result.Items.Select(n => n.TopicId).Distinct().ToList(); + var topicsList = await _db.Topics.Where(t => topicIds.Contains(t.Id)) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + var topicById = topicsList.ToDictionary(t => t.Id); + + return _messages.Ok(result.Map(n => MapToDto(n, topicById)), "ITEMS_LISTED"); } - internal static PublicNewsDto MapToDto(News n) => new( + internal static PublicNewsDto MapToDto(News n, Dictionary topicById) => new( n.Id, n.TitleAr, n.TitleEn, n.ContentAr, n.ContentEn, - n.Slug, + n.TopicId, + topicById.TryGetValue(n.TopicId, out var t) ? t.NameAr : string.Empty, + topicById.TryGetValue(n.TopicId, out t) ? t.NameEn : string.Empty, n.FeaturedImageUrl, n.PublishedOn!.Value, n.IsFeatured); diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListPublicResources/ListPublicResourcesQuery.cs b/backend/src/CCE.Application/Content/Public/Queries/ListPublicResources/ListPublicResourcesQuery.cs index 1e32d56b..78f15def 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicResources/ListPublicResourcesQuery.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicResources/ListPublicResourcesQuery.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; using CCE.Domain.Content; @@ -8,6 +9,7 @@ namespace CCE.Application.Content.Public.Queries.ListPublicResources; public sealed record ListPublicResourcesQuery( int Page = 1, int PageSize = 20, + string? Search = null, System.Guid? CategoryId = null, System.Guid? CountryId = null, - ResourceType? ResourceType = null) : IRequest>; + ResourceType? ResourceType = null) : IRequest>>; diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListPublicResources/ListPublicResourcesQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/ListPublicResources/ListPublicResourcesQueryHandler.cs index 801083b1..88c9fe51 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicResources/ListPublicResourcesQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicResources/ListPublicResourcesQueryHandler.cs @@ -1,40 +1,93 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; +using CCE.Application.Messages; using CCE.Domain.Content; using MediatR; +using Microsoft.EntityFrameworkCore; namespace CCE.Application.Content.Public.Queries.ListPublicResources; -public sealed class ListPublicResourcesQueryHandler : IRequestHandler> +public sealed class ListPublicResourcesQueryHandler : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public ListPublicResourcesQueryHandler(ICceDbContext db) => _db = db; + public ListPublicResourcesQueryHandler(ICceDbContext db, MessageFactory messages) + { + _db = db; + _messages = messages; + } - public async Task> Handle(ListPublicResourcesQuery request, CancellationToken cancellationToken) + public async Task>> Handle(ListPublicResourcesQuery request, CancellationToken cancellationToken) { var query = _db.Resources + .AsNoTracking() + .Include(r => r.Countries) .Where(r => r.PublishedOn != null) + .WhereIf(!string.IsNullOrWhiteSpace(request.Search), + r => r.TitleAr.Contains(request.Search!) || + r.TitleEn.Contains(request.Search!) || + r.DescriptionAr.Contains(request.Search!) || + r.DescriptionEn.Contains(request.Search!)) .WhereIf(request.CategoryId.HasValue, r => r.CategoryId == request.CategoryId!.Value) - .WhereIf(request.CountryId.HasValue, r => r.CountryId == request.CountryId!.Value) + .WhereIf(request.CountryId.HasValue, r => r.Countries.Any(c => c.CountryId == request.CountryId!.Value)) .WhereIf(request.ResourceType.HasValue, r => r.ResourceType == request.ResourceType!.Value) .OrderByDescending(r => r.PublishedOn); - var result = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); - return result.Map(MapToDto); - } + var paged = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); + + // Batch enrich categories / assets / country names for the page + var categoryIds = paged.Items.Select(r => r.CategoryId).Distinct().ToList(); + var assetIds = paged.Items.Select(r => r.AssetFileId).Distinct().ToList(); + var allCountryIds = paged.Items.SelectMany(r => r.Countries.Select(c => c.CountryId)).Distinct().ToList(); + + var categories = await _db.ResourceCategories + .Where(c => categoryIds.Contains(c.Id)) + .Select(c => new { c.Id, c.NameAr, c.NameEn }) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var categoryMap = categories.ToDictionary(c => c.Id, c => new { c.NameAr, c.NameEn }); - internal static PublicResourceDto MapToDto(Resource r) => new( - r.Id, - r.TitleAr, - r.TitleEn, - r.DescriptionAr, - r.DescriptionEn, - r.ResourceType, - r.CategoryId, - r.CountryId, - r.AssetFileId, - r.PublishedOn!.Value, - r.ViewCount); + var assets = await _db.AssetFiles + .Where(a => assetIds.Contains(a.Id)) + .Select(a => new { a.Id, a.OriginalFileName }) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var assetMap = assets.ToDictionary(a => a.Id, a => a.OriginalFileName); + + var countries = await _db.Countries + .Where(c => allCountryIds.Contains(c.Id)) + .Select(c => new { c.Id, c.NameAr }) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var countryNameMap = countries.ToDictionary(c => c.Id, c => c.NameAr); + + var dtos = paged.Items.Select(r => + { + var cat = categoryMap.GetValueOrDefault(r.CategoryId); + var countryIds = r.Countries.Select(c => c.CountryId).ToList(); + var countryNames = countryIds.Select(id => countryNameMap.GetValueOrDefault(id) ?? string.Empty).ToList(); + return new PublicResourceDto( + r.Id, + r.TitleAr, + r.TitleEn, + r.DescriptionAr, + r.DescriptionEn, + r.ResourceType, + r.CategoryId, + cat?.NameAr ?? string.Empty, + cat?.NameEn ?? string.Empty, + r.AssetFileId, + assetMap.GetValueOrDefault(r.AssetFileId) ?? string.Empty, + countryIds, + countryNames, + r.PublishedOn!.Value, + r.ViewCount); + }).ToList(); + + var result = new PagedResult(dtos, paged.Page, paged.PageSize, paged.Total); + return _messages.Ok(result, "ITEMS_LISTED"); + } } diff --git a/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQuery.cs b/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQuery.cs index 7a3792c2..57cc8a10 100644 --- a/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQuery.cs +++ b/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; namespace CCE.Application.Content.Queries.GetEventById; -public sealed record GetEventByIdQuery(System.Guid Id) : IRequest; +public sealed record GetEventByIdQuery(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQueryHandler.cs index d420d89c..9f86fe81 100644 --- a/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQueryHandler.cs @@ -1,27 +1,42 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; +using CCE.Application.Messages; +using CCE.Domain.Community; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Queries.GetEventById; -public sealed class GetEventByIdQueryHandler : IRequestHandler +public sealed class GetEventByIdQueryHandler : IRequestHandler> { private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public GetEventByIdQueryHandler(ICceDbContext db) => _db = db; + public GetEventByIdQueryHandler(ICceDbContext db, MessageFactory messages) + { + _db = db; + _messages = messages; + } - public async Task Handle(GetEventByIdQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetEventByIdQuery request, CancellationToken cancellationToken) { var list = await _db.Events.Where(e => e.Id == request.Id).ToListAsyncEither(cancellationToken).ConfigureAwait(false); var ev = list.SingleOrDefault(); - return ev is null ? null : MapToDto(ev); + if (ev is null) + return _messages.EventNotFound(); + + var topics = await _db.Topics.Where(t => t.Id == ev.TopicId) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + var topic = topics.FirstOrDefault(); + + return _messages.Ok(MapToDto(ev, topic?.NameAr ?? string.Empty, topic?.NameEn ?? string.Empty), "SUCCESS_OPERATION"); } - internal static EventDto MapToDto(Event e) => new( + internal static EventDto MapToDto(Event e, string topicNameAr = "", string topicNameEn = "") => new( e.Id, e.TitleAr, e.TitleEn, e.DescriptionAr, e.DescriptionEn, e.StartsOn, e.EndsOn, e.LocationAr, e.LocationEn, e.OnlineMeetingUrl, e.FeaturedImageUrl, e.ICalUid, - System.Convert.ToBase64String(e.RowVersion)); + e.TopicId, topicNameAr, topicNameEn); } diff --git a/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQuery.cs b/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQuery.cs index 1a9aaf0f..1e9eb182 100644 --- a/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQuery.cs +++ b/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; namespace CCE.Application.Content.Queries.GetNewsById; -public sealed record GetNewsByIdQuery(System.Guid Id) : IRequest; +public sealed record GetNewsByIdQuery(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQueryHandler.cs index 9350a2f2..a7dafa1a 100644 --- a/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQueryHandler.cs @@ -1,27 +1,42 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; +using CCE.Application.Messages; +using CCE.Domain.Community; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Queries.GetNewsById; -public sealed class GetNewsByIdQueryHandler : IRequestHandler +public sealed class GetNewsByIdQueryHandler : IRequestHandler> { private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public GetNewsByIdQueryHandler(ICceDbContext db) => _db = db; + public GetNewsByIdQueryHandler(ICceDbContext db, MessageFactory messages) + { + _db = db; + _messages = messages; + } - public async Task Handle(GetNewsByIdQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetNewsByIdQuery request, CancellationToken cancellationToken) { var list = await _db.News.Where(n => n.Id == request.Id).ToListAsyncEither(cancellationToken).ConfigureAwait(false); var news = list.SingleOrDefault(); - return news is null ? null : MapToDto(news); + if (news is null) + return _messages.NewsNotFound(); + + var topics = await _db.Topics.Where(t => t.Id == news.TopicId) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + var topic = topics.FirstOrDefault(); + + return _messages.Ok(MapToDto(news, topic?.NameAr ?? string.Empty, topic?.NameEn ?? string.Empty), "SUCCESS_OPERATION"); } - internal static NewsDto MapToDto(News n) => new( + internal static NewsDto MapToDto(News n, string topicNameAr = "", string topicNameEn = "") => new( n.Id, n.TitleAr, n.TitleEn, n.ContentAr, n.ContentEn, - n.Slug, n.AuthorId, n.FeaturedImageUrl, - n.PublishedOn, n.IsFeatured, n.IsPublished, - System.Convert.ToBase64String(n.RowVersion)); + n.TopicId, topicNameAr, topicNameEn, + n.AuthorId, n.FeaturedImageUrl, + n.PublishedOn, n.IsFeatured, n.IsPublished); } diff --git a/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQuery.cs b/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQuery.cs new file mode 100644 index 00000000..25b5db92 --- /dev/null +++ b/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQuery.cs @@ -0,0 +1,7 @@ +using CCE.Application.Common; +using CCE.Application.Content.Dtos; +using MediatR; + +namespace CCE.Application.Content.Queries.GetResourceById; + +public sealed record GetResourceByIdQuery(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQueryHandler.cs new file mode 100644 index 00000000..3c1c2488 --- /dev/null +++ b/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQueryHandler.cs @@ -0,0 +1,64 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; +using CCE.Application.Content.Dtos; +using CCE.Application.Messages; +using CCE.Domain.Content; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace CCE.Application.Content.Queries.GetResourceById; + +public sealed class GetResourceByIdQueryHandler : IRequestHandler> +{ + private readonly ICceDbContext _db; + private readonly MessageFactory _messages; + + public GetResourceByIdQueryHandler(ICceDbContext db, MessageFactory messages) + { + _db = db; + _messages = messages; + } + + public async Task> Handle(GetResourceByIdQuery request, CancellationToken cancellationToken) + { + var list = await _db.Resources + .AsNoTracking() + .Include(r => r.Countries) + .Where(r => r.Id == request.Id) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var resource = list.SingleOrDefault(); + return resource is null + ? _messages.ResourceNotFound() + : _messages.Ok(MapToDto(resource), "SUCCESS_OPERATION"); + } + + private ResourceDto MapToDto(Resource r) + { + var category = _db.ResourceCategories.FirstOrDefault(c => c.Id == r.CategoryId); + var asset = _db.AssetFiles.FirstOrDefault(a => a.Id == r.AssetFileId); + var countryIds = r.Countries.Select(c => c.CountryId).ToList(); + var countries = _db.Countries.Where(c => countryIds.Contains(c.Id)).ToList(); + + return new ResourceDto( + r.Id, + r.TitleAr, + r.TitleEn, + r.DescriptionAr, + r.DescriptionEn, + r.ResourceType, + r.CategoryId, + category?.NameAr ?? string.Empty, + category?.NameEn ?? string.Empty, + r.AssetFileId, + asset?.OriginalFileName ?? string.Empty, + countryIds, + countries.Select(c => c.NameAr).ToList(), + r.UploadedById, + r.PublishedOn, + r.ViewCount, + r.IsCenterManaged, + r.IsPublished); + } +} diff --git a/backend/src/CCE.Application/Content/Queries/ListEvents/ListEventsQuery.cs b/backend/src/CCE.Application/Content/Queries/ListEvents/ListEventsQuery.cs index de4d0e44..43db2191 100644 --- a/backend/src/CCE.Application/Content/Queries/ListEvents/ListEventsQuery.cs +++ b/backend/src/CCE.Application/Content/Queries/ListEvents/ListEventsQuery.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; using MediatR; @@ -9,4 +10,5 @@ public sealed record ListEventsQuery( int PageSize = 20, string? Search = null, System.DateTimeOffset? FromDate = null, - System.DateTimeOffset? ToDate = null) : IRequest>; + System.DateTimeOffset? ToDate = null, + System.Guid? TopicId = null) : IRequest>>; diff --git a/backend/src/CCE.Application/Content/Queries/ListEvents/ListEventsQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/ListEvents/ListEventsQueryHandler.cs index 2bb67e68..9e0817c9 100644 --- a/backend/src/CCE.Application/Content/Queries/ListEvents/ListEventsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/ListEvents/ListEventsQueryHandler.cs @@ -1,18 +1,26 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; +using CCE.Application.Messages; +using CCE.Domain.Community; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Queries.ListEvents; -public sealed class ListEventsQueryHandler : IRequestHandler> +public sealed class ListEventsQueryHandler : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public ListEventsQueryHandler(ICceDbContext db) => _db = db; + public ListEventsQueryHandler(ICceDbContext db, MessageFactory messages) + { + _db = db; + _messages = messages; + } - public async Task> Handle(ListEventsQuery request, CancellationToken cancellationToken) + public async Task>> Handle(ListEventsQuery request, CancellationToken cancellationToken) { var query = _db.Events .WhereIf(!string.IsNullOrWhiteSpace(request.Search), @@ -20,15 +28,30 @@ public async Task> Handle(ListEventsQuery request, Cancell e.TitleEn.Contains(request.Search!)) .WhereIf(request.FromDate.HasValue, e => e.StartsOn >= request.FromDate!.Value) .WhereIf(request.ToDate.HasValue, e => e.EndsOn <= request.ToDate!.Value) + .WhereIf(request.TopicId.HasValue, e => e.TopicId == request.TopicId!.Value) .OrderByDescending(e => e.StartsOn); var result = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); - return result.Map(MapToDto); + + var topicIds = result.Items.Select(e => e.TopicId).Distinct().ToList(); + var topics = await _db.Topics.Where(t => topicIds.Contains(t.Id)) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + var topicById = topics.ToDictionary(t => t.Id); + + return _messages.Ok(result.Map(e => MapToDto(e, topicById)), "ITEMS_LISTED"); } - internal static EventDto MapToDto(Event e) => new( + internal static EventDto MapToDto(Event e, Dictionary topicById) => new( + e.Id, e.TitleAr, e.TitleEn, e.DescriptionAr, e.DescriptionEn, + e.StartsOn, e.EndsOn, e.LocationAr, e.LocationEn, + e.OnlineMeetingUrl, e.FeaturedImageUrl, e.ICalUid, + e.TopicId, + topicById.TryGetValue(e.TopicId, out var t) ? t.NameAr : string.Empty, + topicById.TryGetValue(e.TopicId, out t) ? t.NameEn : string.Empty); + + internal static EventDto MapToDto(Event e, string topicNameAr = "", string topicNameEn = "") => new( e.Id, e.TitleAr, e.TitleEn, e.DescriptionAr, e.DescriptionEn, e.StartsOn, e.EndsOn, e.LocationAr, e.LocationEn, e.OnlineMeetingUrl, e.FeaturedImageUrl, e.ICalUid, - System.Convert.ToBase64String(e.RowVersion)); + e.TopicId, topicNameAr, topicNameEn); } diff --git a/backend/src/CCE.Application/Content/Queries/ListNews/ListNewsQuery.cs b/backend/src/CCE.Application/Content/Queries/ListNews/ListNewsQuery.cs index bd8974e1..818c1c59 100644 --- a/backend/src/CCE.Application/Content/Queries/ListNews/ListNewsQuery.cs +++ b/backend/src/CCE.Application/Content/Queries/ListNews/ListNewsQuery.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; using MediatR; @@ -9,4 +10,5 @@ public sealed record ListNewsQuery( int PageSize = 20, string? Search = null, bool? IsPublished = null, - bool? IsFeatured = null) : IRequest>; + bool? IsFeatured = null, + System.Guid? TopicId = null) : IRequest>>; diff --git a/backend/src/CCE.Application/Content/Queries/ListNews/ListNewsQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/ListNews/ListNewsQueryHandler.cs index c7c97445..6226d0b2 100644 --- a/backend/src/CCE.Application/Content/Queries/ListNews/ListNewsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/ListNews/ListNewsQueryHandler.cs @@ -1,18 +1,26 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; +using CCE.Application.Messages; +using CCE.Domain.Community; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Queries.ListNews; -public sealed class ListNewsQueryHandler : IRequestHandler> +public sealed class ListNewsQueryHandler : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public ListNewsQueryHandler(ICceDbContext db) => _db = db; + public ListNewsQueryHandler(ICceDbContext db, MessageFactory messages) + { + _db = db; + _messages = messages; + } - public async Task> Handle(ListNewsQuery request, CancellationToken cancellationToken) + public async Task>> Handle(ListNewsQuery request, CancellationToken cancellationToken) { var query = _db.News .WhereIf(!string.IsNullOrWhiteSpace(request.Search), @@ -22,16 +30,31 @@ public async Task> Handle(ListNewsQuery request, Cancellati .WhereIf(request.IsPublished == true, n => n.PublishedOn != null) .WhereIf(request.IsPublished == false, n => n.PublishedOn == null) .WhereIf(request.IsFeatured.HasValue, n => n.IsFeatured == request.IsFeatured!.Value) + .WhereIf(request.TopicId.HasValue, n => n.TopicId == request.TopicId!.Value) .OrderByDescending(n => n.PublishedOn ?? DateTimeOffset.MinValue) .ThenByDescending(n => n.Id); var result = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); - return result.Map(MapToDto); + + var topicIds = result.Items.Select(n => n.TopicId).Distinct().ToList(); + var topics = await _db.Topics.Where(t => topicIds.Contains(t.Id)) + .ToListAsyncEither(cancellationToken).ConfigureAwait(false); + var topicById = topics.ToDictionary(t => t.Id); + + return _messages.Ok(result.Map(n => MapToDto(n, topicById)), "ITEMS_LISTED"); } - internal static NewsDto MapToDto(News n) => new( + internal static NewsDto MapToDto(News n, Dictionary topicById) => new( + n.Id, n.TitleAr, n.TitleEn, n.ContentAr, n.ContentEn, + n.TopicId, + topicById.TryGetValue(n.TopicId, out var t) ? t.NameAr : string.Empty, + topicById.TryGetValue(n.TopicId, out t) ? t.NameEn : string.Empty, + n.AuthorId, n.FeaturedImageUrl, + n.PublishedOn, n.IsFeatured, n.IsPublished); + + internal static NewsDto MapToDto(News n, string topicNameAr = "", string topicNameEn = "") => new( n.Id, n.TitleAr, n.TitleEn, n.ContentAr, n.ContentEn, - n.Slug, n.AuthorId, n.FeaturedImageUrl, - n.PublishedOn, n.IsFeatured, n.IsPublished, - System.Convert.ToBase64String(n.RowVersion)); + n.TopicId, topicNameAr, topicNameEn, + n.AuthorId, n.FeaturedImageUrl, + n.PublishedOn, n.IsFeatured, n.IsPublished); } diff --git a/backend/src/CCE.Application/Content/Queries/ListResources/ListResourcesQuery.cs b/backend/src/CCE.Application/Content/Queries/ListResources/ListResourcesQuery.cs index 2012d4a2..d8a04026 100644 --- a/backend/src/CCE.Application/Content/Queries/ListResources/ListResourcesQuery.cs +++ b/backend/src/CCE.Application/Content/Queries/ListResources/ListResourcesQuery.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; using MediatR; @@ -10,4 +11,4 @@ public sealed record ListResourcesQuery( string? Search = null, System.Guid? CategoryId = null, System.Guid? CountryId = null, - bool? IsPublished = null) : IRequest>; + bool? IsPublished = null) : IRequest>>; diff --git a/backend/src/CCE.Application/Content/Queries/ListResources/ListResourcesQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/ListResources/ListResourcesQueryHandler.cs index 9d8ce30f..95953f2d 100644 --- a/backend/src/CCE.Application/Content/Queries/ListResources/ListResourcesQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/ListResources/ListResourcesQueryHandler.cs @@ -1,53 +1,99 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; +using CCE.Application.Messages; using CCE.Domain.Content; using MediatR; +using Microsoft.EntityFrameworkCore; namespace CCE.Application.Content.Queries.ListResources; -public sealed class ListResourcesQueryHandler - : IRequestHandler> +public sealed class ListResourcesQueryHandler : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _messages; - public ListResourcesQueryHandler(ICceDbContext db) => _db = db; + public ListResourcesQueryHandler(ICceDbContext db, MessageFactory messages) + { + _db = db; + _messages = messages; + } - public async Task> Handle( + public async Task>> Handle( ListResourcesQuery request, CancellationToken cancellationToken) { var query = _db.Resources + .AsNoTracking() + .Include(r => r.Countries) .WhereIf(!string.IsNullOrWhiteSpace(request.Search), r => r.TitleAr.Contains(request.Search!) || r.TitleEn.Contains(request.Search!) || r.DescriptionAr.Contains(request.Search!) || r.DescriptionEn.Contains(request.Search!)) .WhereIf(request.CategoryId.HasValue, r => r.CategoryId == request.CategoryId!.Value) - .WhereIf(request.CountryId.HasValue, r => r.CountryId == request.CountryId!.Value) + .WhereIf(request.CountryId.HasValue, r => r.Countries.Any(c => c.CountryId == request.CountryId!.Value)) .WhereIf(request.IsPublished == true, r => r.PublishedOn != null) .WhereIf(request.IsPublished == false, r => r.PublishedOn == null) - .OrderByDescending(r => r.PublishedOn ?? DateTimeOffset.MinValue) + .OrderByDescending(r => r.PublishedOn) .ThenByDescending(r => r.Id); - var result = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); - return result.Map(MapToDto); - } + var paged = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); + + // Batch enrich categories / assets / country names for the page (avoids N+1) + var categoryIds = paged.Items.Select(r => r.CategoryId).Distinct().ToList(); + var assetIds = paged.Items.Select(r => r.AssetFileId).Distinct().ToList(); + var allCountryIds = paged.Items.SelectMany(r => r.Countries.Select(c => c.CountryId)).Distinct().ToList(); + + var categories = await _db.ResourceCategories + .Where(c => categoryIds.Contains(c.Id)) + .Select(c => new { c.Id, c.NameAr, c.NameEn }) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var categoryMap = categories.ToDictionary(c => c.Id, c => new { c.NameAr, c.NameEn }); - internal static ResourceDto MapToDto(Resource r) => new( - r.Id, - r.TitleAr, - r.TitleEn, - r.DescriptionAr, - r.DescriptionEn, - r.ResourceType, - r.CategoryId, - r.CountryId, - r.UploadedById, - r.AssetFileId, - r.PublishedOn, - r.ViewCount, - r.IsCenterManaged, - r.IsPublished, - System.Convert.ToBase64String(r.RowVersion)); + var assets = await _db.AssetFiles + .Where(a => assetIds.Contains(a.Id)) + .Select(a => new { a.Id, a.OriginalFileName }) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var assetMap = assets.ToDictionary(a => a.Id, a => a.OriginalFileName); + + var countries = await _db.Countries + .Where(c => allCountryIds.Contains(c.Id)) + .Select(c => new { c.Id, c.NameAr }) + .ToListAsyncEither(cancellationToken) + .ConfigureAwait(false); + var countryNameMap = countries.ToDictionary(c => c.Id, c => c.NameAr); + + var dtos = paged.Items.Select(r => + { + var cat = categoryMap.GetValueOrDefault(r.CategoryId); + var countryIds = r.Countries.Select(c => c.CountryId).ToList(); + var countryNames = countryIds.Select(id => countryNameMap.GetValueOrDefault(id) ?? string.Empty).ToList(); + return new ResourceDto( + r.Id, + r.TitleAr, + r.TitleEn, + r.DescriptionAr, + r.DescriptionEn, + r.ResourceType, + r.CategoryId, + cat?.NameAr ?? string.Empty, + cat?.NameEn ?? string.Empty, + r.AssetFileId, + assetMap.GetValueOrDefault(r.AssetFileId) ?? string.Empty, + countryIds, + countryNames, + r.UploadedById, + r.PublishedOn, + r.ViewCount, + r.IsCenterManaged, + r.IsPublished); + }).ToList(); + + var result = new PagedResult(dtos, paged.Page, paged.PageSize, paged.Total); + return _messages.Ok(result, "ITEMS_LISTED"); + } } diff --git a/backend/src/CCE.Application/Evaluation/DTOs/ServiceEvaluationDto.cs b/backend/src/CCE.Application/Evaluation/DTOs/ServiceEvaluationDto.cs index ae99acad..962551ed 100644 --- a/backend/src/CCE.Application/Evaluation/DTOs/ServiceEvaluationDto.cs +++ b/backend/src/CCE.Application/Evaluation/DTOs/ServiceEvaluationDto.cs @@ -10,4 +10,4 @@ public sealed record ServiceEvaluationDto( string Feedback, System.Guid? UserId, System.DateTimeOffset CreatedOn, - System.Guid? CreatedById); + System.Guid CreatedById); diff --git a/backend/src/CCE.Application/Messages/MessageFactory.cs b/backend/src/CCE.Application/Messages/MessageFactory.cs index 7c135a2c..3f248781 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 ResourceNotFound() => NotFound("RESOURCE_NOT_FOUND"); public Response PageNotFound() => NotFound("PAGE_NOT_FOUND"); public Response CategoryNotFound() => NotFound("CATEGORY_NOT_FOUND"); public Response AssetNotFound() => NotFound("ASSET_NOT_FOUND"); diff --git a/backend/src/CCE.Application/Messages/SystemCode.cs b/backend/src/CCE.Application/Messages/SystemCode.cs index f2b84c57..aa254095 100644 --- a/backend/src/CCE.Application/Messages/SystemCode.cs +++ b/backend/src/CCE.Application/Messages/SystemCode.cs @@ -160,16 +160,17 @@ public static class SystemCode public const string CON053 = "CON053"; // State rep assignment revoked public const string CON054 = "CON054"; // Roles assigned public const string CON055 = "CON055"; // User status changed + public const string CON056 = "CON056"; // Login success // ─── Content Success ─── public const string CON020 = "CON020"; // Content created - public const string CON021 = "CON021"; // Content updated - public const string CON022 = "CON022"; // Content deleted + public const string CON021 = "CON021"; // Resource created (BRD appendix) + public const string CON022 = "CON022"; // Resource deleted (BRD appendix) public const string CON023 = "CON023"; // Content published public const string CON024 = "CON024"; // Content archived - public const string CON025 = "CON025"; // Resource created + public const string CON025 = "CON025"; // Content updated public const string CON026 = "CON026"; // Resource updated - public const string CON027 = "CON027"; // Resource deleted + public const string CON027 = "CON027"; // Content deleted public const string CON028 = "CON028"; // Resource published // ─── Media Success ─── diff --git a/backend/src/CCE.Application/Messages/SystemCodeMap.cs b/backend/src/CCE.Application/Messages/SystemCodeMap.cs index a1e1751d..112d3f54 100644 --- a/backend/src/CCE.Application/Messages/SystemCodeMap.cs +++ b/backend/src/CCE.Application/Messages/SystemCodeMap.cs @@ -42,6 +42,9 @@ public static class SystemCodeMap ["PAGE_DUPLICATE"] = SystemCode.ERR050, ["NEWS_DUPLICATE"] = SystemCode.ERR051, ["EVENT_DUPLICATE"] = SystemCode.ERR052, + ["RESOURCE_DOWNLOAD_FAILED"] = SystemCode.ERR002, + ["RESOURCE_UPLOAD_FAILED"] = SystemCode.ERR029, + ["RESOURCE_DELETE_FAILED"] = SystemCode.ERR030, // ─── Community Errors ─── ["TOPIC_NOT_FOUND"] = SystemCode.ERR060, @@ -110,7 +113,7 @@ public static class SystemCodeMap ["DUPLICATE_VALUE"] = SystemCode.ERR908, // ─── Identity Success (appendix-aligned) ─── - ["LOGIN_SUCCESS"] = SystemCode.CON001, + ["LOGIN_SUCCESS"] = SystemCode.CON056, ["TOKEN_REFRESHED"] = SystemCode.CON004, ["PROFILE_UPDATED"] = SystemCode.CON005, ["EXPERT_REQUEST_SUBMITTED"] = SystemCode.CON006, @@ -133,8 +136,8 @@ public static class SystemCodeMap // ─── Content Success ─── ["CONTENT_CREATED"] = SystemCode.CON020, - ["CONTENT_UPDATED"] = SystemCode.CON021, - ["CONTENT_DELETED"] = SystemCode.CON022, + ["CONTENT_UPDATED"] = SystemCode.CON025, + ["CONTENT_DELETED"] = SystemCode.CON027, // ─── Asset Success ─── ["ASSET_UPLOADED"] = SystemCode.CON038, @@ -145,10 +148,13 @@ public static class SystemCodeMap ["MEDIA_DELETED"] = SystemCode.CON037, ["CONTENT_PUBLISHED"] = SystemCode.CON023, ["CONTENT_ARCHIVED"] = SystemCode.CON024, - ["RESOURCE_CREATED"] = SystemCode.CON025, + ["RESOURCE_CREATED"] = SystemCode.CON021, ["RESOURCE_UPDATED"] = SystemCode.CON026, - ["RESOURCE_DELETED"] = SystemCode.CON027, + ["RESOURCE_DELETED"] = SystemCode.CON022, ["RESOURCE_PUBLISHED"] = SystemCode.CON028, + ["RESOURCE_DOWNLOAD_SUCCESS"] = SystemCode.CON001, + ["RESOURCE_SHARE_SUCCESS"] = SystemCode.CON002, + ["RESOURCE_SHARE_FAILED"] = SystemCode.ERR003, // ─── Notification Success ─── ["NOTIFICATION_CREATED"] = SystemCode.CON040, @@ -195,7 +201,7 @@ public static class SystemCodeMap /// Get the ERR/CON/VAL code for a domain key. Returns ERR900 if unmapped. public static string ToSystemCode(string domainKey) - => DomainToCode.TryGetValue(domainKey, out var code) ? code : SystemCode.ERR900; + => domainKey is not null && DomainToCode.TryGetValue(domainKey, out var code) ? code : SystemCode.ERR900; /// Get the domain key from a system code. Returns null if unmapped. public static string? ToDomainKey(string systemCode) diff --git a/backend/src/CCE.Domain/Common/AuditableEntity.cs b/backend/src/CCE.Domain/Common/AuditableEntity.cs index cc355a1e..a1ab1f0c 100644 --- a/backend/src/CCE.Domain/Common/AuditableEntity.cs +++ b/backend/src/CCE.Domain/Common/AuditableEntity.cs @@ -15,7 +15,7 @@ protected AuditableEntity(TId id) : base(id) { } public DateTimeOffset CreatedOn { get; protected set; } /// - public Guid? CreatedById { get; protected set; } + public Guid CreatedById { get; protected set; } /// public DateTimeOffset? LastModifiedOn { get; protected set; } diff --git a/backend/src/CCE.Domain/Common/SystemConstants.cs b/backend/src/CCE.Domain/Common/SystemConstants.cs new file mode 100644 index 00000000..47ae9bf0 --- /dev/null +++ b/backend/src/CCE.Domain/Common/SystemConstants.cs @@ -0,0 +1,14 @@ +namespace CCE.Domain.Common; + +/// +/// Well-known sentinel values used across the domain. +/// +public static class SystemConstants +{ + /// + /// Represents an anonymous or system actor when no real user is available. + /// Used for audit fields (CreatedById, LastModifiedById) on entities + /// created by unauthenticated users. + /// + public static readonly Guid AnonymousUserId = new("00000000-0000-0000-0000-000000000001"); +} diff --git a/backend/src/CCE.Domain/Content/Event.cs b/backend/src/CCE.Domain/Content/Event.cs index 26fe909d..9ba06609 100644 --- a/backend/src/CCE.Domain/Content/Event.cs +++ b/backend/src/CCE.Domain/Content/Event.cs @@ -23,7 +23,8 @@ private Event( string? locationEn, string? onlineMeetingUrl, string? featuredImageUrl, - string iCalUid) : base(id) + string iCalUid, + System.Guid topicId) : base(id) { TitleAr = titleAr; TitleEn = titleEn; @@ -36,6 +37,7 @@ private Event( OnlineMeetingUrl = onlineMeetingUrl; FeaturedImageUrl = featuredImageUrl; ICalUid = iCalUid; + TopicId = topicId; } public string TitleAr { get; private set; } @@ -52,6 +54,8 @@ private Event( /// Stable iCalendar UID (set at creation). Never changes. public string ICalUid { get; private set; } + public System.Guid TopicId { get; private set; } + public byte[] RowVersion { get; private set; } = System.Array.Empty(); public static Event Schedule( @@ -65,6 +69,7 @@ public static Event Schedule( string? locationEn, string? onlineMeetingUrl, string? featuredImageUrl, + System.Guid topicId, ISystemClock clock) { if (string.IsNullOrWhiteSpace(titleAr)) throw new DomainException("TitleAr is required."); @@ -75,6 +80,7 @@ public static Event Schedule( { throw new DomainException("EndsOn must be strictly after StartsOn."); } + if (topicId == System.Guid.Empty) throw new DomainException("TopicId is required."); if (onlineMeetingUrl is not null && !onlineMeetingUrl.StartsWith("https://", System.StringComparison.OrdinalIgnoreCase)) { @@ -88,7 +94,7 @@ public static Event Schedule( var id = System.Guid.NewGuid(); var iCalUid = $"{id:N}@cce.moenergy.gov.sa"; var ev = new Event(id, titleAr, titleEn, descriptionAr, descriptionEn, - startsOn, endsOn, locationAr, locationEn, onlineMeetingUrl, featuredImageUrl, iCalUid); + startsOn, endsOn, locationAr, locationEn, onlineMeetingUrl, featuredImageUrl, iCalUid, topicId); ev.RaiseDomainEvent(new EventScheduledEvent(id, startsOn, endsOn, clock.UtcNow)); return ev; } @@ -101,12 +107,14 @@ public void UpdateContent( string? locationAr, string? locationEn, string? onlineMeetingUrl, - string? featuredImageUrl) + string? featuredImageUrl, + System.Guid topicId) { if (string.IsNullOrWhiteSpace(titleAr)) throw new DomainException("TitleAr is required."); if (string.IsNullOrWhiteSpace(titleEn)) throw new DomainException("TitleEn is required."); if (string.IsNullOrWhiteSpace(descriptionAr)) throw new DomainException("DescriptionAr is required."); if (string.IsNullOrWhiteSpace(descriptionEn)) throw new DomainException("DescriptionEn is required."); + if (topicId == System.Guid.Empty) throw new DomainException("TopicId is required."); if (onlineMeetingUrl is not null && !onlineMeetingUrl.StartsWith("https://", System.StringComparison.OrdinalIgnoreCase)) { @@ -125,6 +133,7 @@ public void UpdateContent( LocationEn = locationEn; OnlineMeetingUrl = onlineMeetingUrl; FeaturedImageUrl = featuredImageUrl; + TopicId = topicId; } public void Reschedule(System.DateTimeOffset startsOn, System.DateTimeOffset endsOn) diff --git a/backend/src/CCE.Domain/Content/News.cs b/backend/src/CCE.Domain/Content/News.cs index a9154af5..a12b753e 100644 --- a/backend/src/CCE.Domain/Content/News.cs +++ b/backend/src/CCE.Domain/Content/News.cs @@ -1,4 +1,3 @@ -using System.Text.RegularExpressions; using CCE.Domain.Common; using CCE.Domain.Content.Events; @@ -6,13 +5,11 @@ namespace CCE.Domain.Content; /// /// News article — bilingual title + rich-text content + optional featured image. -/// Slug is unique (enforced in Phase 08 DB unique index). Soft-deletable, audited. +/// Slug is auto-generated from the English title. Soft-deletable, audited. /// [Audited] public sealed class News : AggregateRoot { - private static readonly Regex SlugPattern = new("^[a-z0-9]+(-[a-z0-9]+)*$", RegexOptions.Compiled); - private News( System.Guid id, string titleAr, @@ -20,6 +17,7 @@ private News( string contentAr, string contentEn, string slug, + System.Guid topicId, System.Guid authorId, string? featuredImageUrl) : base(id) { @@ -28,6 +26,7 @@ private News( ContentAr = contentAr; ContentEn = contentEn; Slug = slug; + TopicId = topicId; AuthorId = authorId; FeaturedImageUrl = featuredImageUrl; } @@ -37,6 +36,7 @@ private News( public string ContentAr { get; private set; } public string ContentEn { get; private set; } public string Slug { get; private set; } + public System.Guid TopicId { get; private set; } public System.Guid AuthorId { get; private set; } public string? FeaturedImageUrl { get; private set; } public System.DateTimeOffset? PublishedOn { get; private set; } @@ -50,7 +50,7 @@ public static News Draft( string titleEn, string contentAr, string contentEn, - string slug, + System.Guid topicId, System.Guid authorId, string? featuredImageUrl, ISystemClock clock) @@ -60,10 +60,7 @@ public static News Draft( if (string.IsNullOrWhiteSpace(titleEn)) throw new DomainException("TitleEn is required."); if (string.IsNullOrWhiteSpace(contentAr)) throw new DomainException("ContentAr is required."); if (string.IsNullOrWhiteSpace(contentEn)) throw new DomainException("ContentEn is required."); - if (string.IsNullOrWhiteSpace(slug) || !SlugPattern.IsMatch(slug)) - { - throw new DomainException($"slug '{slug}' must be kebab-case."); - } + if (topicId == System.Guid.Empty) throw new DomainException("TopicId is required."); if (authorId == System.Guid.Empty) throw new DomainException("AuthorId is required."); if (featuredImageUrl is not null && !featuredImageUrl.StartsWith("https://", System.StringComparison.OrdinalIgnoreCase)) @@ -76,7 +73,8 @@ public static News Draft( titleEn: titleEn, contentAr: contentAr, contentEn: contentEn, - slug: slug, + slug: ToKebabSlug(titleEn), + topicId: topicId, authorId: authorId, featuredImageUrl: featuredImageUrl); } @@ -86,17 +84,14 @@ public void UpdateContent( string titleEn, string contentAr, string contentEn, - string slug, + System.Guid topicId, string? featuredImageUrl) { if (string.IsNullOrWhiteSpace(titleAr)) throw new DomainException("TitleAr is required."); if (string.IsNullOrWhiteSpace(titleEn)) throw new DomainException("TitleEn is required."); if (string.IsNullOrWhiteSpace(contentAr)) throw new DomainException("ContentAr is required."); if (string.IsNullOrWhiteSpace(contentEn)) throw new DomainException("ContentEn is required."); - if (string.IsNullOrWhiteSpace(slug) || !SlugPattern.IsMatch(slug)) - { - throw new DomainException($"slug '{slug}' must be kebab-case."); - } + if (topicId == System.Guid.Empty) throw new DomainException("TopicId is required."); if (featuredImageUrl is not null && !featuredImageUrl.StartsWith("https://", System.StringComparison.OrdinalIgnoreCase)) { @@ -106,7 +101,7 @@ public void UpdateContent( TitleEn = titleEn; ContentAr = contentAr; ContentEn = contentEn; - Slug = slug; + TopicId = topicId; FeaturedImageUrl = featuredImageUrl; } @@ -120,4 +115,28 @@ public void Publish(ISystemClock clock) public void MarkFeatured() => IsFeatured = true; public void UnmarkFeatured() => IsFeatured = false; + + private static string ToKebabSlug(string text) + { + if (string.IsNullOrWhiteSpace(text)) return "news"; + var sb = new System.Text.StringBuilder(); + bool lastWasHyphen = true; + foreach (var c in text.ToLowerInvariant()) + { + if (char.IsLetterOrDigit(c)) + { + sb.Append(c); + lastWasHyphen = false; + } + else if (!lastWasHyphen) + { + sb.Append('-'); + lastWasHyphen = true; + } + } + if (lastWasHyphen && sb.Length > 0) + sb.Length--; + var result = sb.ToString(); + return string.IsNullOrWhiteSpace(result) ? "news" : result; + } } diff --git a/backend/src/CCE.Domain/Content/Resource.cs b/backend/src/CCE.Domain/Content/Resource.cs index f07bf9bf..37d1a96c 100644 --- a/backend/src/CCE.Domain/Content/Resource.cs +++ b/backend/src/CCE.Domain/Content/Resource.cs @@ -48,6 +48,9 @@ private Resource( public System.DateTimeOffset? PublishedOn { get; private set; } public long ViewCount { get; private set; } + private readonly List _countries = new(); + public IReadOnlyCollection Countries => _countries.AsReadOnly(); + /// EF-managed concurrency token (rowversion). public byte[] RowVersion { get; private set; } = System.Array.Empty(); @@ -67,6 +70,7 @@ public static Resource Draft( System.Guid? countryId, System.Guid uploadedById, System.Guid assetFileId, + IEnumerable countryIds, ISystemClock clock) { _ = clock; @@ -77,7 +81,8 @@ public static Resource Draft( if (categoryId == System.Guid.Empty) throw new DomainException("CategoryId is required."); if (uploadedById == System.Guid.Empty) throw new DomainException("UploadedById is required."); if (assetFileId == System.Guid.Empty) throw new DomainException("AssetFileId is required."); - return new Resource( + + var resource = new Resource( id: System.Guid.NewGuid(), titleAr: titleAr, titleEn: titleEn, @@ -88,6 +93,13 @@ public static Resource Draft( countryId: countryId, uploadedById: uploadedById, assetFileId: assetFileId); + + foreach (var cid in countryIds.Distinct().Where(id => id != System.Guid.Empty)) + { + resource._countries.Add(ResourceCountry.Create(resource.Id, cid)); + } + + return resource; } public void Publish(ISystemClock clock) @@ -105,7 +117,8 @@ public void Publish(ISystemClock clock) } /// - /// Mutates the editable content fields. Audited via the existing AuditingInterceptor. + /// Mutates the editable content fields and covered countries. + /// Audited via the existing AuditingInterceptor. /// public void UpdateContent( string titleAr, @@ -113,7 +126,8 @@ public void UpdateContent( string descriptionAr, string descriptionEn, ResourceType resourceType, - System.Guid categoryId) + System.Guid categoryId, + IEnumerable countryIds) { if (string.IsNullOrWhiteSpace(titleAr)) throw new DomainException("TitleAr is required."); if (string.IsNullOrWhiteSpace(titleEn)) throw new DomainException("TitleEn is required."); @@ -126,6 +140,20 @@ public void UpdateContent( DescriptionEn = descriptionEn; ResourceType = resourceType; CategoryId = categoryId; + SyncCountries(countryIds); + } + + private void SyncCountries(IEnumerable countryIds) + { + var distinctIds = countryIds.Distinct().Where(id => id != System.Guid.Empty).ToList(); + + _countries.RemoveAll(rc => !distinctIds.Contains(rc.CountryId)); + + var existingIds = _countries.Select(rc => rc.CountryId).ToHashSet(); + foreach (var cid in distinctIds.Where(id => !existingIds.Contains(id))) + { + _countries.Add(ResourceCountry.Create(Id, cid)); + } } public void IncrementViewCount() => ViewCount++; diff --git a/backend/src/CCE.Domain/Content/ResourceCountry.cs b/backend/src/CCE.Domain/Content/ResourceCountry.cs new file mode 100644 index 00000000..bfa4dda4 --- /dev/null +++ b/backend/src/CCE.Domain/Content/ResourceCountry.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace CCE.Domain.Content; + +/// +/// Join entity linking a to one of its covered countries. +/// +public sealed class ResourceCountry +{ + private ResourceCountry(System.Guid resourceId, System.Guid countryId) + { + ResourceId = resourceId; + CountryId = countryId; + } + + public System.Guid ResourceId { get; private set; } + public System.Guid CountryId { get; private set; } + + public static ResourceCountry Create(System.Guid resourceId, System.Guid countryId) + => new(resourceId, countryId); +} diff --git a/backend/src/CCE.Domain/Content/ResourceType.cs b/backend/src/CCE.Domain/Content/ResourceType.cs index 776ed876..14af235c 100644 --- a/backend/src/CCE.Domain/Content/ResourceType.cs +++ b/backend/src/CCE.Domain/Content/ResourceType.cs @@ -1,14 +1,19 @@ namespace CCE.Domain.Content; /// -/// Format of a Resource. Drives both UI rendering (icon + viewer) and -/// validation rules (e.g., Video resources may require an associated transcript file). +/// Publication type of a Resource. Drives UI rendering (icon + viewer) +/// and categorization in the resource center. /// public enum ResourceType { - Pdf = 0, - Video = 1, - Image = 2, - Link = 3, - Document = 4, + Paper = 0, + Article = 1, + Study = 2, + Presentation = 3, + ScientificPaper = 4, + Report = 5, + Book = 6, + Research = 7, + CceGuide = 8, + Media = 9, } diff --git a/backend/src/CCE.Domain/Evaluation/ServiceEvaluation.cs b/backend/src/CCE.Domain/Evaluation/ServiceEvaluation.cs index bb981eac..4e66dcaa 100644 --- a/backend/src/CCE.Domain/Evaluation/ServiceEvaluation.cs +++ b/backend/src/CCE.Domain/Evaluation/ServiceEvaluation.cs @@ -2,7 +2,6 @@ namespace CCE.Domain.Evaluation; -[Audited] public sealed class ServiceEvaluation : AuditableEntity { @@ -53,7 +52,7 @@ public static ServiceEvaluation Submit( userId); entity.CreatedOn = clock.UtcNow; - entity.CreatedById = userId; + entity.CreatedById = userId ?? SystemConstants.AnonymousUserId; return entity; } diff --git a/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/EventConfiguration.cs b/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/EventConfiguration.cs index 69e31956..fcefd2f3 100644 --- a/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/EventConfiguration.cs +++ b/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/EventConfiguration.cs @@ -22,6 +22,7 @@ public void Configure(EntityTypeBuilder builder) builder.Property(e => e.RowVersion).IsRowVersion(); builder.HasIndex(e => e.ICalUid).IsUnique().HasDatabaseName("ux_event_ical_uid"); builder.HasIndex(e => e.StartsOn).HasDatabaseName("ix_event_starts_on"); + builder.HasIndex(e => e.TopicId).HasDatabaseName("ix_event_topic_id"); builder.Ignore(e => e.DomainEvents); } } diff --git a/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/NewsConfiguration.cs b/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/NewsConfiguration.cs index 693f13d8..785e1080 100644 --- a/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/NewsConfiguration.cs +++ b/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/NewsConfiguration.cs @@ -22,6 +22,7 @@ public void Configure(EntityTypeBuilder builder) .HasFilter("[is_deleted] = 0") .HasDatabaseName("ux_news_slug_active"); builder.HasIndex(n => n.PublishedOn).HasDatabaseName("ix_news_published_on"); + builder.HasIndex(n => n.TopicId).HasDatabaseName("ix_news_topic_id"); builder.Ignore(n => n.DomainEvents); } } diff --git a/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/ResourceConfiguration.cs b/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/ResourceConfiguration.cs index e2954cbf..4d706409 100644 --- a/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/ResourceConfiguration.cs +++ b/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/ResourceConfiguration.cs @@ -10,15 +10,19 @@ public void Configure(EntityTypeBuilder builder) { builder.HasKey(r => r.Id); builder.Property(r => r.Id).ValueGeneratedNever(); - builder.Property(r => r.TitleAr).HasMaxLength(512).IsRequired(); - builder.Property(r => r.TitleEn).HasMaxLength(512).IsRequired(); + builder.Property(r => r.TitleAr).HasMaxLength(255).IsRequired(); + builder.Property(r => r.TitleEn).HasMaxLength(255).IsRequired(); builder.Property(r => r.DescriptionAr).HasColumnType("nvarchar(max)"); builder.Property(r => r.DescriptionEn).HasColumnType("nvarchar(max)"); builder.Property(r => r.ResourceType).HasConversion(); builder.Property(r => r.RowVersion).IsRowVersion(); builder.HasIndex(r => new { r.CategoryId, r.PublishedOn }).HasDatabaseName("ix_resource_category_published"); - builder.HasIndex(r => r.CountryId).HasDatabaseName("ix_resource_country_id"); builder.HasIndex(r => r.AssetFileId).HasDatabaseName("ix_resource_asset_file_id"); builder.Ignore(r => r.DomainEvents); + + builder.HasMany(r => r.Countries) + .WithOne() + .HasForeignKey(rc => rc.ResourceId) + .OnDelete(DeleteBehavior.Cascade); } } diff --git a/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/ResourceCountryConfiguration.cs b/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/ResourceCountryConfiguration.cs new file mode 100644 index 00000000..63d8fd44 --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Configurations/Content/ResourceCountryConfiguration.cs @@ -0,0 +1,16 @@ +using CCE.Domain.Content; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CCE.Infrastructure.Persistence.Configurations.Content; + +internal sealed class ResourceCountryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(rc => new { rc.ResourceId, rc.CountryId }); + builder.Property(rc => rc.ResourceId).ValueGeneratedNever(); + builder.Property(rc => rc.CountryId).ValueGeneratedNever(); + builder.HasIndex(rc => rc.CountryId).HasDatabaseName("ix_resource_country_country_id"); + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/20260531170325_ExpandResourceTypeAndAddCountries.Designer.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260531170325_ExpandResourceTypeAndAddCountries.Designer.cs new file mode 100644 index 00000000..211644c5 --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260531170325_ExpandResourceTypeAndAddCountries.Designer.cs @@ -0,0 +1,3941 @@ +// +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("20260531170325_ExpandResourceTypeAndAddCountries")] + partial class ExpandResourceTypeAndAddCountries + { + /// + 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("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.Content.ResourceCountry", b => + { + b.Property("ResourceId") + .HasColumnType("uniqueidentifier") + .HasColumnName("resource_id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.HasKey("ResourceId", "CountryId") + .HasName("pk_resource_country"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_resource_country_country_id"); + + b.ToTable("resource_country", (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.Evaluation.ServiceEvaluation", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ContentSuitability") + .HasColumnType("int") + .HasColumnName("content_suitability"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("EaseOfUse") + .HasColumnType("int") + .HasColumnName("ease_of_use"); + + b.Property("Feedback") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("feedback"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OverallSatisfaction") + .HasColumnType("int") + .HasColumnName("overall_satisfaction"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_service_evaluations"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_service_evaluation_created_on"); + + b.ToTable("service_evaluations", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertProfile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AcademicTitleAr") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("academic_title_ar"); + + b.Property("AcademicTitleEn") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("academic_title_en"); + + b.Property("ApprovedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("approved_by_id"); + + b.Property("ApprovedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("approved_on"); + + b.Property("BioAr") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("bio_ar"); + + b.Property("BioEn") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("bio_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.PrimitiveCollection("ExpertiseTags") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("expertise_tags"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_expert_profiles"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("ux_expert_profile_active_user") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("expert_profiles", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRegistrationRequest", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("ProcessedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("processed_by_id"); + + b.Property("ProcessedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("processed_on"); + + b.Property("RejectionReasonAr") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("rejection_reason_ar"); + + b.Property("RejectionReasonEn") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("rejection_reason_en"); + + b.Property("RequestedBioAr") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("requested_bio_ar"); + + b.Property("RequestedBioEn") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("requested_bio_en"); + + b.Property("RequestedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("requested_by_id"); + + b.PrimitiveCollection("RequestedTags") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("requested_tags"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.HasKey("Id") + .HasName("pk_expert_registration_requests"); + + b.HasIndex("RequestedById") + .HasDatabaseName("ix_expert_request_requested_by"); + + b.HasIndex("Status") + .HasDatabaseName("ix_expert_request_status"); + + b.ToTable("expert_registration_requests", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRequestAttachment", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssetFileId") + .HasColumnType("uniqueidentifier") + .HasColumnName("asset_file_id"); + + b.Property("AttachmentType") + .HasColumnType("int") + .HasColumnName("attachment_type"); + + b.Property("ExpertRequestId") + .HasColumnType("uniqueidentifier") + .HasColumnName("expert_request_id"); + + b.Property("UploadedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("uploaded_at"); + + b.HasKey("Id") + .HasName("pk_expert_request_attachments"); + + b.HasIndex("ExpertRequestId") + .HasDatabaseName("ix_expert_request_attachments_expert_request_id"); + + b.ToTable("expert_request_attachments", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset") + .HasColumnName("created_at_utc"); + + b.Property("CreatedByIp") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("created_by_ip"); + + b.Property("ExpiresAtUtc") + .HasColumnType("datetimeoffset") + .HasColumnName("expires_at_utc"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("replaced_by_token_hash"); + + b.Property("RevokedAtUtc") + .HasColumnType("datetimeoffset") + .HasColumnName("revoked_at_utc"); + + b.Property("RevokedByIp") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("revoked_by_ip"); + + b.Property("TokenFamilyId") + .HasColumnType("uniqueidentifier") + .HasColumnName("token_family_id"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("token_hash"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("user_agent"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("TokenFamilyId") + .HasDatabaseName("ix_refresh_tokens_token_family_id"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ux_refresh_tokens_token_hash"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_refresh_tokens_user_id"); + + b.ToTable("refresh_tokens", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("normalized_name"); + + b.HasKey("Id") + .HasName("pk_asp_net_roles"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[normalized_name] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.StateRepresentativeAssignment", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssignedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("assigned_by_id"); + + b.Property("AssignedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("assigned_on"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RevokedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("revoked_by_id"); + + b.Property("RevokedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("revoked_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_state_representative_assignments"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_state_rep_country_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_state_rep_user_id"); + + b.HasIndex("UserId", "CountryId") + .IsUnique() + .HasDatabaseName("ux_state_rep_active_user_country") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("state_representative_assignments", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AccessFailedCount") + .HasColumnType("int") + .HasColumnName("access_failed_count"); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("avatar_url"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)") + .HasColumnName("concurrency_stamp"); + + b.Property("CountryCodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_code_id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("bit") + .HasColumnName("email_confirmed"); + + b.Property("EntraIdObjectId") + .HasColumnType("uniqueidentifier") + .HasColumnName("entra_id_object_id"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("first_name"); + + b.PrimitiveCollection("Interests") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("interests"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("job_title"); + + b.Property("KnowledgeLevel") + .HasColumnType("int") + .HasColumnName("knowledge_level"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("last_name"); + + b.Property("LocalePreference") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale_preference"); + + b.Property("LockoutEnabled") + .HasColumnType("bit") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("normalized_user_name"); + + b.Property("OrganizationName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("organization_name"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)") + .HasColumnName("password_hash"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit") + .HasColumnName("phone_number_confirmed"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)") + .HasColumnName("security_stamp"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit") + .HasColumnName("two_factor_enabled"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("pk_asp_net_users"); + + b.HasIndex("CountryCodeId") + .HasDatabaseName("ix_users_country_code_id"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_users_country_id"); + + b.HasIndex("EntraIdObjectId") + .IsUnique() + .HasDatabaseName("ix_asp_net_users_entra_id_object_id") + .HasFilter("[entra_id_object_id] IS NOT NULL"); + + b.HasIndex("NormalizedEmail") + .IsUnique() + .HasDatabaseName("ix_users_normalized_email_unique") + .HasFilter("[normalized_email] IS NOT NULL"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[normalized_user_name] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityScenario", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CityType") + .HasColumnType("int") + .HasColumnName("city_type"); + + b.Property("ConfigurationJson") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("configuration_json"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("TargetYear") + .HasColumnType("int") + .HasColumnName("target_year"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_city_scenarios"); + + b.HasIndex("UserId", "LastModifiedOn") + .HasDatabaseName("ix_city_scenario_user_modified"); + + b.ToTable("city_scenarios", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityScenarioResult", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ComputedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("computed_at"); + + b.Property("ComputedCarbonNeutralityYear") + .HasColumnType("int") + .HasColumnName("computed_carbon_neutrality_year"); + + b.Property("ComputedTotalCostUsd") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("computed_total_cost_usd"); + + b.Property("EngineVersion") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("engine_version"); + + b.Property("ScenarioId") + .HasColumnType("uniqueidentifier") + .HasColumnName("scenario_id"); + + b.HasKey("Id") + .HasName("pk_city_scenario_results"); + + b.HasIndex("ScenarioId", "ComputedAt") + .HasDatabaseName("ix_city_result_scenario_at"); + + b.ToTable("city_scenario_results", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityTechnology", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CarbonImpactKgPerYear") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("carbon_impact_kg_per_year"); + + b.Property("CategoryAr") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("category_ar"); + + b.Property("CategoryEn") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("category_en"); + + b.Property("CostUsd") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("cost_usd"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IconUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("icon_url"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.HasKey("Id") + .HasName("pk_city_technologies"); + + b.HasIndex("IsActive") + .HasDatabaseName("ix_city_tech_is_active"); + + b.ToTable("city_technologies", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMap", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_knowledge_maps"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ux_knowledge_map_slug_active") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("knowledge_maps", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMapAssociation", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssociatedId") + .HasColumnType("uniqueidentifier") + .HasColumnName("associated_id"); + + b.Property("AssociatedType") + .HasColumnType("int") + .HasColumnName("associated_type"); + + b.Property("NodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("node_id"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_knowledge_map_associations"); + + b.HasIndex("NodeId", "AssociatedType", "AssociatedId") + .IsUnique() + .HasDatabaseName("ux_km_assoc_node_type_id"); + + b.ToTable("knowledge_map_associations", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMapEdge", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("FromNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("from_node_id"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("map_id"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("RelationshipType") + .HasColumnType("int") + .HasColumnName("relationship_type"); + + b.Property("ToNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("to_node_id"); + + b.HasKey("Id") + .HasName("pk_knowledge_map_edges"); + + b.HasIndex("FromNodeId") + .HasDatabaseName("ix_km_edge_from_node"); + + b.HasIndex("ToNodeId") + .HasDatabaseName("ix_km_edge_to_node"); + + b.HasIndex("MapId", "FromNodeId", "ToNodeId", "RelationshipType") + .IsUnique() + .HasDatabaseName("ux_km_edge_map_from_to_relation"); + + b.ToTable("knowledge_map_edges", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMapNode", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("DescriptionAr") + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IconUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("icon_url"); + + b.Property("LayoutX") + .HasColumnType("float") + .HasColumnName("layout_x"); + + b.Property("LayoutY") + .HasColumnType("float") + .HasColumnName("layout_y"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("map_id"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("NodeType") + .HasColumnType("int") + .HasColumnName("node_type"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_knowledge_map_nodes"); + + b.HasIndex("MapId", "OrderIndex") + .HasDatabaseName("ix_km_node_map_order"); + + b.ToTable("knowledge_map_nodes", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Lookups.CountryCode", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DialCode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("nvarchar(16)") + .HasColumnName("dial_code"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.HasKey("Id") + .HasName("pk_country_codes"); + + b.HasIndex("DialCode") + .HasDatabaseName("ix_country_code_dial_code"); + + b.ToTable("country_codes", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Media.MediaFile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AltTextAr") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("alt_text_ar"); + + b.Property("AltTextEn") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("alt_text_en"); + + b.Property("DescriptionAr") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_en"); + + b.Property("MimeType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("mime_type"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("original_file_name"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size_bytes"); + + b.Property("StorageKey") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("storage_key"); + + b.Property("TitleAr") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("title_en"); + + b.Property("UploadedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("uploaded_by_id"); + + b.Property("UploadedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("uploaded_on"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("pk_media_files"); + + b.ToTable("media_files", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.NotificationLog", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AttemptCount") + .HasColumnType("int") + .HasColumnName("attempt_count"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("correlation_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FailedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("failed_on"); + + b.Property("PayloadJson") + .HasColumnType("nvarchar(max)") + .HasColumnName("payload_json"); + + b.Property("ProviderMessageId") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("provider_message_id"); + + b.Property("RecipientUserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("recipient_user_id"); + + b.Property("SentOn") + .HasColumnType("datetimeoffset") + .HasColumnName("sent_on"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("TemplateCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("template_code"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier") + .HasColumnName("template_id"); + + b.HasKey("Id") + .HasName("pk_notification_logs"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_notification_log_correlation_id"); + + b.HasIndex("TemplateCode", "Channel") + .HasDatabaseName("ix_notification_log_template_channel"); + + b.HasIndex("RecipientUserId", "Status", "CreatedOn") + .HasDatabaseName("ix_notification_log_recipient_status_created"); + + b.ToTable("notification_logs", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.NotificationTemplate", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("BodyAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("body_ar"); + + b.Property("BodyEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("body_en"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("code"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("SubjectAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("subject_ar"); + + b.Property("SubjectEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("subject_en"); + + b.Property("VariableSchemaJson") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("variable_schema_json"); + + b.HasKey("Id") + .HasName("pk_notification_templates"); + + b.HasIndex("Code", "Channel") + .IsUnique() + .HasDatabaseName("ux_notification_template_code_channel"); + + b.ToTable("notification_templates", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.UserNotification", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("ReadOn") + .HasColumnType("datetimeoffset") + .HasColumnName("read_on"); + + b.Property("RenderedBody") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("rendered_body"); + + b.Property("RenderedLocale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("rendered_locale"); + + b.Property("RenderedSubjectAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("rendered_subject_ar"); + + b.Property("RenderedSubjectEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("rendered_subject_en"); + + b.Property("SentOn") + .HasColumnType("datetimeoffset") + .HasColumnName("sent_on"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier") + .HasColumnName("template_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_notifications"); + + b.HasIndex("UserId", "Status") + .HasDatabaseName("ix_user_notification_user_status"); + + b.ToTable("user_notifications", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.UserNotificationSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("EventCode") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("event_code"); + + b.Property("IsEnabled") + .HasColumnType("bit") + .HasColumnName("is_enabled"); + + b.Property("UpdatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("updated_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_notification_settings"); + + b.HasIndex("UserId", "Channel", "EventCode") + .IsUnique() + .HasDatabaseName("ux_user_notification_settings_user_channel_event") + .HasFilter("[event_code] IS NOT NULL"); + + b.ToTable("user_notification_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.AboutSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("HowToUseVideoUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("how_to_use_video_url"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.HasKey("Id") + .HasName("pk_about_settings"); + + b.ToTable("about_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.GlossaryEntry", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AboutSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("about_settings_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_glossary_entries"); + + b.HasIndex("AboutSettingsId") + .HasDatabaseName("ix_glossary_entries_about_settings_id"); + + b.ToTable("glossary_entries", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageCountry", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("HomepageSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("homepage_settings_id"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_homepage_countries"); + + b.HasIndex("HomepageSettingsId", "CountryId") + .IsUnique() + .HasDatabaseName("ix_homepage_country_settings_country"); + + b.ToTable("homepage_countries", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CceConceptsAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("cce_concepts_ar"); + + b.Property("CceConceptsEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("cce_concepts_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("VideoUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("video_url"); + + b.HasKey("Id") + .HasName("pk_homepage_settings"); + + b.ToTable("homepage_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.KnowledgePartner", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AboutSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("about_settings_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LogoUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("logo_url"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("WebsiteUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("website_url"); + + b.HasKey("Id") + .HasName("pk_knowledge_partners"); + + b.HasIndex("AboutSettingsId") + .HasDatabaseName("ix_knowledge_partners_about_settings_id"); + + b.ToTable("knowledge_partners", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PoliciesSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.HasKey("Id") + .HasName("pk_policies_settings"); + + b.ToTable("policies_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PolicySection", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("PoliciesSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("policies_settings_id"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_policy_sections"); + + b.HasIndex("PoliciesSettingsId") + .HasDatabaseName("ix_policy_sections_policies_settings_id"); + + b.ToTable("policy_sections", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Surveys.SearchQueryLog", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale"); + + b.Property("QueryText") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("query_text"); + + b.Property("ResponseTimeMs") + .HasColumnType("int") + .HasColumnName("response_time_ms"); + + b.Property("ResultsCount") + .HasColumnType("int") + .HasColumnName("results_count"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_search_query_logs"); + + b.HasIndex("SubmittedOn") + .HasDatabaseName("ix_search_query_log_submitted_on"); + + b.ToTable("search_query_logs", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Surveys.ServiceRating", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CommentAr") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("comment_ar"); + + b.Property("CommentEn") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("comment_en"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale"); + + b.Property("Page") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("page"); + + b.Property("Rating") + .HasColumnType("int") + .HasColumnName("rating"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_service_ratings"); + + b.HasIndex("SubmittedOn") + .HasDatabaseName("ix_service_rating_submitted_on"); + + b.ToTable("service_ratings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Verification.OtpVerification", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AttemptCount") + .HasColumnType("int") + .HasColumnName("attempt_count"); + + b.Property("CodeHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("code_hash"); + + b.Property("Contact") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("contact"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset") + .HasColumnName("expires_at"); + + b.Property("ExtraData") + .HasColumnType("nvarchar(max)") + .HasColumnName("extra_data"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("IsInvalidated") + .HasColumnType("bit") + .HasColumnName("is_invalidated"); + + b.Property("IsVerified") + .HasColumnType("bit") + .HasColumnName("is_verified"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LastSentAt") + .HasColumnType("datetimeoffset") + .HasColumnName("last_sent_at"); + + b.Property("TypeId") + .HasColumnType("int") + .HasColumnName("type_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_otp_verifications"); + + b.HasIndex("Contact", "TypeId") + .HasDatabaseName("ix_otp_verifications_contact_type_id"); + + b.HasIndex("UserId", "Contact", "TypeId") + .HasDatabaseName("ix_otp_verifications_user_contact_type"); + + b.ToTable("otp_verifications", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Verification.UserVerification", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Contact") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("contact"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("IsVerified") + .HasColumnType("bit") + .HasColumnName("is_verified"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("TypeId") + .HasColumnType("int") + .HasColumnName("type_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("VerifiedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_user_verifications"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_verifications_user_id"); + + b.HasIndex("Contact", "TypeId") + .IsUnique() + .HasDatabaseName("ix_user_verifications_contact_type_id"); + + b.ToTable("user_verifications", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_asp_net_role_claims"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_asp_net_role_claims_role_id"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_asp_net_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_asp_net_user_claims_user_id"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_asp_net_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_asp_net_user_logins_user_id"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier") + .HasColumnName("role_id"); + + b.HasKey("UserId", "RoleId") + .HasName("pk_asp_net_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_asp_net_user_roles_role_id"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("nvarchar(450)") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("nvarchar(max)") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_asp_net_user_tokens"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.ResourceCountry", b => + { + b.HasOne("CCE.Domain.Content.Resource", null) + .WithMany("Countries") + .HasForeignKey("ResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_resource_country_resources_resource_id"); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRequestAttachment", b => + { + b.HasOne("CCE.Domain.Identity.ExpertRegistrationRequest", null) + .WithMany("Attachments") + .HasForeignKey("ExpertRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_expert_request_attachments_expert_registration_requests_expert_request_id"); + }); + + modelBuilder.Entity("CCE.Domain.Identity.RefreshToken", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_asp_net_users_user_id"); + }); + + modelBuilder.Entity("CCE.Domain.Lookups.CountryCode", b => + { + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Name", b1 => + { + b1.Property("CountryCodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b1.HasKey("CountryCodeId"); + + b1.ToTable("country_codes"); + + b1.WithOwner() + .HasForeignKey("CountryCodeId") + .HasConstraintName("fk_country_codes_country_codes_id"); + }); + + b.Navigation("Name") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.AboutSettings", b => + { + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Description", b1 => + { + b1.Property("AboutSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_en"); + + b1.HasKey("AboutSettingsId"); + + b1.ToTable("about_settings"); + + b1.WithOwner() + .HasForeignKey("AboutSettingsId") + .HasConstraintName("fk_about_settings_about_settings_id"); + }); + + b.Navigation("Description") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.GlossaryEntry", b => + { + b.HasOne("CCE.Domain.PlatformSettings.AboutSettings", null) + .WithMany("GlossaryEntries") + .HasForeignKey("AboutSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_glossary_entries_about_settings_about_settings_id"); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Definition", b1 => + { + b1.Property("GlossaryEntryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("definition_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("definition_en"); + + b1.HasKey("GlossaryEntryId"); + + b1.ToTable("glossary_entries"); + + b1.WithOwner() + .HasForeignKey("GlossaryEntryId") + .HasConstraintName("fk_glossary_entries_glossary_entries_id"); + }); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Term", b1 => + { + b1.Property("GlossaryEntryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("term_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("term_en"); + + b1.HasKey("GlossaryEntryId"); + + b1.ToTable("glossary_entries"); + + b1.WithOwner() + .HasForeignKey("GlossaryEntryId") + .HasConstraintName("fk_glossary_entries_glossary_entries_id"); + }); + + b.Navigation("Definition") + .IsRequired(); + + b.Navigation("Term") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageCountry", b => + { + b.HasOne("CCE.Domain.PlatformSettings.HomepageSettings", null) + .WithMany("Countries") + .HasForeignKey("HomepageSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_homepage_countries_homepage_settings_homepage_settings_id"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageSettings", b => + { + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Objective", b1 => + { + b1.Property("HomepageSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("objective_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("objective_en"); + + b1.HasKey("HomepageSettingsId"); + + b1.ToTable("homepage_settings"); + + b1.WithOwner() + .HasForeignKey("HomepageSettingsId") + .HasConstraintName("fk_homepage_settings_homepage_settings_id"); + }); + + b.Navigation("Objective") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.KnowledgePartner", b => + { + b.HasOne("CCE.Domain.PlatformSettings.AboutSettings", null) + .WithMany("KnowledgePartners") + .HasForeignKey("AboutSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_knowledge_partners_about_settings_about_settings_id"); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Description", b1 => + { + b1.Property("KnowledgePartnerId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_en"); + + b1.HasKey("KnowledgePartnerId"); + + b1.ToTable("knowledge_partners"); + + b1.WithOwner() + .HasForeignKey("KnowledgePartnerId") + .HasConstraintName("fk_knowledge_partners_knowledge_partners_id"); + }); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Name", b1 => + { + b1.Property("KnowledgePartnerId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("name_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("name_en"); + + b1.HasKey("KnowledgePartnerId"); + + b1.ToTable("knowledge_partners"); + + b1.WithOwner() + .HasForeignKey("KnowledgePartnerId") + .HasConstraintName("fk_knowledge_partners_knowledge_partners_id"); + }); + + b.Navigation("Description"); + + b.Navigation("Name") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PolicySection", b => + { + b.HasOne("CCE.Domain.PlatformSettings.PoliciesSettings", null) + .WithMany("Sections") + .HasForeignKey("PoliciesSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_policy_sections_policies_settings_policies_settings_id"); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Content", b1 => + { + b1.Property("PolicySectionId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_ar"); + + b1.Property("En") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_en"); + + b1.HasKey("PolicySectionId"); + + b1.ToTable("policy_sections"); + + b1.WithOwner() + .HasForeignKey("PolicySectionId") + .HasConstraintName("fk_policy_sections_policy_sections_id"); + }); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Title", b1 => + { + b1.Property("PolicySectionId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("title_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("title_en"); + + b1.HasKey("PolicySectionId"); + + b1.ToTable("policy_sections"); + + b1.WithOwner() + .HasForeignKey("PolicySectionId") + .HasConstraintName("fk_policy_sections_policy_sections_id"); + }); + + b.Navigation("Content") + .IsRequired(); + + b.Navigation("Title") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.Verification.UserVerification", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_user_verifications_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("CCE.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_role_claims_asp_net_roles_role_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_claims_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_logins_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("CCE.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_roles_asp_net_roles_role_id"); + + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_roles_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_tokens_asp_net_users_user_id"); + }); + + modelBuilder.Entity("CCE.Domain.Content.Resource", b => + { + b.Navigation("Countries"); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRegistrationRequest", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.AboutSettings", b => + { + b.Navigation("GlossaryEntries"); + + b.Navigation("KnowledgePartners"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageSettings", b => + { + b.Navigation("Countries"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PoliciesSettings", b => + { + b.Navigation("Sections"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/20260531170325_ExpandResourceTypeAndAddCountries.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260531170325_ExpandResourceTypeAndAddCountries.cs new file mode 100644 index 00000000..9e35b546 --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260531170325_ExpandResourceTypeAndAddCountries.cs @@ -0,0 +1,54 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CCE.Infrastructure.Persistence.Migrations +{ + /// + public partial class ExpandResourceTypeAndAddCountries : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_resource_country_id", + table: "resources"); + + migrationBuilder.CreateTable( + name: "resource_country", + columns: table => new + { + resource_id = table.Column(type: "uniqueidentifier", nullable: false), + country_id = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_resource_country", x => new { x.resource_id, x.country_id }); + table.ForeignKey( + name: "fk_resource_country_resources_resource_id", + column: x => x.resource_id, + principalTable: "resources", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_resource_country_country_id", + table: "resource_country", + column: "country_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "resource_country"); + + migrationBuilder.CreateIndex( + name: "ix_resource_country_id", + table: "resources", + column: "country_id"); + } + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/20260531210555_AddTopicIdToNewsAndEvents.Designer.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260531210555_AddTopicIdToNewsAndEvents.Designer.cs new file mode 100644 index 00000000..1396ba2d --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260531210555_AddTopicIdToNewsAndEvents.Designer.cs @@ -0,0 +1,3955 @@ +// +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("20260531210555_AddTopicIdToNewsAndEvents")] + partial class AddTopicIdToNewsAndEvents + { + /// + 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.Property("TopicId") + .HasColumnType("uniqueidentifier") + .HasColumnName("topic_id"); + + b.HasKey("Id") + .HasName("pk_events"); + + b.HasIndex("ICalUid") + .IsUnique() + .HasDatabaseName("ux_event_ical_uid"); + + b.HasIndex("StartsOn") + .HasDatabaseName("ix_event_starts_on"); + + b.HasIndex("TopicId") + .HasDatabaseName("ix_event_topic_id"); + + 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.Property("TopicId") + .HasColumnType("uniqueidentifier") + .HasColumnName("topic_id"); + + 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.HasIndex("TopicId") + .HasDatabaseName("ix_news_topic_id"); + + 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(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .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("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.Content.ResourceCountry", b => + { + b.Property("ResourceId") + .HasColumnType("uniqueidentifier") + .HasColumnName("resource_id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.HasKey("ResourceId", "CountryId") + .HasName("pk_resource_country"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_resource_country_country_id"); + + b.ToTable("resource_country", (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.Evaluation.ServiceEvaluation", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ContentSuitability") + .HasColumnType("int") + .HasColumnName("content_suitability"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("EaseOfUse") + .HasColumnType("int") + .HasColumnName("ease_of_use"); + + b.Property("Feedback") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("feedback"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OverallSatisfaction") + .HasColumnType("int") + .HasColumnName("overall_satisfaction"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_service_evaluations"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_service_evaluation_created_on"); + + b.ToTable("service_evaluations", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertProfile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AcademicTitleAr") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("academic_title_ar"); + + b.Property("AcademicTitleEn") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("academic_title_en"); + + b.Property("ApprovedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("approved_by_id"); + + b.Property("ApprovedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("approved_on"); + + b.Property("BioAr") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("bio_ar"); + + b.Property("BioEn") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("bio_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.PrimitiveCollection("ExpertiseTags") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("expertise_tags"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_expert_profiles"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("ux_expert_profile_active_user") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("expert_profiles", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRegistrationRequest", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("ProcessedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("processed_by_id"); + + b.Property("ProcessedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("processed_on"); + + b.Property("RejectionReasonAr") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("rejection_reason_ar"); + + b.Property("RejectionReasonEn") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("rejection_reason_en"); + + b.Property("RequestedBioAr") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("requested_bio_ar"); + + b.Property("RequestedBioEn") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("requested_bio_en"); + + b.Property("RequestedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("requested_by_id"); + + b.PrimitiveCollection("RequestedTags") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("requested_tags"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.HasKey("Id") + .HasName("pk_expert_registration_requests"); + + b.HasIndex("RequestedById") + .HasDatabaseName("ix_expert_request_requested_by"); + + b.HasIndex("Status") + .HasDatabaseName("ix_expert_request_status"); + + b.ToTable("expert_registration_requests", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRequestAttachment", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssetFileId") + .HasColumnType("uniqueidentifier") + .HasColumnName("asset_file_id"); + + b.Property("AttachmentType") + .HasColumnType("int") + .HasColumnName("attachment_type"); + + b.Property("ExpertRequestId") + .HasColumnType("uniqueidentifier") + .HasColumnName("expert_request_id"); + + b.Property("UploadedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("uploaded_at"); + + b.HasKey("Id") + .HasName("pk_expert_request_attachments"); + + b.HasIndex("ExpertRequestId") + .HasDatabaseName("ix_expert_request_attachments_expert_request_id"); + + b.ToTable("expert_request_attachments", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset") + .HasColumnName("created_at_utc"); + + b.Property("CreatedByIp") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("created_by_ip"); + + b.Property("ExpiresAtUtc") + .HasColumnType("datetimeoffset") + .HasColumnName("expires_at_utc"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("replaced_by_token_hash"); + + b.Property("RevokedAtUtc") + .HasColumnType("datetimeoffset") + .HasColumnName("revoked_at_utc"); + + b.Property("RevokedByIp") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("revoked_by_ip"); + + b.Property("TokenFamilyId") + .HasColumnType("uniqueidentifier") + .HasColumnName("token_family_id"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("token_hash"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("user_agent"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("TokenFamilyId") + .HasDatabaseName("ix_refresh_tokens_token_family_id"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ux_refresh_tokens_token_hash"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_refresh_tokens_user_id"); + + b.ToTable("refresh_tokens", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("normalized_name"); + + b.HasKey("Id") + .HasName("pk_asp_net_roles"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[normalized_name] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.StateRepresentativeAssignment", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssignedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("assigned_by_id"); + + b.Property("AssignedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("assigned_on"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RevokedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("revoked_by_id"); + + b.Property("RevokedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("revoked_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_state_representative_assignments"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_state_rep_country_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_state_rep_user_id"); + + b.HasIndex("UserId", "CountryId") + .IsUnique() + .HasDatabaseName("ux_state_rep_active_user_country") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("state_representative_assignments", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AccessFailedCount") + .HasColumnType("int") + .HasColumnName("access_failed_count"); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("avatar_url"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)") + .HasColumnName("concurrency_stamp"); + + b.Property("CountryCodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_code_id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("bit") + .HasColumnName("email_confirmed"); + + b.Property("EntraIdObjectId") + .HasColumnType("uniqueidentifier") + .HasColumnName("entra_id_object_id"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("first_name"); + + b.PrimitiveCollection("Interests") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("interests"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("job_title"); + + b.Property("KnowledgeLevel") + .HasColumnType("int") + .HasColumnName("knowledge_level"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("last_name"); + + b.Property("LocalePreference") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale_preference"); + + b.Property("LockoutEnabled") + .HasColumnType("bit") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("normalized_user_name"); + + b.Property("OrganizationName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("organization_name"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)") + .HasColumnName("password_hash"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit") + .HasColumnName("phone_number_confirmed"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)") + .HasColumnName("security_stamp"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit") + .HasColumnName("two_factor_enabled"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("pk_asp_net_users"); + + b.HasIndex("CountryCodeId") + .HasDatabaseName("ix_users_country_code_id"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_users_country_id"); + + b.HasIndex("EntraIdObjectId") + .IsUnique() + .HasDatabaseName("ix_asp_net_users_entra_id_object_id") + .HasFilter("[entra_id_object_id] IS NOT NULL"); + + b.HasIndex("NormalizedEmail") + .IsUnique() + .HasDatabaseName("ix_users_normalized_email_unique") + .HasFilter("[normalized_email] IS NOT NULL"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[normalized_user_name] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityScenario", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CityType") + .HasColumnType("int") + .HasColumnName("city_type"); + + b.Property("ConfigurationJson") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("configuration_json"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("TargetYear") + .HasColumnType("int") + .HasColumnName("target_year"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_city_scenarios"); + + b.HasIndex("UserId", "LastModifiedOn") + .HasDatabaseName("ix_city_scenario_user_modified"); + + b.ToTable("city_scenarios", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityScenarioResult", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("ComputedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("computed_at"); + + b.Property("ComputedCarbonNeutralityYear") + .HasColumnType("int") + .HasColumnName("computed_carbon_neutrality_year"); + + b.Property("ComputedTotalCostUsd") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("computed_total_cost_usd"); + + b.Property("EngineVersion") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("engine_version"); + + b.Property("ScenarioId") + .HasColumnType("uniqueidentifier") + .HasColumnName("scenario_id"); + + b.HasKey("Id") + .HasName("pk_city_scenario_results"); + + b.HasIndex("ScenarioId", "ComputedAt") + .HasDatabaseName("ix_city_result_scenario_at"); + + b.ToTable("city_scenario_results", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.InteractiveCity.CityTechnology", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CarbonImpactKgPerYear") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("carbon_impact_kg_per_year"); + + b.Property("CategoryAr") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("category_ar"); + + b.Property("CategoryEn") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("category_en"); + + b.Property("CostUsd") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("cost_usd"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IconUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("icon_url"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.HasKey("Id") + .HasName("pk_city_technologies"); + + b.HasIndex("IsActive") + .HasDatabaseName("ix_city_tech_is_active"); + + b.ToTable("city_technologies", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMap", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DescriptionAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_knowledge_maps"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ux_knowledge_map_slug_active") + .HasFilter("[is_deleted] = 0"); + + b.ToTable("knowledge_maps", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMapAssociation", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AssociatedId") + .HasColumnType("uniqueidentifier") + .HasColumnName("associated_id"); + + b.Property("AssociatedType") + .HasColumnType("int") + .HasColumnName("associated_type"); + + b.Property("NodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("node_id"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_knowledge_map_associations"); + + b.HasIndex("NodeId", "AssociatedType", "AssociatedId") + .IsUnique() + .HasDatabaseName("ux_km_assoc_node_type_id"); + + b.ToTable("knowledge_map_associations", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMapEdge", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("FromNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("from_node_id"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("map_id"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("RelationshipType") + .HasColumnType("int") + .HasColumnName("relationship_type"); + + b.Property("ToNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("to_node_id"); + + b.HasKey("Id") + .HasName("pk_knowledge_map_edges"); + + b.HasIndex("FromNodeId") + .HasDatabaseName("ix_km_edge_from_node"); + + b.HasIndex("ToNodeId") + .HasDatabaseName("ix_km_edge_to_node"); + + b.HasIndex("MapId", "FromNodeId", "ToNodeId", "RelationshipType") + .IsUnique() + .HasDatabaseName("ux_km_edge_map_from_to_relation"); + + b.ToTable("knowledge_map_edges", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.KnowledgeMaps.KnowledgeMapNode", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("DescriptionAr") + .HasColumnType("nvarchar(max)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .HasColumnType("nvarchar(max)") + .HasColumnName("description_en"); + + b.Property("IconUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("icon_url"); + + b.Property("LayoutX") + .HasColumnType("float") + .HasColumnName("layout_x"); + + b.Property("LayoutY") + .HasColumnType("float") + .HasColumnName("layout_y"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("map_id"); + + b.Property("NameAr") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b.Property("NodeType") + .HasColumnType("int") + .HasColumnName("node_type"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_knowledge_map_nodes"); + + b.HasIndex("MapId", "OrderIndex") + .HasDatabaseName("ix_km_node_map_order"); + + b.ToTable("knowledge_map_nodes", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Lookups.CountryCode", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("DialCode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("nvarchar(16)") + .HasColumnName("dial_code"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.HasKey("Id") + .HasName("pk_country_codes"); + + b.HasIndex("DialCode") + .HasDatabaseName("ix_country_code_dial_code"); + + b.ToTable("country_codes", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Media.MediaFile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AltTextAr") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("alt_text_ar"); + + b.Property("AltTextEn") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("alt_text_en"); + + b.Property("DescriptionAr") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_ar"); + + b.Property("DescriptionEn") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_en"); + + b.Property("MimeType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("mime_type"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("original_file_name"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size_bytes"); + + b.Property("StorageKey") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("storage_key"); + + b.Property("TitleAr") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("title_ar"); + + b.Property("TitleEn") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("title_en"); + + b.Property("UploadedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("uploaded_by_id"); + + b.Property("UploadedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("uploaded_on"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("pk_media_files"); + + b.ToTable("media_files", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.NotificationLog", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AttemptCount") + .HasColumnType("int") + .HasColumnName("attempt_count"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("correlation_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FailedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("failed_on"); + + b.Property("PayloadJson") + .HasColumnType("nvarchar(max)") + .HasColumnName("payload_json"); + + b.Property("ProviderMessageId") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("provider_message_id"); + + b.Property("RecipientUserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("recipient_user_id"); + + b.Property("SentOn") + .HasColumnType("datetimeoffset") + .HasColumnName("sent_on"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("TemplateCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("template_code"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier") + .HasColumnName("template_id"); + + b.HasKey("Id") + .HasName("pk_notification_logs"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_notification_log_correlation_id"); + + b.HasIndex("TemplateCode", "Channel") + .HasDatabaseName("ix_notification_log_template_channel"); + + b.HasIndex("RecipientUserId", "Status", "CreatedOn") + .HasDatabaseName("ix_notification_log_recipient_status_created"); + + b.ToTable("notification_logs", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.NotificationTemplate", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("BodyAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("body_ar"); + + b.Property("BodyEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("body_en"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("code"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("is_active"); + + b.Property("SubjectAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("subject_ar"); + + b.Property("SubjectEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("subject_en"); + + b.Property("VariableSchemaJson") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("variable_schema_json"); + + b.HasKey("Id") + .HasName("pk_notification_templates"); + + b.HasIndex("Code", "Channel") + .IsUnique() + .HasDatabaseName("ux_notification_template_code_channel"); + + b.ToTable("notification_templates", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.UserNotification", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("ReadOn") + .HasColumnType("datetimeoffset") + .HasColumnName("read_on"); + + b.Property("RenderedBody") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("rendered_body"); + + b.Property("RenderedLocale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("rendered_locale"); + + b.Property("RenderedSubjectAr") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("rendered_subject_ar"); + + b.Property("RenderedSubjectEn") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("rendered_subject_en"); + + b.Property("SentOn") + .HasColumnType("datetimeoffset") + .HasColumnName("sent_on"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier") + .HasColumnName("template_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_notifications"); + + b.HasIndex("UserId", "Status") + .HasDatabaseName("ix_user_notification_user_status"); + + b.ToTable("user_notifications", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Notifications.UserNotificationSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Channel") + .HasColumnType("int") + .HasColumnName("channel"); + + b.Property("EventCode") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("event_code"); + + b.Property("IsEnabled") + .HasColumnType("bit") + .HasColumnName("is_enabled"); + + b.Property("UpdatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("updated_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_notification_settings"); + + b.HasIndex("UserId", "Channel", "EventCode") + .IsUnique() + .HasDatabaseName("ux_user_notification_settings_user_channel_event") + .HasFilter("[event_code] IS NOT NULL"); + + b.ToTable("user_notification_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.AboutSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("HowToUseVideoUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("how_to_use_video_url"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.HasKey("Id") + .HasName("pk_about_settings"); + + b.ToTable("about_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.GlossaryEntry", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AboutSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("about_settings_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_glossary_entries"); + + b.HasIndex("AboutSettingsId") + .HasDatabaseName("ix_glossary_entries_about_settings_id"); + + b.ToTable("glossary_entries", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageCountry", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("HomepageSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("homepage_settings_id"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.HasKey("Id") + .HasName("pk_homepage_countries"); + + b.HasIndex("HomepageSettingsId", "CountryId") + .IsUnique() + .HasDatabaseName("ix_homepage_country_settings_country"); + + b.ToTable("homepage_countries", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CceConceptsAr") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("cce_concepts_ar"); + + b.Property("CceConceptsEn") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("cce_concepts_en"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.Property("VideoUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("video_url"); + + b.HasKey("Id") + .HasName("pk_homepage_settings"); + + b.ToTable("homepage_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.KnowledgePartner", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AboutSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("about_settings_id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LogoUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("logo_url"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("WebsiteUrl") + .HasColumnType("nvarchar(max)") + .HasColumnName("website_url"); + + b.HasKey("Id") + .HasName("pk_knowledge_partners"); + + b.HasIndex("AboutSettingsId") + .HasDatabaseName("ix_knowledge_partners_about_settings_id"); + + b.ToTable("knowledge_partners", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PoliciesSettings", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion") + .HasColumnName("row_version"); + + b.HasKey("Id") + .HasName("pk_policies_settings"); + + b.ToTable("policies_settings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PolicySection", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("OrderIndex") + .HasColumnType("int") + .HasColumnName("order_index"); + + b.Property("PoliciesSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("policies_settings_id"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_policy_sections"); + + b.HasIndex("PoliciesSettingsId") + .HasDatabaseName("ix_policy_sections_policies_settings_id"); + + b.ToTable("policy_sections", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Surveys.SearchQueryLog", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale"); + + b.Property("QueryText") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("query_text"); + + b.Property("ResponseTimeMs") + .HasColumnType("int") + .HasColumnName("response_time_ms"); + + b.Property("ResultsCount") + .HasColumnType("int") + .HasColumnName("results_count"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_search_query_logs"); + + b.HasIndex("SubmittedOn") + .HasDatabaseName("ix_search_query_log_submitted_on"); + + b.ToTable("search_query_logs", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Surveys.ServiceRating", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CommentAr") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("comment_ar"); + + b.Property("CommentEn") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("comment_en"); + + b.Property("Locale") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)") + .HasColumnName("locale"); + + b.Property("Page") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("page"); + + b.Property("Rating") + .HasColumnType("int") + .HasColumnName("rating"); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("submitted_on"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_service_ratings"); + + b.HasIndex("SubmittedOn") + .HasDatabaseName("ix_service_rating_submitted_on"); + + b.ToTable("service_ratings", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Verification.OtpVerification", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("AttemptCount") + .HasColumnType("int") + .HasColumnName("attempt_count"); + + b.Property("CodeHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("code_hash"); + + b.Property("Contact") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("contact"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset") + .HasColumnName("expires_at"); + + b.Property("ExtraData") + .HasColumnType("nvarchar(max)") + .HasColumnName("extra_data"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("IsInvalidated") + .HasColumnType("bit") + .HasColumnName("is_invalidated"); + + b.Property("IsVerified") + .HasColumnType("bit") + .HasColumnName("is_verified"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("LastSentAt") + .HasColumnType("datetimeoffset") + .HasColumnName("last_sent_at"); + + b.Property("TypeId") + .HasColumnType("int") + .HasColumnName("type_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_otp_verifications"); + + b.HasIndex("Contact", "TypeId") + .HasDatabaseName("ix_otp_verifications_contact_type_id"); + + b.HasIndex("UserId", "Contact", "TypeId") + .HasDatabaseName("ix_otp_verifications_user_contact_type"); + + b.ToTable("otp_verifications", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Verification.UserVerification", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("Contact") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("contact"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("created_by_id"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("created_on"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("deleted_by_id"); + + b.Property("DeletedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("deleted_on"); + + b.Property("IsDeleted") + .HasColumnType("bit") + .HasColumnName("is_deleted"); + + b.Property("IsVerified") + .HasColumnType("bit") + .HasColumnName("is_verified"); + + b.Property("LastModifiedById") + .HasColumnType("uniqueidentifier") + .HasColumnName("last_modified_by_id"); + + b.Property("LastModifiedOn") + .HasColumnType("datetimeoffset") + .HasColumnName("last_modified_on"); + + b.Property("TypeId") + .HasColumnType("int") + .HasColumnName("type_id"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("VerifiedAt") + .HasColumnType("datetimeoffset") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_user_verifications"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_verifications_user_id"); + + b.HasIndex("Contact", "TypeId") + .IsUnique() + .HasDatabaseName("ix_user_verifications_contact_type_id"); + + b.ToTable("user_verifications", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_asp_net_role_claims"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_asp_net_role_claims_role_id"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_asp_net_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_asp_net_user_claims_user_id"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_asp_net_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_asp_net_user_logins_user_id"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier") + .HasColumnName("role_id"); + + b.HasKey("UserId", "RoleId") + .HasName("pk_asp_net_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_asp_net_user_roles_role_id"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("nvarchar(450)") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("nvarchar(max)") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_asp_net_user_tokens"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("CCE.Domain.Content.ResourceCountry", b => + { + b.HasOne("CCE.Domain.Content.Resource", null) + .WithMany("Countries") + .HasForeignKey("ResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_resource_country_resources_resource_id"); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRequestAttachment", b => + { + b.HasOne("CCE.Domain.Identity.ExpertRegistrationRequest", null) + .WithMany("Attachments") + .HasForeignKey("ExpertRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_expert_request_attachments_expert_registration_requests_expert_request_id"); + }); + + modelBuilder.Entity("CCE.Domain.Identity.RefreshToken", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_asp_net_users_user_id"); + }); + + modelBuilder.Entity("CCE.Domain.Lookups.CountryCode", b => + { + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Name", b1 => + { + b1.Property("CountryCodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("name_en"); + + b1.HasKey("CountryCodeId"); + + b1.ToTable("country_codes"); + + b1.WithOwner() + .HasForeignKey("CountryCodeId") + .HasConstraintName("fk_country_codes_country_codes_id"); + }); + + b.Navigation("Name") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.AboutSettings", b => + { + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Description", b1 => + { + b1.Property("AboutSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_en"); + + b1.HasKey("AboutSettingsId"); + + b1.ToTable("about_settings"); + + b1.WithOwner() + .HasForeignKey("AboutSettingsId") + .HasConstraintName("fk_about_settings_about_settings_id"); + }); + + b.Navigation("Description") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.GlossaryEntry", b => + { + b.HasOne("CCE.Domain.PlatformSettings.AboutSettings", null) + .WithMany("GlossaryEntries") + .HasForeignKey("AboutSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_glossary_entries_about_settings_about_settings_id"); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Definition", b1 => + { + b1.Property("GlossaryEntryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("definition_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("definition_en"); + + b1.HasKey("GlossaryEntryId"); + + b1.ToTable("glossary_entries"); + + b1.WithOwner() + .HasForeignKey("GlossaryEntryId") + .HasConstraintName("fk_glossary_entries_glossary_entries_id"); + }); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Term", b1 => + { + b1.Property("GlossaryEntryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("term_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("term_en"); + + b1.HasKey("GlossaryEntryId"); + + b1.ToTable("glossary_entries"); + + b1.WithOwner() + .HasForeignKey("GlossaryEntryId") + .HasConstraintName("fk_glossary_entries_glossary_entries_id"); + }); + + b.Navigation("Definition") + .IsRequired(); + + b.Navigation("Term") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageCountry", b => + { + b.HasOne("CCE.Domain.PlatformSettings.HomepageSettings", null) + .WithMany("Countries") + .HasForeignKey("HomepageSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_homepage_countries_homepage_settings_homepage_settings_id"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageSettings", b => + { + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Objective", b1 => + { + b1.Property("HomepageSettingsId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("objective_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("objective_en"); + + b1.HasKey("HomepageSettingsId"); + + b1.ToTable("homepage_settings"); + + b1.WithOwner() + .HasForeignKey("HomepageSettingsId") + .HasConstraintName("fk_homepage_settings_homepage_settings_id"); + }); + + b.Navigation("Objective") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.KnowledgePartner", b => + { + b.HasOne("CCE.Domain.PlatformSettings.AboutSettings", null) + .WithMany("KnowledgePartners") + .HasForeignKey("AboutSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_knowledge_partners_about_settings_about_settings_id"); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Description", b1 => + { + b1.Property("KnowledgePartnerId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("description_en"); + + b1.HasKey("KnowledgePartnerId"); + + b1.ToTable("knowledge_partners"); + + b1.WithOwner() + .HasForeignKey("KnowledgePartnerId") + .HasConstraintName("fk_knowledge_partners_knowledge_partners_id"); + }); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Name", b1 => + { + b1.Property("KnowledgePartnerId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("name_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("name_en"); + + b1.HasKey("KnowledgePartnerId"); + + b1.ToTable("knowledge_partners"); + + b1.WithOwner() + .HasForeignKey("KnowledgePartnerId") + .HasConstraintName("fk_knowledge_partners_knowledge_partners_id"); + }); + + b.Navigation("Description"); + + b.Navigation("Name") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PolicySection", b => + { + b.HasOne("CCE.Domain.PlatformSettings.PoliciesSettings", null) + .WithMany("Sections") + .HasForeignKey("PoliciesSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_policy_sections_policies_settings_policies_settings_id"); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Content", b1 => + { + b1.Property("PolicySectionId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_ar"); + + b1.Property("En") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("content_en"); + + b1.HasKey("PolicySectionId"); + + b1.ToTable("policy_sections"); + + b1.WithOwner() + .HasForeignKey("PolicySectionId") + .HasConstraintName("fk_policy_sections_policy_sections_id"); + }); + + b.OwnsOne("CCE.Domain.PlatformSettings.ValueObjects.LocalizedText", "Title", b1 => + { + b1.Property("PolicySectionId") + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b1.Property("Ar") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("title_ar"); + + b1.Property("En") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("title_en"); + + b1.HasKey("PolicySectionId"); + + b1.ToTable("policy_sections"); + + b1.WithOwner() + .HasForeignKey("PolicySectionId") + .HasConstraintName("fk_policy_sections_policy_sections_id"); + }); + + b.Navigation("Content") + .IsRequired(); + + b.Navigation("Title") + .IsRequired(); + }); + + modelBuilder.Entity("CCE.Domain.Verification.UserVerification", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_user_verifications_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("CCE.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_role_claims_asp_net_roles_role_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_claims_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_logins_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("CCE.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_roles_asp_net_roles_role_id"); + + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_roles_asp_net_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("CCE.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_asp_net_user_tokens_asp_net_users_user_id"); + }); + + modelBuilder.Entity("CCE.Domain.Content.Resource", b => + { + b.Navigation("Countries"); + }); + + modelBuilder.Entity("CCE.Domain.Identity.ExpertRegistrationRequest", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.AboutSettings", b => + { + b.Navigation("GlossaryEntries"); + + b.Navigation("KnowledgePartners"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.HomepageSettings", b => + { + b.Navigation("Countries"); + }); + + modelBuilder.Entity("CCE.Domain.PlatformSettings.PoliciesSettings", b => + { + b.Navigation("Sections"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/20260531210555_AddTopicIdToNewsAndEvents.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260531210555_AddTopicIdToNewsAndEvents.cs new file mode 100644 index 00000000..ba798aec --- /dev/null +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/20260531210555_AddTopicIdToNewsAndEvents.cs @@ -0,0 +1,99 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CCE.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddTopicIdToNewsAndEvents : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "title_en", + table: "resources", + type: "nvarchar(255)", + maxLength: 255, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(512)", + oldMaxLength: 512); + + migrationBuilder.AlterColumn( + name: "title_ar", + table: "resources", + type: "nvarchar(255)", + maxLength: 255, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(512)", + oldMaxLength: 512); + + migrationBuilder.AddColumn( + name: "topic_id", + table: "news", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "topic_id", + table: "events", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.CreateIndex( + name: "ix_news_topic_id", + table: "news", + column: "topic_id"); + + migrationBuilder.CreateIndex( + name: "ix_event_topic_id", + table: "events", + column: "topic_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_news_topic_id", + table: "news"); + + migrationBuilder.DropIndex( + name: "ix_event_topic_id", + table: "events"); + + migrationBuilder.DropColumn( + name: "topic_id", + table: "news"); + + migrationBuilder.DropColumn( + name: "topic_id", + table: "events"); + + migrationBuilder.AlterColumn( + name: "title_en", + table: "resources", + type: "nvarchar(512)", + maxLength: 512, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(255)", + oldMaxLength: 255); + + migrationBuilder.AlterColumn( + name: "title_ar", + table: "resources", + type: "nvarchar(512)", + maxLength: 512, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(255)", + oldMaxLength: 255); + } + } +} diff --git a/backend/src/CCE.Infrastructure/Persistence/Migrations/CceDbContextModelSnapshot.cs b/backend/src/CCE.Infrastructure/Persistence/Migrations/CceDbContextModelSnapshot.cs index 7714ff72..c3282a5e 100644 --- a/backend/src/CCE.Infrastructure/Persistence/Migrations/CceDbContextModelSnapshot.cs +++ b/backend/src/CCE.Infrastructure/Persistence/Migrations/CceDbContextModelSnapshot.cs @@ -579,6 +579,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(512)") .HasColumnName("title_en"); + b.Property("TopicId") + .HasColumnType("uniqueidentifier") + .HasColumnName("topic_id"); + b.HasKey("Id") .HasName("pk_events"); @@ -589,6 +593,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("StartsOn") .HasDatabaseName("ix_event_starts_on"); + b.HasIndex("TopicId") + .HasDatabaseName("ix_event_topic_id"); + b.ToTable("events", (string)null); }); @@ -743,6 +750,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(512)") .HasColumnName("title_en"); + b.Property("TopicId") + .HasColumnType("uniqueidentifier") + .HasColumnName("topic_id"); + b.HasKey("Id") .HasName("pk_news"); @@ -754,6 +765,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDatabaseName("ux_news_slug_active") .HasFilter("[is_deleted] = 0"); + b.HasIndex("TopicId") + .HasDatabaseName("ix_news_topic_id"); + b.ToTable("news", (string)null); }); @@ -991,14 +1005,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("TitleAr") .IsRequired() - .HasMaxLength(512) - .HasColumnType("nvarchar(512)") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") .HasColumnName("title_ar"); b.Property("TitleEn") .IsRequired() - .HasMaxLength(512) - .HasColumnType("nvarchar(512)") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") .HasColumnName("title_en"); b.Property("UploadedById") @@ -1015,9 +1029,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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"); @@ -1073,6 +1084,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("resource_categories", (string)null); }); + modelBuilder.Entity("CCE.Domain.Content.ResourceCountry", b => + { + b.Property("ResourceId") + .HasColumnType("uniqueidentifier") + .HasColumnName("resource_id"); + + b.Property("CountryId") + .HasColumnType("uniqueidentifier") + .HasColumnName("country_id"); + + b.HasKey("ResourceId", "CountryId") + .HasName("pk_resource_country"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_resource_country_country_id"); + + b.ToTable("resource_country", (string)null); + }); + modelBuilder.Entity("CCE.Domain.Country.Country", b => { b.Property("Id") @@ -3479,6 +3509,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); + modelBuilder.Entity("CCE.Domain.Content.ResourceCountry", b => + { + b.HasOne("CCE.Domain.Content.Resource", null) + .WithMany("Countries") + .HasForeignKey("ResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_resource_country_resources_resource_id"); + }); + modelBuilder.Entity("CCE.Domain.Identity.ExpertRequestAttachment", b => { b.HasOne("CCE.Domain.Identity.ExpertRegistrationRequest", null) @@ -3521,7 +3561,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("CountryCodeId"); - b1.ToTable("country_codes", (string)null); + b1.ToTable("country_codes"); b1.WithOwner() .HasForeignKey("CountryCodeId") @@ -3554,7 +3594,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("AboutSettingsId"); - b1.ToTable("about_settings", (string)null); + b1.ToTable("about_settings"); b1.WithOwner() .HasForeignKey("AboutSettingsId") @@ -3594,7 +3634,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("GlossaryEntryId"); - b1.ToTable("glossary_entries", (string)null); + b1.ToTable("glossary_entries"); b1.WithOwner() .HasForeignKey("GlossaryEntryId") @@ -3621,7 +3661,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("GlossaryEntryId"); - b1.ToTable("glossary_entries", (string)null); + b1.ToTable("glossary_entries"); b1.WithOwner() .HasForeignKey("GlossaryEntryId") @@ -3667,7 +3707,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("HomepageSettingsId"); - b1.ToTable("homepage_settings", (string)null); + b1.ToTable("homepage_settings"); b1.WithOwner() .HasForeignKey("HomepageSettingsId") @@ -3707,7 +3747,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("KnowledgePartnerId"); - b1.ToTable("knowledge_partners", (string)null); + b1.ToTable("knowledge_partners"); b1.WithOwner() .HasForeignKey("KnowledgePartnerId") @@ -3734,7 +3774,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("KnowledgePartnerId"); - b1.ToTable("knowledge_partners", (string)null); + b1.ToTable("knowledge_partners"); b1.WithOwner() .HasForeignKey("KnowledgePartnerId") @@ -3774,7 +3814,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("PolicySectionId"); - b1.ToTable("policy_sections", (string)null); + b1.ToTable("policy_sections"); b1.WithOwner() .HasForeignKey("PolicySectionId") @@ -3801,7 +3841,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("PolicySectionId"); - b1.ToTable("policy_sections", (string)null); + b1.ToTable("policy_sections"); b1.WithOwner() .HasForeignKey("PolicySectionId") @@ -3880,6 +3920,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasConstraintName("fk_asp_net_user_tokens_asp_net_users_user_id"); }); + modelBuilder.Entity("CCE.Domain.Content.Resource", b => + { + b.Navigation("Countries"); + }); + modelBuilder.Entity("CCE.Domain.Identity.ExpertRegistrationRequest", b => { b.Navigation("Attachments"); diff --git a/backend/src/CCE.Seeder/Seeders/DemoDataSeeder.cs b/backend/src/CCE.Seeder/Seeders/DemoDataSeeder.cs index 912b2d7d..0f57e71f 100644 --- a/backend/src/CCE.Seeder/Seeders/DemoDataSeeder.cs +++ b/backend/src/CCE.Seeder/Seeders/DemoDataSeeder.cs @@ -34,55 +34,66 @@ public async Task SeedAsync(CancellationToken cancellationToken = default) DeterministicGuid.From("user:system_demo_author"); private static readonly (string Slug, string TitleAr, string TitleEn, - string ContentAr, string ContentEn, bool Featured)[] DemoNews = + string ContentAr, string ContentEn, bool Featured, string TopicSlug)[] DemoNews = { ("welcome", "أهلاً بكم في منصة المعرفة", "Welcome to the Knowledge Center", "

منصة جديدة لمشاركة المعرفة حول الاقتصاد الكربوني الدائري.

", "

A new platform for sharing knowledge about the Circular Carbon Economy.

", - true), + true, "general"), ("solar-milestone", "إنجاز جديد في الطاقة الشمسية", "New Solar Milestone", "

تم تجاوز رقم قياسي عالمي في كفاءة الخلايا الشمسية، مع تحقيق 33٪ في ظروف اختبار قياسية.

", "

A new world record was set in solar-cell efficiency, reaching 33% under standard test conditions.

", - false), + false, "solar-power"), ("dac-pilot", "إطلاق مشروع تجريبي للالتقاط المباشر", "Direct Air Capture Pilot Goes Live", "

وحدة جديدة قادرة على التقاط 1000 طن من ثاني أكسيد الكربون سنوياً بدأت العمل في الرياض.

", "

A new unit capable of capturing 1,000 tonnes of CO₂ per year went live near Riyadh.

", - true), + true, "research"), ("methane-leakage", "تقرير: انخفاض كبير في تسرب الميثان", "Report: Major Drop in Methane Leakage", "

تقرير سنوي يظهر انخفاضاً بنسبة 18٪ في انبعاثات الميثان عبر القطاع.

", "

An annual report shows an 18% drop in methane emissions across the sector.

", - false), + false, "research"), ("hydrogen-corridor", "ممر الهيدروجين الإقليمي يبدأ المرحلة الثانية", "Regional Hydrogen Corridor Enters Phase II", "

توسيع ممر الهيدروجين منخفض الكربون ليشمل ثلاث دول إضافية.

", "

The low-carbon hydrogen corridor expands to include three additional countries.

", - false), + false, "general"), }; private async Task SeedNewsAsync(CancellationToken ct) { + var topicMap = await _ctx.Topics + .ToDictionaryAsync(t => t.Slug, t => t.Id, ct).ConfigureAwait(false); + var dayOffset = -1; foreach (var n in DemoNews) { + if (!topicMap.TryGetValue(n.TopicSlug, out var topicId)) + { + _logger.LogWarning( + "DemoDataSeeder: topic '{TopicSlug}' missing — skipping news '{NewsSlug}'.", + n.TopicSlug, n.Slug); + continue; + } + var id = DeterministicGuid.From($"news:{n.Slug}"); var exists = await _ctx.News.IgnoreQueryFilters() .AnyAsync(x => x.Id == id, ct).ConfigureAwait(false); if (exists) { dayOffset -= 7; continue; } var news = News.Draft(n.TitleAr, n.TitleEn, n.ContentAr, n.ContentEn, - n.Slug, SystemAuthorId, featuredImageUrl: null, _clock); + topicId, SystemAuthorId, null, _clock); typeof(News).GetProperty(nameof(news.Id))!.SetValue(news, id); news.Publish(_clock); if (n.Featured) @@ -98,33 +109,44 @@ private async Task SeedNewsAsync(CancellationToken ct) private static readonly (string Slug, string TitleAr, string TitleEn, string DescAr, string DescEn, int DaysFromNow, int LengthHours, - string LocationAr, string LocationEn, string? OnlineUrl)[] DemoEvents = + string LocationAr, string LocationEn, string? OnlineUrl, string TopicSlug)[] DemoEvents = { ("cce-conference", "مؤتمر CCE السنوي", "CCE Annual Conference", "نقاش حول مستقبل الاقتصاد الكربوني", "Discussion on the future of CCE", - 30, 2, "الرياض", "Riyadh", null), + 30, 2, "الرياض", "Riyadh", null, "general"), ("hydrogen-summit", "قمة الهيدروجين الأخضر", "Green Hydrogen Summit", "أحدث التطورات في إنتاج الهيدروجين", "Latest developments in hydrogen production", - 60, 6, "نيوم", "Neom", null), + 60, 6, "نيوم", "Neom", null, "general"), ("dac-workshop", "ورشة الالتقاط المباشر", "DAC Workshop", "ورشة عملية حول تقنيات الالتقاط", "Hands-on workshop on capture technologies", - 15, 4, "عبر الإنترنت", "Online", "https://meet.example.com/dac-workshop"), + 15, 4, "عبر الإنترنت", "Online", "https://meet.example.com/dac-workshop", "research"), ("policy-forum", "منتدى السياسات المناخية", "Climate Policy Forum", "حوار بين صناع السياسات والباحثين", "Dialogue between policymakers and researchers", - 90, 8, "جدة", "Jeddah", null), + 90, 8, "جدة", "Jeddah", null, "policy"), }; private async Task SeedEventsAsync(CancellationToken ct) { + var topicMap = await _ctx.Topics + .ToDictionaryAsync(t => t.Slug, t => t.Id, ct).ConfigureAwait(false); + foreach (var e in DemoEvents) { + if (!topicMap.TryGetValue(e.TopicSlug, out var topicId)) + { + _logger.LogWarning( + "DemoDataSeeder: topic '{TopicSlug}' missing — skipping event '{EventSlug}'.", + e.TopicSlug, e.Slug); + continue; + } + var id = DeterministicGuid.From($"event:demo:{e.Slug}"); var exists = await _ctx.Events.IgnoreQueryFilters() .AnyAsync(x => x.Id == id, ct).ConfigureAwait(false); @@ -138,7 +160,7 @@ private async Task SeedEventsAsync(CancellationToken ct) e.DescAr, e.DescEn, startsOn, endsOn, e.LocationAr, e.LocationEn, - e.OnlineUrl, null, _clock); + e.OnlineUrl, null, topicId, _clock); typeof(CCE.Domain.Content.Event).GetProperty(nameof(ev.Id))!.SetValue(ev, id); _ctx.Events.Add(ev); } diff --git a/backend/tests/CCE.Api.IntegrationTests/Endpoints/EventsEndpointTests.cs b/backend/tests/CCE.Api.IntegrationTests/Endpoints/EventsEndpointTests.cs index 9fceda90..5a8e3e9a 100644 --- a/backend/tests/CCE.Api.IntegrationTests/Endpoints/EventsEndpointTests.cs +++ b/backend/tests/CCE.Api.IntegrationTests/Endpoints/EventsEndpointTests.cs @@ -42,10 +42,11 @@ public async Task List_SuperAdmin_returns_200_with_paged_result_shape() resp.StatusCode.Should().Be(HttpStatusCode.OK); var body = await resp.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(body).RootElement; - doc.GetProperty("items").ValueKind.Should().Be(JsonValueKind.Array); - doc.GetProperty("page").GetInt32().Should().Be(1); - doc.GetProperty("pageSize").GetInt32().Should().Be(20); - doc.GetProperty("total").GetInt64().Should().BeGreaterThanOrEqualTo(0); + var data = doc.GetProperty("data"); + data.GetProperty("items").ValueKind.Should().Be(JsonValueKind.Array); + data.GetProperty("page").GetInt32().Should().Be(1); + data.GetProperty("pageSize").GetInt32().Should().Be(20); + data.GetProperty("total").GetInt64().Should().BeGreaterThanOrEqualTo(0); } [Fact] @@ -83,6 +84,7 @@ public async Task Post_anonymous_returns_401() locationEn = (string?)null, onlineMeetingUrl = (string?)null, featuredImageUrl = (string?)null, + topicId = System.Guid.NewGuid(), }); var resp = await client.PostAsync(new Uri("/api/admin/events", UriKind.Relative), body); @@ -102,7 +104,7 @@ public async Task Put_anonymous_returns_401() locationEn = (string?)null, onlineMeetingUrl = (string?)null, featuredImageUrl = (string?)null, - rowVersion = System.Convert.ToBase64String(new byte[8]), + topicId = System.Guid.NewGuid(), }); var resp = await client.PutAsync(new Uri($"/api/admin/events/{System.Guid.NewGuid()}", UriKind.Relative), body); @@ -123,7 +125,7 @@ public async Task Put_unknown_id_returns_404() locationEn = (string?)null, onlineMeetingUrl = (string?)null, featuredImageUrl = (string?)null, - rowVersion = System.Convert.ToBase64String(new byte[8]), + topicId = System.Guid.NewGuid(), }); var resp = await client.PutAsync(new Uri($"/api/admin/events/{System.Guid.NewGuid()}", UriKind.Relative), body); diff --git a/backend/tests/CCE.Api.IntegrationTests/Endpoints/EventsPublicEndpointTests.cs b/backend/tests/CCE.Api.IntegrationTests/Endpoints/EventsPublicEndpointTests.cs index 27381e2b..0a1cf244 100644 --- a/backend/tests/CCE.Api.IntegrationTests/Endpoints/EventsPublicEndpointTests.cs +++ b/backend/tests/CCE.Api.IntegrationTests/Endpoints/EventsPublicEndpointTests.cs @@ -18,10 +18,11 @@ public async Task List_returns_200_with_paged_result_shape() resp.StatusCode.Should().Be(HttpStatusCode.OK); var body = await resp.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(body).RootElement; - doc.GetProperty("items").ValueKind.Should().Be(JsonValueKind.Array); - doc.GetProperty("page").GetInt32().Should().Be(1); - doc.GetProperty("pageSize").GetInt32().Should().Be(20); - doc.GetProperty("total").GetInt64().Should().BeGreaterThanOrEqualTo(0); + var data = doc.GetProperty("data"); + data.GetProperty("items").ValueKind.Should().Be(JsonValueKind.Array); + data.GetProperty("page").GetInt32().Should().Be(1); + data.GetProperty("pageSize").GetInt32().Should().Be(20); + data.GetProperty("total").GetInt64().Should().BeGreaterThanOrEqualTo(0); } [Fact] diff --git a/backend/tests/CCE.Api.IntegrationTests/Endpoints/NewsEndpointTests.cs b/backend/tests/CCE.Api.IntegrationTests/Endpoints/NewsEndpointTests.cs index 787e4f5d..c3ca9daf 100644 --- a/backend/tests/CCE.Api.IntegrationTests/Endpoints/NewsEndpointTests.cs +++ b/backend/tests/CCE.Api.IntegrationTests/Endpoints/NewsEndpointTests.cs @@ -43,10 +43,11 @@ public async Task List_SuperAdmin_returns_200_with_paged_result_shape() resp.StatusCode.Should().Be(HttpStatusCode.OK); var body = await resp.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(body).RootElement; - doc.GetProperty("items").ValueKind.Should().Be(JsonValueKind.Array); - doc.GetProperty("page").GetInt32().Should().Be(1); - doc.GetProperty("pageSize").GetInt32().Should().Be(20); - doc.GetProperty("total").GetInt64().Should().BeGreaterThanOrEqualTo(0); + var data = doc.GetProperty("data"); + data.GetProperty("items").ValueKind.Should().Be(JsonValueKind.Array); + data.GetProperty("page").GetInt32().Should().Be(1); + data.GetProperty("pageSize").GetInt32().Should().Be(20); + data.GetProperty("total").GetInt64().Should().BeGreaterThanOrEqualTo(0); } [Fact] @@ -78,7 +79,7 @@ public async Task Post_anonymous_returns_401() { titleAr = "خبر", titleEn = "News", contentAr = "محتوى", contentEn = "Content", - slug = "test-post", + topicId = System.Guid.NewGuid(), featuredImageUrl = (string?)null, }); @@ -95,9 +96,8 @@ public async Task Put_anonymous_returns_401() { titleAr = "خبر", titleEn = "News", contentAr = "محتوى", contentEn = "Content", - slug = "test-post", + topicId = System.Guid.NewGuid(), featuredImageUrl = (string?)null, - rowVersion = System.Convert.ToBase64String(new byte[8]), }); var resp = await client.PutAsync(new Uri($"/api/admin/news/{System.Guid.NewGuid()}", UriKind.Relative), body); @@ -114,9 +114,8 @@ public async Task Put_unknown_id_returns_404() { titleAr = "خبر", titleEn = "News", contentAr = "محتوى", contentEn = "Content", - slug = "test-post", + topicId = System.Guid.NewGuid(), featuredImageUrl = (string?)null, - rowVersion = System.Convert.ToBase64String(new byte[8]), }); var resp = await client.PutAsync(new Uri($"/api/admin/news/{System.Guid.NewGuid()}", UriKind.Relative), body); diff --git a/backend/tests/CCE.Api.IntegrationTests/Endpoints/ResourcesEndpointTests.cs b/backend/tests/CCE.Api.IntegrationTests/Endpoints/ResourcesEndpointTests.cs index eb319f6c..277ec31c 100644 --- a/backend/tests/CCE.Api.IntegrationTests/Endpoints/ResourcesEndpointTests.cs +++ b/backend/tests/CCE.Api.IntegrationTests/Endpoints/ResourcesEndpointTests.cs @@ -44,10 +44,11 @@ public async Task SuperAdmin_request_returns_200_with_paged_result_shape() resp.StatusCode.Should().Be(HttpStatusCode.OK); var body = await resp.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(body).RootElement; - doc.GetProperty("items").ValueKind.Should().Be(JsonValueKind.Array); - doc.GetProperty("page").GetInt32().Should().Be(1); - doc.GetProperty("pageSize").GetInt32().Should().Be(20); - doc.GetProperty("total").GetInt64().Should().BeGreaterThanOrEqualTo(0); + var data = doc.GetProperty("data"); + data.GetProperty("items").ValueKind.Should().Be(JsonValueKind.Array); + data.GetProperty("page").GetInt32().Should().Be(1); + data.GetProperty("pageSize").GetInt32().Should().Be(20); + data.GetProperty("total").GetInt64().Should().BeGreaterThanOrEqualTo(0); } [Fact] @@ -98,6 +99,7 @@ public async Task Put_with_unknown_id_returns_404() descriptionAr = "desc-ar", descriptionEn = "desc-en", resourceType = 0, categoryId = System.Guid.NewGuid(), + countryIds = new[] { System.Guid.NewGuid() }, rowVersion = System.Convert.ToBase64String(new byte[8]), }); diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/ApproveCountryResourceRequestCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/ApproveCountryResourceRequestCommandHandlerTests.cs index 5fd1f78e..23cdb456 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/ApproveCountryResourceRequestCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/ApproveCountryResourceRequestCommandHandlerTests.cs @@ -77,7 +77,7 @@ private static CountryResourceRequest BuildPendingRequest(FakeSystemClock clock) System.Guid.NewGuid(), System.Guid.NewGuid(), "عنوان", "Title", "وصف", "Description", - ResourceType.Pdf, System.Guid.NewGuid(), clock); + ResourceType.Paper, System.Guid.NewGuid(), clock); private static ICurrentUserAccessor BuildCurrentUser(System.Guid? userId = null) { diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/CreateEventCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/CreateEventCommandHandlerTests.cs index 94e9548f..5d87f599 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/CreateEventCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/CreateEventCommandHandlerTests.cs @@ -1,5 +1,8 @@ -using CCE.Application.Content; +using CCE.Application.Common.Interfaces; using CCE.Application.Content.Commands.CreateEvent; +using CCE.Application.Localization; +using CCE.Application.Messages; +using CCE.Domain.Common; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -7,6 +10,7 @@ namespace CCE.Application.Tests.Content.Commands; public class CreateEventCommandHandlerTests { + private static readonly System.Guid TopicId = System.Guid.NewGuid(); private static readonly System.DateTimeOffset StartsOn = new(2026, 9, 1, 9, 0, 0, System.TimeSpan.Zero); @@ -16,35 +20,47 @@ public class CreateEventCommandHandlerTests [Fact] public async Task Persists_event_when_inputs_valid() { - var (sut, service) = BuildSut(); + var (sut, repo, db) = BuildSut(TopicId); - await sut.Handle(BuildCmd(), CancellationToken.None); + var result = await sut.Handle(BuildCmd(TopicId), CancellationToken.None); - await service.Received(1).SaveAsync(Arg.Any(), Arg.Any()); + result.Success.Should().BeTrue(); + await repo.Received(1).AddAsync(Arg.Any(), Arg.Any()); + await db.Received(1).SaveChangesAsync(Arg.Any()); } [Fact] public async Task Returns_dto_with_correct_fields() { - var (sut, _) = BuildSut(); + var (sut, _, _) = BuildSut(TopicId); - var dto = await sut.Handle(BuildCmd(), CancellationToken.None); + var result = await sut.Handle(BuildCmd(TopicId), CancellationToken.None); - dto.TitleAr.Should().Be("حدث"); - dto.TitleEn.Should().Be("Event"); - dto.StartsOn.Should().Be(StartsOn); - dto.EndsOn.Should().Be(EndsOn); - dto.ICalUid.Should().EndWith("@cce.moenergy.gov.sa"); + result.Data!.TitleAr.Should().Be("حدث"); + result.Data.TitleEn.Should().Be("Event"); + result.Data.StartsOn.Should().Be(StartsOn); + result.Data.EndsOn.Should().Be(EndsOn); + result.Data.ICalUid.Should().EndWith("@cce.moenergy.gov.sa"); } - private static CreateEventCommand BuildCmd() => + private static CreateEventCommand BuildCmd(System.Guid topicId) => new("حدث", "Event", "وصف", "Description", StartsOn, EndsOn, - null, null, null, null); + null, null, null, null, topicId); - private static (CreateEventCommandHandler sut, IEventRepository service) BuildSut() + private static (CreateEventCommandHandler sut, + IRepository repo, + ICceDbContext db) BuildSut(System.Guid topicId) { - var service = Substitute.For(); - var sut = new CreateEventCommandHandler(service, new FakeSystemClock()); - return (sut, service); + var repo = Substitute.For>(); + var db = Substitute.For(); + var topic = CCE.Domain.Community.Topic.Create( + "name-ar", "name-en", "desc-ar", "desc-en", "slug", null, null, 0); + typeof(CCE.Domain.Community.Topic).GetProperty(nameof(CCE.Domain.Community.Topic.Id))! + .SetValue(topic, topicId); + db.Topics.Returns(new[] { topic }.AsQueryable()); + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + var sut = new CreateEventCommandHandler(repo, db, new FakeSystemClock(), new MessageFactory(localization)); + return (sut, repo, db); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/CreateEventCommandValidatorTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/CreateEventCommandValidatorTests.cs index 69b85d45..651639ae 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/CreateEventCommandValidatorTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/CreateEventCommandValidatorTests.cs @@ -13,7 +13,8 @@ public class CreateEventCommandValidatorTests private static CreateEventCommand ValidCmd() => new( "حدث", "Event", "وصف", "Description", StartsOn, EndsOn, - null, null, null, null); + null, null, null, null, + System.Guid.NewGuid()); [Fact] public void Valid_command_passes() diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/CreateNewsCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/CreateNewsCommandHandlerTests.cs index 1182bb1e..ebc82ebd 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/CreateNewsCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/CreateNewsCommandHandlerTests.cs @@ -1,6 +1,7 @@ using CCE.Application.Common.Interfaces; -using CCE.Application.Content; using CCE.Application.Content.Commands.CreateNews; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -9,51 +10,65 @@ namespace CCE.Application.Tests.Content.Commands; public class CreateNewsCommandHandlerTests { + private static readonly System.Guid TopicId = System.Guid.NewGuid(); + [Fact] - public async Task Throws_DomainException_when_actor_unknown() + public async Task Returns_not_authenticated_when_actor_unknown() { - var (sut, _, _) = BuildSut(noUser: true); + var (sut, _, _) = BuildSut(TopicId, noUser: true); - var act = async () => await sut.Handle(BuildCmd(), CancellationToken.None); + var result = await sut.Handle(BuildCmd(TopicId), CancellationToken.None); - await act.Should().ThrowAsync(); + result.Success.Should().BeFalse(); } [Fact] public async Task Persists_news_when_inputs_valid() { - var (sut, service, _) = BuildSut(); + var (sut, repo, db) = BuildSut(TopicId); - await sut.Handle(BuildCmd(), CancellationToken.None); + var result = await sut.Handle(BuildCmd(TopicId), CancellationToken.None); - await service.Received(1).SaveAsync(Arg.Any(), Arg.Any()); + result.Success.Should().BeTrue(); + await repo.Received(1).AddAsync(Arg.Any(), Arg.Any()); + await db.Received(1).SaveChangesAsync(Arg.Any()); } [Fact] public async Task Returns_dto_with_correct_fields() { - var (sut, _, _) = BuildSut(); + var (sut, _, _) = BuildSut(TopicId); - var dto = await sut.Handle(BuildCmd(), CancellationToken.None); + var result = await sut.Handle(BuildCmd(TopicId), CancellationToken.None); - dto.TitleAr.Should().Be("خبر"); - dto.TitleEn.Should().Be("News"); - dto.Slug.Should().Be("first-post"); - dto.IsPublished.Should().BeFalse(); + result.Data!.TitleAr.Should().Be("خبر"); + result.Data.TitleEn.Should().Be("News"); + result.Data.TopicId.Should().Be(TopicId); + result.Data.IsPublished.Should().BeFalse(); } - private static CreateNewsCommand BuildCmd() => - new("خبر", "News", "محتوى", "Content", "first-post", null); + private static CreateNewsCommand BuildCmd(System.Guid topicId) => + new("خبر", "News", "محتوى", "Content", topicId, null); - private static (CreateNewsCommandHandler sut, INewsRepository service, ICurrentUserAccessor user) BuildSut(bool noUser = false) + private static (CreateNewsCommandHandler sut, + IRepository repo, + ICceDbContext db) BuildSut(System.Guid topicId, bool noUser = false) { - var service = Substitute.For(); + var repo = Substitute.For>(); + var db = Substitute.For(); + var topic = CCE.Domain.Community.Topic.Create( + "name-ar", "name-en", "desc-ar", "desc-en", "slug", null, null, 0); + typeof(CCE.Domain.Community.Topic).GetProperty(nameof(CCE.Domain.Community.Topic.Id))! + .SetValue(topic, topicId); + db.Topics.Returns(new[] { topic }.AsQueryable()); var user = Substitute.For(); if (noUser) user.GetUserId().Returns((System.Guid?)null); else user.GetUserId().Returns(System.Guid.NewGuid()); - var sut = new CreateNewsCommandHandler(service, user, new FakeSystemClock()); - return (sut, service, user); + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + var sut = new CreateNewsCommandHandler(repo, db, user, new FakeSystemClock(), new MessageFactory(localization)); + return (sut, repo, db); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/CreateNewsCommandValidatorTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/CreateNewsCommandValidatorTests.cs index 7398e801..4570da42 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/CreateNewsCommandValidatorTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/CreateNewsCommandValidatorTests.cs @@ -5,7 +5,7 @@ namespace CCE.Application.Tests.Content.Commands; public class CreateNewsCommandValidatorTests { private static CreateNewsCommand ValidCmd() => new( - "خبر", "News", "محتوى", "Content", "first-post", null); + "خبر", "News", "محتوى", "Content", System.Guid.NewGuid(), null); [Fact] public void Valid_command_passes() @@ -31,14 +31,14 @@ public void Empty_titles_are_rejected(string titleAr, string titleEn) } [Fact] - public void Empty_slug_is_rejected() + public void Empty_topic_id_is_rejected() { var sut = new CreateNewsCommandValidator(); - var cmd = ValidCmd() with { Slug = "" }; + var cmd = ValidCmd() with { TopicId = System.Guid.Empty }; var result = sut.Validate(cmd); result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(e => e.PropertyName == nameof(CreateNewsCommand.Slug)); + result.Errors.Should().Contain(e => e.PropertyName == nameof(CreateNewsCommand.TopicId)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/CreateResourceCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/CreateResourceCommandHandlerTests.cs index 7f18b4e7..795e197c 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/CreateResourceCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/CreateResourceCommandHandlerTests.cs @@ -1,89 +1,128 @@ using CCE.Application.Common.Interfaces; -using CCE.Application.Content; using CCE.Application.Content.Commands.CreateResource; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; +using DomainCountry = CCE.Domain.Country; using CCE.TestInfrastructure.Time; namespace CCE.Application.Tests.Content.Commands; public class CreateResourceCommandHandlerTests { + private static readonly FakeSystemClock Clock = new(); + [Fact] - public async Task Throws_KeyNotFound_when_asset_missing() + public async Task Returns_asset_not_found_when_asset_missing() { - var (sut, _, asset, _) = BuildSut(); - asset.GetByIdAsync(Arg.Any(), Arg.Any()).Returns((AssetFile?)null); + var (sut, _, _) = BuildSut(Array.Empty()); - var act = async () => await sut.Handle(BuildCmd(System.Guid.NewGuid()), CancellationToken.None); + var result = await sut.Handle(BuildCmd(System.Guid.NewGuid()), CancellationToken.None); - await act.Should().ThrowAsync(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Throws_DomainException_when_asset_not_clean() + public async Task Returns_asset_not_clean_when_asset_not_scanned() { - var (sut, _, asset, _) = BuildSut(); - var clock = new FakeSystemClock(); - var pendingAsset = AssetFile.Register("k", "x.pdf", 1, "application/pdf", System.Guid.NewGuid(), clock); - asset.GetByIdAsync(Arg.Any(), Arg.Any()).Returns(pendingAsset); + var pendingAsset = AssetFile.Register("k", "x.pdf", 1, "application/pdf", System.Guid.NewGuid(), Clock); + + var (sut, _, _) = BuildSut([pendingAsset]); - var act = async () => await sut.Handle(BuildCmd(pendingAsset.Id), CancellationToken.None); + var result = await sut.Handle(BuildCmd(pendingAsset.Id), CancellationToken.None); - await act.Should().ThrowAsync().WithMessage("*virus scan*"); + result.Success.Should().BeFalse(); } [Fact] - public async Task Throws_DomainException_when_actor_unknown() + public async Task Returns_not_authenticated_when_actor_unknown() { - var (sut, _, asset, _) = BuildSut(noUser: true); - var clock = new FakeSystemClock(); - var clean = AssetFile.Register("k", "x.pdf", 1, "application/pdf", System.Guid.NewGuid(), clock); - clean.MarkClean(clock); - asset.GetByIdAsync(Arg.Any(), Arg.Any()).Returns(clean); + var clean = AssetFile.Register("k", "x.pdf", 1, "application/pdf", System.Guid.NewGuid(), Clock); + clean.MarkClean(Clock); + + var category = ResourceCategory.Create("cat-ar", "cat-en", "cat-1", null, 1); + var country = DomainCountry.Country.Register("SAU", "SA", "السعودية", "Saudi Arabia", "MENA", "MENA", "https://flag"); + + var (sut, _, _) = BuildSut([clean], noUser: true, categoryId: category.Id, countryId: country.Id); - var act = async () => await sut.Handle(BuildCmd(clean.Id), CancellationToken.None); + var result = await sut.Handle(BuildCmd(clean.Id, category.Id, country.Id), CancellationToken.None); - await act.Should().ThrowAsync(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Persists_resource_when_inputs_valid() + public async Task Returns_dto_and_saves_when_inputs_valid() { - var (sut, service, asset, _) = BuildSut(); - var clock = new FakeSystemClock(); - var clean = AssetFile.Register("k", "x.pdf", 1, "application/pdf", System.Guid.NewGuid(), clock); - clean.MarkClean(clock); - asset.GetByIdAsync(Arg.Any(), Arg.Any()).Returns(clean); - - var dto = await sut.Handle(BuildCmd(clean.Id), CancellationToken.None); - - dto.TitleAr.Should().Be("عنوان"); - dto.TitleEn.Should().Be("Title"); - dto.AssetFileId.Should().Be(clean.Id); - dto.IsPublished.Should().BeFalse(); - await service.Received(1).SaveAsync(Arg.Any(), Arg.Any()); + var clean = AssetFile.Register("k", "x.pdf", 1, "application/pdf", System.Guid.NewGuid(), Clock); + clean.MarkClean(Clock); + + var category = ResourceCategory.Create("cat-ar", "cat-en", "cat-1", null, 1); + var country = DomainCountry.Country.Register("SAU", "SA", "السعودية", "Saudi Arabia", "MENA", "MENA", "https://flag"); + + var (sut, repo, db) = BuildSut([clean], categoryId: category.Id, countryId: country.Id); + + var result = await sut.Handle(BuildCmd(clean.Id, category.Id, country.Id), CancellationToken.None); + + result.Success.Should().BeTrue(); + result.Data.Should().NotBe(System.Guid.Empty); + await repo.Received(1).AddAsync(Arg.Any(), Arg.Any()); + await db.Received(1).SaveChangesAsync(Arg.Any()); } - private static CreateResourceCommand BuildCmd(System.Guid assetFileId) => - new( + private static CreateResourceCommand BuildCmd(System.Guid assetFileId, System.Guid? categoryId = null, System.Guid? countryId = null) + { + var catId = categoryId ?? System.Guid.NewGuid(); + var cId = countryId ?? System.Guid.NewGuid(); + return new( "عنوان", "Title", "وصف", "Description", - ResourceType.Pdf, - System.Guid.NewGuid(), + ResourceType.Paper, + catId, null, - assetFileId); + assetFileId, + new[] { cId }); + } - private static (CreateResourceCommandHandler sut, IResourceRepository service, IAssetRepository asset, ICurrentUserAccessor user) BuildSut(bool noUser = false) + private static (CreateResourceCommandHandler sut, + IRepository repo, + ICceDbContext db) BuildSut(IEnumerable assets, bool noUser = false, System.Guid? categoryId = null, System.Guid? countryId = null) { - var service = Substitute.For(); - var asset = Substitute.For(); + var repo = Substitute.For>(); + var db = Substitute.For(); + db.AssetFiles.Returns(assets.AsQueryable()); + + if (categoryId.HasValue) + { + var cat = (ResourceCategory)System.Activator.CreateInstance( + typeof(ResourceCategory), + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, + null, + new object?[] { categoryId.Value, "cat-ar", "cat-en", "cat-1", null, 1 }, + null)!; + db.ResourceCategories.Returns(new[] { cat }.AsQueryable()); + } + if (countryId.HasValue) + { + var cty = (DomainCountry.Country)System.Activator.CreateInstance( + typeof(DomainCountry.Country), + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, + null, + new object?[] { countryId.Value, "SAU", "SA", "السعودية", "Saudi Arabia", "MENA", "MENA", "https://flag" }, + null)!; + db.Countries.Returns(new[] { cty }.AsQueryable()); + } + var user = Substitute.For(); if (noUser) user.GetUserId().Returns((System.Guid?)null); else user.GetUserId().Returns(System.Guid.NewGuid()); - var sut = new CreateResourceCommandHandler(service, asset, user, new FakeSystemClock()); - return (sut, service, asset, user); + + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + + var sut = new CreateResourceCommandHandler(repo, db, user, Clock, new MessageFactory(localization)); + return (sut, repo, db); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/CreateResourceCommandValidatorTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/CreateResourceCommandValidatorTests.cs index 1d2d99a3..f9ea23f3 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/CreateResourceCommandValidatorTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/CreateResourceCommandValidatorTests.cs @@ -8,10 +8,11 @@ public class CreateResourceCommandValidatorTests private static CreateResourceCommand ValidCmd() => new( "عنوان", "Title", "وصف", "Description", - ResourceType.Pdf, + ResourceType.Paper, System.Guid.NewGuid(), null, - System.Guid.NewGuid()); + System.Guid.NewGuid(), + new[] { System.Guid.NewGuid() }); [Fact] public void Valid_command_passes() diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/DeleteEventCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/DeleteEventCommandHandlerTests.cs index 5206af1e..e262d5c2 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/DeleteEventCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/DeleteEventCommandHandlerTests.cs @@ -1,6 +1,7 @@ using CCE.Application.Common.Interfaces; -using CCE.Application.Content; using CCE.Application.Content.Commands.DeleteEvent; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -16,59 +17,83 @@ public class DeleteEventCommandHandlerTests new(2026, 9, 1, 17, 0, 0, System.TimeSpan.Zero); [Fact] - public async Task Throws_KeyNotFound_when_event_missing() + public async Task Returns_not_found_when_event_missing() { - var service = Substitute.For(); - service.FindAsync(Arg.Any(), Arg.Any()).Returns((Event?)null); - var currentUser = Substitute.For(); - var sut = new DeleteEventCommandHandler(service, currentUser, new FakeSystemClock()); + var (sut, _, _, _) = BuildSut(); - var act = async () => await sut.Handle(new DeleteEventCommand(System.Guid.NewGuid()), CancellationToken.None); + var result = await sut.Handle(new DeleteEventCommand(System.Guid.NewGuid()), CancellationToken.None); - await act.Should().ThrowAsync(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Throws_DomainException_when_actor_unknown() + public async Task Returns_not_authenticated_when_actor_unknown() { var clock = new FakeSystemClock(); var ev = Event.Schedule( "ar", "en", "desc-ar", "desc-en", - StartsOn, EndsOn, null, null, null, null, clock); + StartsOn, EndsOn, null, null, null, null, System.Guid.NewGuid(), clock); - var service = Substitute.For(); - service.FindAsync(ev.Id, Arg.Any()).Returns(ev); + var repo = Substitute.For>(); + repo.GetByIdAsync(ev.Id, Arg.Any()).Returns(ev); + var db = Substitute.For(); var currentUser = Substitute.For(); currentUser.GetUserId().Returns((System.Guid?)null); - var sut = new DeleteEventCommandHandler(service, currentUser, clock); + var sut = BuildHandler(repo, db, currentUser, clock); - var act = async () => await sut.Handle(new DeleteEventCommand(ev.Id), CancellationToken.None); + var result = await sut.Handle(new DeleteEventCommand(ev.Id), CancellationToken.None); - await act.Should().ThrowAsync(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Soft_deletes_and_calls_UpdateAsync() + public async Task Soft_deletes_and_saves_via_db_context() { var clock = new FakeSystemClock(); var actorId = System.Guid.NewGuid(); var ev = Event.Schedule( "ar", "en", "desc-ar", "desc-en", - StartsOn, EndsOn, null, null, null, null, clock); + StartsOn, EndsOn, null, null, null, null, System.Guid.NewGuid(), clock); - var service = Substitute.For(); - service.FindAsync(ev.Id, Arg.Any()).Returns(ev); + var repo = Substitute.For>(); + repo.GetByIdAsync(ev.Id, Arg.Any()).Returns(ev); + var db = Substitute.For(); var currentUser = Substitute.For(); currentUser.GetUserId().Returns(actorId); - var sut = new DeleteEventCommandHandler(service, currentUser, clock); + var sut = BuildHandler(repo, db, currentUser, clock); - await sut.Handle(new DeleteEventCommand(ev.Id), CancellationToken.None); + var result = await sut.Handle(new DeleteEventCommand(ev.Id), CancellationToken.None); + result.Success.Should().BeTrue(); ev.IsDeleted.Should().BeTrue(); - await service.Received(1).UpdateAsync(ev, Arg.Any(), Arg.Any()); + await db.Received(1).SaveChangesAsync(Arg.Any()); + } + + private static (DeleteEventCommandHandler sut, + IRepository repo, + ICceDbContext db, + ICurrentUserAccessor user) BuildSut() + { + var repo = Substitute.For>(); + repo.GetByIdAsync(Arg.Any(), Arg.Any()).Returns((Event?)null); + var db = Substitute.For(); + var user = Substitute.For(); + user.GetUserId().Returns(System.Guid.NewGuid()); + return (BuildHandler(repo, db, user, new FakeSystemClock()), repo, db, user); + } + + private static DeleteEventCommandHandler BuildHandler( + IRepository repo, + ICceDbContext db, + ICurrentUserAccessor currentUser, + ISystemClock clock) + { + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new DeleteEventCommandHandler(repo, db, currentUser, clock, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/DeleteNewsCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/DeleteNewsCommandHandlerTests.cs index be279435..1f29ae01 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/DeleteNewsCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/DeleteNewsCommandHandlerTests.cs @@ -1,6 +1,7 @@ using CCE.Application.Common.Interfaces; -using CCE.Application.Content; using CCE.Application.Content.Commands.DeleteNews; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -10,55 +11,79 @@ namespace CCE.Application.Tests.Content.Commands; public class DeleteNewsCommandHandlerTests { [Fact] - public async Task Throws_KeyNotFound_when_news_missing() + public async Task Returns_not_found_when_news_missing() { - var service = Substitute.For(); - service.FindAsync(Arg.Any(), Arg.Any()).Returns((News?)null); - var currentUser = Substitute.For(); - var sut = new DeleteNewsCommandHandler(service, currentUser, new FakeSystemClock()); - - var act = async () => await sut.Handle(new DeleteNewsCommand(System.Guid.NewGuid()), CancellationToken.None); + var (sut, _, _, _) = BuildSut(); + // repo returns null for any id + var result = await sut.Handle(new DeleteNewsCommand(System.Guid.NewGuid()), CancellationToken.None); - await act.Should().ThrowAsync(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Throws_DomainException_when_actor_unknown() + public async Task Returns_not_authenticated_when_actor_unknown() { var clock = new FakeSystemClock(); - var news = News.Draft("ar", "en", "content-ar", "content-en", "slug", System.Guid.NewGuid(), null, clock); + var news = News.Draft("ar", "en", "content-ar", "content-en", System.Guid.NewGuid(), System.Guid.NewGuid(), null, clock); - var service = Substitute.For(); - service.FindAsync(news.Id, Arg.Any()).Returns(news); + var repo = Substitute.For>(); + repo.GetByIdAsync(news.Id, Arg.Any()).Returns(news); + var db = Substitute.For(); var currentUser = Substitute.For(); currentUser.GetUserId().Returns((System.Guid?)null); - var sut = new DeleteNewsCommandHandler(service, currentUser, clock); + var sut = BuildHandler(repo, db, currentUser, clock); - var act = async () => await sut.Handle(new DeleteNewsCommand(news.Id), CancellationToken.None); + var result = await sut.Handle(new DeleteNewsCommand(news.Id), CancellationToken.None); - await act.Should().ThrowAsync(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Soft_deletes_and_calls_UpdateAsync() + public async Task Soft_deletes_and_saves_via_db_context() { var clock = new FakeSystemClock(); var actorId = System.Guid.NewGuid(); - var news = News.Draft("ar", "en", "content-ar", "content-en", "slug", System.Guid.NewGuid(), null, clock); + var news = News.Draft("ar", "en", "content-ar", "content-en", System.Guid.NewGuid(), System.Guid.NewGuid(), null, clock); - var service = Substitute.For(); - service.FindAsync(news.Id, Arg.Any()).Returns(news); + var repo = Substitute.For>(); + repo.GetByIdAsync(news.Id, Arg.Any()).Returns(news); + var db = Substitute.For(); var currentUser = Substitute.For(); currentUser.GetUserId().Returns(actorId); - var sut = new DeleteNewsCommandHandler(service, currentUser, clock); + var sut = BuildHandler(repo, db, currentUser, clock); - await sut.Handle(new DeleteNewsCommand(news.Id), CancellationToken.None); + var result = await sut.Handle(new DeleteNewsCommand(news.Id), CancellationToken.None); + result.Success.Should().BeTrue(); news.IsDeleted.Should().BeTrue(); - await service.Received(1).UpdateAsync(news, Arg.Any(), Arg.Any()); + await db.Received(1).SaveChangesAsync(Arg.Any()); + } + + private static (DeleteNewsCommandHandler sut, + IRepository repo, + ICceDbContext db, + ICurrentUserAccessor user) BuildSut() + { + var repo = Substitute.For>(); + repo.GetByIdAsync(Arg.Any(), Arg.Any()).Returns((News?)null); + var db = Substitute.For(); + var user = Substitute.For(); + user.GetUserId().Returns(System.Guid.NewGuid()); + return (BuildHandler(repo, db, user, new FakeSystemClock()), repo, db, user); + } + + private static DeleteNewsCommandHandler BuildHandler( + IRepository repo, + ICceDbContext db, + ICurrentUserAccessor currentUser, + ISystemClock clock) + { + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new DeleteNewsCommandHandler(repo, db, currentUser, clock, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/PublishNewsCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/PublishNewsCommandHandlerTests.cs index 8990293a..89764575 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/PublishNewsCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/PublishNewsCommandHandlerTests.cs @@ -1,5 +1,8 @@ -using CCE.Application.Content; +using CCE.Application.Common.Interfaces; using CCE.Application.Content.Commands.PublishNews; +using CCE.Application.Localization; +using CCE.Application.Messages; +using CCE.Domain.Common; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -8,52 +11,57 @@ namespace CCE.Application.Tests.Content.Commands; public class PublishNewsCommandHandlerTests { [Fact] - public async Task Returns_null_when_news_not_found() + public async Task Returns_not_found_when_news_missing() { - var service = Substitute.For(); - service.FindAsync(Arg.Any(), Arg.Any()).Returns((News?)null); - var sut = new PublishNewsCommandHandler(service, new FakeSystemClock()); + var (sut, _, _) = BuildSut(null); var result = await sut.Handle(new PublishNewsCommand(System.Guid.NewGuid()), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } [Fact] public async Task Publishes_and_returns_dto_when_valid() { var clock = new FakeSystemClock(); - var news = News.Draft("ar", "en", "content-ar", "content-en", "slug", System.Guid.NewGuid(), null, clock); + var news = News.Draft("ar", "en", "content-ar", "content-en", System.Guid.NewGuid(), System.Guid.NewGuid(), null, clock); - var service = Substitute.For(); - service.FindAsync(news.Id, Arg.Any()).Returns(news); + var (sut, repo, db) = BuildSut(news); - var sut = new PublishNewsCommandHandler(service, clock); + var result = await sut.Handle(new PublishNewsCommand(news.Id), CancellationToken.None); - var dto = await sut.Handle(new PublishNewsCommand(news.Id), CancellationToken.None); - - dto.Should().NotBeNull(); - dto!.IsPublished.Should().BeTrue(); - dto.PublishedOn.Should().NotBeNull(); - await service.Received(1).UpdateAsync(news, Arg.Any(), Arg.Any()); + result.Success.Should().BeTrue(); + result.Data!.IsPublished.Should().BeTrue(); + result.Data.PublishedOn.Should().NotBeNull(); + await db.Received(1).SaveChangesAsync(Arg.Any()); } [Fact] public async Task Returns_dto_unchanged_when_already_published() { var clock = new FakeSystemClock(); - var news = News.Draft("ar", "en", "content-ar", "content-en", "slug", System.Guid.NewGuid(), null, clock); - news.Publish(clock); // already published + var news = News.Draft("ar", "en", "content-ar", "content-en", System.Guid.NewGuid(), System.Guid.NewGuid(), null, clock); + news.Publish(clock); var firstPublishedOn = news.PublishedOn; - var service = Substitute.For(); - service.FindAsync(news.Id, Arg.Any()).Returns(news); + var (sut, _, _) = BuildSut(news); - var sut = new PublishNewsCommandHandler(service, clock); + var result = await sut.Handle(new PublishNewsCommand(news.Id), CancellationToken.None); - var dto = await sut.Handle(new PublishNewsCommand(news.Id), CancellationToken.None); + result.Data!.IsPublished.Should().BeTrue(); + result.Data.PublishedOn.Should().Be(firstPublishedOn); + } - dto!.IsPublished.Should().BeTrue(); - dto.PublishedOn.Should().Be(firstPublishedOn); + private static (PublishNewsCommandHandler sut, + IRepository repo, + ICceDbContext db) BuildSut(News? newsToReturn) + { + var clock = new FakeSystemClock(); + var repo = Substitute.For>(); + repo.GetByIdAsync(Arg.Any(), Arg.Any()).Returns(newsToReturn); + var db = Substitute.For(); + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return (new PublishNewsCommandHandler(repo, db, clock, new MessageFactory(localization)), repo, db); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/PublishResourceCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/PublishResourceCommandHandlerTests.cs index 732d12dc..ce513184 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/PublishResourceCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/PublishResourceCommandHandlerTests.cs @@ -1,5 +1,7 @@ -using CCE.Application.Content; +using CCE.Application.Common.Interfaces; using CCE.Application.Content.Commands.PublishResource; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -8,90 +10,88 @@ namespace CCE.Application.Tests.Content.Commands; public class PublishResourceCommandHandlerTests { + private static readonly FakeSystemClock Clock = new(); + [Fact] - public async Task Returns_null_when_resource_not_found() + public async Task Returns_not_found_when_resource_missing() { - var (sut, _, _) = BuildSut(); + var (sut, _) = BuildSut(null, Array.Empty()); + var result = await sut.Handle(new PublishResourceCommand(System.Guid.NewGuid()), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Throws_DomainException_when_asset_not_clean() + public async Task Returns_asset_not_clean_when_asset_pending() { - var clock = new FakeSystemClock(); - var (sut, resourceService, assetService) = BuildSut(); - var assetId = System.Guid.NewGuid(); - var resource = Resource.Draft("ar", "en", "desc-ar", "desc-en", ResourceType.Pdf, System.Guid.NewGuid(), null, System.Guid.NewGuid(), assetId, clock); - resourceService.FindAsync(resource.Id, Arg.Any()).Returns(resource); - var pendingAsset = AssetFile.Register("k", "x.pdf", 1, "application/pdf", System.Guid.NewGuid(), clock); - assetService.GetByIdAsync(assetId, Arg.Any()).Returns(pendingAsset); + var pendingAsset = AssetFile.Register("k", "x.pdf", 1, "application/pdf", System.Guid.NewGuid(), Clock); + var resource = Resource.Draft("ar", "en", "desc-ar", "desc-en", ResourceType.Paper, System.Guid.NewGuid(), null, System.Guid.NewGuid(), pendingAsset.Id, System.Array.Empty(), Clock); + + var (sut, _) = BuildSut(resource, [pendingAsset]); - var act = async () => await sut.Handle(new PublishResourceCommand(resource.Id), CancellationToken.None); + var result = await sut.Handle(new PublishResourceCommand(resource.Id), CancellationToken.None); - await act.Should().ThrowAsync().WithMessage("*virus scan*"); + result.Success.Should().BeFalse(); } [Fact] - public async Task Throws_DomainException_when_asset_not_found() + public async Task Returns_asset_not_found_when_asset_missing() { - var clock = new FakeSystemClock(); - var (sut, resourceService, assetService) = BuildSut(); - var resource = Resource.Draft("ar", "en", "desc-ar", "desc-en", ResourceType.Pdf, System.Guid.NewGuid(), null, System.Guid.NewGuid(), System.Guid.NewGuid(), clock); - resourceService.FindAsync(resource.Id, Arg.Any()).Returns(resource); - assetService.GetByIdAsync(Arg.Any(), Arg.Any()).Returns((AssetFile?)null); + var resource = Resource.Draft("ar", "en", "desc-ar", "desc-en", ResourceType.Paper, System.Guid.NewGuid(), null, System.Guid.NewGuid(), System.Guid.NewGuid(), System.Array.Empty(), Clock); - var act = async () => await sut.Handle(new PublishResourceCommand(resource.Id), CancellationToken.None); + var (sut, _) = BuildSut(resource, Array.Empty()); - await act.Should().ThrowAsync().WithMessage("*not found*"); + var result = await sut.Handle(new PublishResourceCommand(resource.Id), CancellationToken.None); + + result.Success.Should().BeFalse(); } [Fact] public async Task Publishes_resource_when_asset_clean() { - var clock = new FakeSystemClock(); - var (sut, resourceService, assetService) = BuildSut(); - var assetId = System.Guid.NewGuid(); - var resource = Resource.Draft("ar", "en", "desc-ar", "desc-en", ResourceType.Pdf, System.Guid.NewGuid(), null, System.Guid.NewGuid(), assetId, clock); - resourceService.FindAsync(resource.Id, Arg.Any()).Returns(resource); - var clean = AssetFile.Register("k", "x.pdf", 1, "application/pdf", System.Guid.NewGuid(), clock); - clean.MarkClean(clock); - assetService.GetByIdAsync(assetId, Arg.Any()).Returns(clean); - - var dto = await sut.Handle(new PublishResourceCommand(resource.Id), CancellationToken.None); - - dto.Should().NotBeNull(); - dto!.IsPublished.Should().BeTrue(); - dto.PublishedOn.Should().NotBeNull(); - await resourceService.Received(1).UpdateAsync(resource, Arg.Any(), Arg.Any()); + var clean = AssetFile.Register("k", "x.pdf", 1, "application/pdf", System.Guid.NewGuid(), Clock); + clean.MarkClean(Clock); + var resource = Resource.Draft("ar", "en", "desc-ar", "desc-en", ResourceType.Paper, System.Guid.NewGuid(), null, System.Guid.NewGuid(), clean.Id, System.Array.Empty(), Clock); + + var (sut, db) = BuildSut(resource, [clean]); + + var result = await sut.Handle(new PublishResourceCommand(resource.Id), CancellationToken.None); + + result.Success.Should().BeTrue(); + result.Data!.IsPublished.Should().BeTrue(); + result.Data.PublishedOn.Should().NotBeNull(); + await db.Received(1).SaveChangesAsync(Arg.Any()); } [Fact] public async Task Returns_dto_unchanged_when_already_published() { - var clock = new FakeSystemClock(); - var (sut, resourceService, assetService) = BuildSut(); - var assetId = System.Guid.NewGuid(); - var resource = Resource.Draft("ar", "en", "desc-ar", "desc-en", ResourceType.Pdf, System.Guid.NewGuid(), null, System.Guid.NewGuid(), assetId, clock); - resource.Publish(clock); // already published - resourceService.FindAsync(resource.Id, Arg.Any()).Returns(resource); - var clean = AssetFile.Register("k", "x.pdf", 1, "application/pdf", System.Guid.NewGuid(), clock); - clean.MarkClean(clock); - assetService.GetByIdAsync(assetId, Arg.Any()).Returns(clean); - + var clean = AssetFile.Register("k", "x.pdf", 1, "application/pdf", System.Guid.NewGuid(), Clock); + clean.MarkClean(Clock); + var resource = Resource.Draft("ar", "en", "desc-ar", "desc-en", ResourceType.Paper, System.Guid.NewGuid(), null, System.Guid.NewGuid(), clean.Id, System.Array.Empty(), Clock); + resource.Publish(Clock); var firstPublishedOn = resource.PublishedOn; - var dto = await sut.Handle(new PublishResourceCommand(resource.Id), CancellationToken.None); - dto!.IsPublished.Should().BeTrue(); - dto.PublishedOn.Should().Be(firstPublishedOn); + var (sut, _) = BuildSut(resource, [clean]); + + var result = await sut.Handle(new PublishResourceCommand(resource.Id), CancellationToken.None); + + result.Data!.IsPublished.Should().BeTrue(); + result.Data.PublishedOn.Should().Be(firstPublishedOn); } - private static (PublishResourceCommandHandler sut, IResourceRepository rs, IAssetRepository asset) BuildSut() + private static (PublishResourceCommandHandler sut, ICceDbContext db) BuildSut( + Resource? resourceToReturn, + IEnumerable assets) { - var rs = Substitute.For(); - var asset = Substitute.For(); - var sut = new PublishResourceCommandHandler(rs, asset, new FakeSystemClock()); - return (sut, rs, asset); + var db = Substitute.For(); + db.Resources.Returns(resourceToReturn is null ? Array.Empty().AsQueryable() : new[] { resourceToReturn }.AsQueryable()); + db.AssetFiles.Returns(assets.AsQueryable()); + + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + + return (new PublishResourceCommandHandler(db, Clock, new MessageFactory(localization)), db); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/RejectCountryResourceRequestCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/RejectCountryResourceRequestCommandHandlerTests.cs index 045e7d34..ceb66cbd 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/RejectCountryResourceRequestCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/RejectCountryResourceRequestCommandHandlerTests.cs @@ -77,7 +77,7 @@ private static CountryResourceRequest BuildPendingRequest(FakeSystemClock clock) System.Guid.NewGuid(), System.Guid.NewGuid(), "عنوان", "Title", "وصف", "Description", - ResourceType.Pdf, System.Guid.NewGuid(), clock); + ResourceType.Paper, System.Guid.NewGuid(), clock); private static ICurrentUserAccessor BuildCurrentUser(System.Guid? userId = null) { diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/RescheduleEventCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/RescheduleEventCommandHandlerTests.cs index df9a3a1a..434f0199 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/RescheduleEventCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/RescheduleEventCommandHandlerTests.cs @@ -1,5 +1,7 @@ -using CCE.Application.Content; +using CCE.Application.Common.Interfaces; using CCE.Application.Content.Commands.RescheduleEvent; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -15,64 +17,46 @@ public class RescheduleEventCommandHandlerTests new(2026, 9, 1, 17, 0, 0, System.TimeSpan.Zero); [Fact] - public async Task Returns_null_when_event_not_found() + public async Task Returns_not_found_when_event_missing() { - var service = Substitute.For(); - service.FindAsync(Arg.Any(), Arg.Any()).Returns((Event?)null); - var sut = new RescheduleEventCommandHandler(service); + var (sut, _, _) = BuildSut(null); var result = await sut.Handle( - new RescheduleEventCommand(System.Guid.NewGuid(), StartsOn, EndsOn, new byte[8]), + new RescheduleEventCommand(System.Guid.NewGuid(), StartsOn, EndsOn), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Reschedules_and_calls_UpdateAsync() + public async Task Reschedules_and_saves() { var clock = new FakeSystemClock(); var ev = Event.Schedule( "ar", "en", "desc-ar", "desc-en", - StartsOn, EndsOn, null, null, null, null, clock); + StartsOn, EndsOn, null, null, null, null, System.Guid.NewGuid(), clock); - var service = Substitute.For(); - service.FindAsync(ev.Id, Arg.Any()).Returns(ev); - - var sut = new RescheduleEventCommandHandler(service); + var (sut, db, _) = BuildSut(ev); var newStart = new System.DateTimeOffset(2026, 10, 1, 9, 0, 0, System.TimeSpan.Zero); var newEnd = new System.DateTimeOffset(2026, 10, 1, 17, 0, 0, System.TimeSpan.Zero); - var rowVersion = new byte[8] { 1, 2, 3, 4, 5, 6, 7, 8 }; var result = await sut.Handle( - new RescheduleEventCommand(ev.Id, newStart, newEnd, rowVersion), + new RescheduleEventCommand(ev.Id, newStart, newEnd), CancellationToken.None); - result.Should().NotBeNull(); - result!.StartsOn.Should().Be(newStart); - result.EndsOn.Should().Be(newEnd); - await service.Received(1).UpdateAsync(ev, rowVersion, Arg.Any()); + result.Success.Should().BeTrue(); + result.Data!.StartsOn.Should().Be(newStart); + result.Data.EndsOn.Should().Be(newEnd); + await db.Received(1).SaveChangesAsync(Arg.Any()); } - [Fact] - public async Task Propagates_ConcurrencyException_from_UpdateAsync() + private static (RescheduleEventCommandHandler sut, ICceDbContext db, IRepository repo) BuildSut(Event? evToReturn) { - var clock = new FakeSystemClock(); - var ev = Event.Schedule( - "ar", "en", "desc-ar", "desc-en", - StartsOn, EndsOn, null, null, null, null, clock); - - var service = Substitute.For(); - service.FindAsync(ev.Id, Arg.Any()).Returns(ev); - service.UpdateAsync(default!, default!, default).ReturnsForAnyArgs(_ => - throw new ConcurrencyException("conflict")); - - var sut = new RescheduleEventCommandHandler(service); - - var act = async () => await sut.Handle( - new RescheduleEventCommand(ev.Id, StartsOn, EndsOn, new byte[8]), - CancellationToken.None); - - await act.Should().ThrowAsync(); + var repo = Substitute.For>(); + repo.GetByIdAsync(Arg.Any(), Arg.Any()).Returns(evToReturn); + var db = Substitute.For(); + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return (new RescheduleEventCommandHandler(repo, db, new MessageFactory(localization)), db, repo); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/RescheduleEventCommandValidatorTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/RescheduleEventCommandValidatorTests.cs index 6ec4dbd4..18bcc517 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/RescheduleEventCommandValidatorTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/RescheduleEventCommandValidatorTests.cs @@ -11,7 +11,7 @@ public class RescheduleEventCommandValidatorTests new(2026, 9, 1, 17, 0, 0, System.TimeSpan.Zero); private static RescheduleEventCommand ValidCmd() => new( - System.Guid.NewGuid(), StartsOn, EndsOn, new byte[8]); + System.Guid.NewGuid(), StartsOn, EndsOn); [Fact] public void Valid_command_passes() diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateEventCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateEventCommandHandlerTests.cs index 9feb5f04..2e7c3113 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateEventCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateEventCommandHandlerTests.cs @@ -1,5 +1,7 @@ -using CCE.Application.Content; +using CCE.Application.Common.Interfaces; using CCE.Application.Content.Commands.UpdateEvent; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -15,67 +17,56 @@ public class UpdateEventCommandHandlerTests new(2026, 9, 1, 17, 0, 0, System.TimeSpan.Zero); [Fact] - public async Task Returns_null_when_event_not_found() + public async Task Returns_not_found_when_event_missing() { - var service = Substitute.For(); - service.FindAsync(Arg.Any(), Arg.Any()).Returns((Event?)null); - var sut = new UpdateEventCommandHandler(service); + var (sut, _, _) = BuildSut(null, System.Guid.NewGuid()); var result = await sut.Handle(BuildCommand(System.Guid.NewGuid()), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Updates_content_and_calls_UpdateAsync_with_expected_rowversion() + public async Task Updates_content_and_saves() { var clock = new FakeSystemClock(); + var topicId = System.Guid.NewGuid(); var ev = Event.Schedule( "old-ar", "old-en", "old-desc-ar", "old-desc-en", - StartsOn, EndsOn, null, null, null, null, clock); + StartsOn, EndsOn, null, null, null, null, topicId, clock); - var service = Substitute.For(); - service.FindAsync(ev.Id, Arg.Any()).Returns(ev); - - var sut = new UpdateEventCommandHandler(service); - var rowVersion = new byte[8] { 1, 2, 3, 4, 5, 6, 7, 8 }; + var (sut, db, repo) = BuildSut(ev, topicId); var cmd = new UpdateEventCommand( ev.Id, "new-ar", "new-en", "new-desc-ar", "new-desc-en", "الرياض", "Riyadh", null, null, - rowVersion); + topicId); var result = await sut.Handle(cmd, CancellationToken.None); - result.Should().NotBeNull(); - result!.TitleEn.Should().Be("new-en"); - result.DescriptionAr.Should().Be("new-desc-ar"); - await service.Received(1).UpdateAsync(ev, rowVersion, Arg.Any()); + result.Success.Should().BeTrue(); + result.Data!.TitleEn.Should().Be("new-en"); + result.Data.DescriptionAr.Should().Be("new-desc-ar"); + await db.Received(1).SaveChangesAsync(Arg.Any()); } - [Fact] - public async Task Propagates_ConcurrencyException_from_UpdateAsync() - { - var clock = new FakeSystemClock(); - var ev = Event.Schedule( - "ar", "en", "desc-ar", "desc-en", - StartsOn, EndsOn, null, null, null, null, clock); - - var service = Substitute.For(); - service.FindAsync(ev.Id, Arg.Any()).Returns(ev); - service.UpdateAsync(default!, default!, default).ReturnsForAnyArgs(_ => - throw new ConcurrencyException("conflict")); - - var sut = new UpdateEventCommandHandler(service); - var cmd = BuildCommand(ev.Id); - - var act = async () => await sut.Handle(cmd, CancellationToken.None); + private static UpdateEventCommand BuildCommand(System.Guid id) => + new(id, "ar", "en", "desc-ar", "desc-en", null, null, null, null, System.Guid.NewGuid()); - await act.Should().ThrowAsync(); + private static (UpdateEventCommandHandler sut, ICceDbContext db, IRepository repo) BuildSut(Event? evToReturn, System.Guid topicId) + { + var repo = Substitute.For>(); + repo.GetByIdAsync(Arg.Any(), Arg.Any()).Returns(evToReturn); + var db = Substitute.For(); + var topic = CCE.Domain.Community.Topic.Create( + "name-ar", "name-en", "desc-ar", "desc-en", "slug", null, null, 0); + typeof(CCE.Domain.Community.Topic).GetProperty(nameof(CCE.Domain.Community.Topic.Id))! + .SetValue(topic, topicId); + db.Topics.Returns(new[] { topic }.AsQueryable()); + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return (new UpdateEventCommandHandler(repo, db, new MessageFactory(localization)), db, repo); } - - private static UpdateEventCommand BuildCommand(System.Guid id) => - new(id, "ar", "en", "desc-ar", "desc-en", null, null, null, null, new byte[8]); } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateEventCommandValidatorTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateEventCommandValidatorTests.cs index 444aa30e..71b0bfff 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateEventCommandValidatorTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateEventCommandValidatorTests.cs @@ -9,7 +9,7 @@ public class UpdateEventCommandValidatorTests "حدث", "Event", "وصف", "Description", null, null, null, null, - new byte[8]); + System.Guid.NewGuid()); [Fact] public void Valid_command_passes() @@ -33,15 +33,4 @@ public void Empty_Id_is_rejected() result.Errors.Should().Contain(e => e.PropertyName == nameof(UpdateEventCommand.Id)); } - [Fact] - public void RowVersion_wrong_length_is_rejected() - { - var sut = new UpdateEventCommandValidator(); - var cmd = ValidCmd() with { RowVersion = new byte[4] }; - - var result = sut.Validate(cmd); - - result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(e => e.PropertyName == nameof(UpdateEventCommand.RowVersion)); - } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateNewsCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateNewsCommandHandlerTests.cs index feeda1de..a042a683 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateNewsCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateNewsCommandHandlerTests.cs @@ -1,5 +1,7 @@ -using CCE.Application.Content; +using CCE.Application.Common.Interfaces; using CCE.Application.Content.Commands.UpdateNews; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -9,63 +11,53 @@ namespace CCE.Application.Tests.Content.Commands; public class UpdateNewsCommandHandlerTests { [Fact] - public async Task Returns_null_when_news_not_found() + public async Task Returns_not_found_when_news_missing() { - var service = Substitute.For(); - service.FindAsync(Arg.Any(), Arg.Any()).Returns((News?)null); - var sut = new UpdateNewsCommandHandler(service); + var (sut, _, _) = BuildSut(null, System.Guid.NewGuid()); var result = await sut.Handle(BuildCommand(System.Guid.NewGuid()), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Updates_content_and_calls_UpdateAsync_with_expected_rowversion() + public async Task Updates_content_and_saves() { var clock = new FakeSystemClock(); + var topicId = System.Guid.NewGuid(); var news = News.Draft("old-ar", "old-en", "old-content-ar", "old-content-en", - "old-slug", System.Guid.NewGuid(), null, clock); + topicId, System.Guid.NewGuid(), null, clock); - var service = Substitute.For(); - service.FindAsync(news.Id, Arg.Any()).Returns(news); - - var sut = new UpdateNewsCommandHandler(service); - var rowVersion = new byte[8] { 1, 2, 3, 4, 5, 6, 7, 8 }; + var (sut, db, _) = BuildSut(news, topicId); var cmd = new UpdateNewsCommand( news.Id, "new-ar", "new-en", "new-content-ar", "new-content-en", - "new-slug", null, rowVersion); + topicId, null); var result = await sut.Handle(cmd, CancellationToken.None); - result.Should().NotBeNull(); - result!.TitleEn.Should().Be("new-en"); - result.Slug.Should().Be("new-slug"); - await service.Received(1).UpdateAsync(news, rowVersion, Arg.Any()); + result.Success.Should().BeTrue(); + result.Data!.TitleEn.Should().Be("new-en"); + result.Data.TopicId.Should().Be(topicId); + await db.Received(1).SaveChangesAsync(Arg.Any()); } - [Fact] - public async Task Propagates_ConcurrencyException_from_UpdateAsync() - { - var clock = new FakeSystemClock(); - var news = News.Draft("ar", "en", "content-ar", "content-en", - "my-slug", System.Guid.NewGuid(), null, clock); - - var service = Substitute.For(); - service.FindAsync(news.Id, Arg.Any()).Returns(news); - service.UpdateAsync(default!, default!, default).ReturnsForAnyArgs(_ => - throw new ConcurrencyException("conflict")); - - var sut = new UpdateNewsCommandHandler(service); - var cmd = BuildCommand(news.Id); - - var act = async () => await sut.Handle(cmd, CancellationToken.None); + private static UpdateNewsCommand BuildCommand(System.Guid id) => + new(id, "ar", "en", "content-ar", "content-en", System.Guid.NewGuid(), null); - await act.Should().ThrowAsync(); + private static (UpdateNewsCommandHandler sut, ICceDbContext db, IRepository repo) BuildSut(News? newsToReturn, System.Guid topicId) + { + var repo = Substitute.For>(); + repo.GetByIdAsync(Arg.Any(), Arg.Any()).Returns(newsToReturn); + var db = Substitute.For(); + var topic = CCE.Domain.Community.Topic.Create( + "name-ar", "name-en", "desc-ar", "desc-en", "slug", null, null, 0); + typeof(CCE.Domain.Community.Topic).GetProperty(nameof(CCE.Domain.Community.Topic.Id))! + .SetValue(topic, topicId); + db.Topics.Returns(new[] { topic }.AsQueryable()); + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return (new UpdateNewsCommandHandler(repo, db, new MessageFactory(localization)), db, repo); } - - private static UpdateNewsCommand BuildCommand(System.Guid id) => - new(id, "ar", "en", "content-ar", "content-en", "my-slug", null, new byte[8]); } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateNewsCommandValidatorTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateNewsCommandValidatorTests.cs index 0979f430..23b8f0b4 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateNewsCommandValidatorTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateNewsCommandValidatorTests.cs @@ -7,8 +7,7 @@ public class UpdateNewsCommandValidatorTests private static UpdateNewsCommand ValidCmd() => new( System.Guid.NewGuid(), "خبر", "News", "محتوى", "Content", - "first-post", null, - new byte[8]); + System.Guid.NewGuid(), null); [Fact] public void Valid_command_passes() @@ -33,26 +32,14 @@ public void Empty_Id_is_rejected() } [Fact] - public void RowVersion_wrong_length_is_rejected() + public void Empty_topic_id_is_rejected() { var sut = new UpdateNewsCommandValidator(); - var cmd = ValidCmd() with { RowVersion = new byte[4] }; + var cmd = ValidCmd() with { TopicId = System.Guid.Empty }; var result = sut.Validate(cmd); result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(e => e.PropertyName == nameof(UpdateNewsCommand.RowVersion)); - } - - [Fact] - public void Slug_not_kebab_case_is_rejected() - { - var sut = new UpdateNewsCommandValidator(); - var cmd = ValidCmd() with { Slug = "Bad Slug!" }; - - var result = sut.Validate(cmd); - - result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(e => e.PropertyName == nameof(UpdateNewsCommand.Slug)); + result.Errors.Should().Contain(e => e.PropertyName == nameof(UpdateNewsCommand.TopicId)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateResourceCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateResourceCommandHandlerTests.cs index b26d480f..7c92390b 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateResourceCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateResourceCommandHandlerTests.cs @@ -1,5 +1,7 @@ -using CCE.Application.Content; +using CCE.Application.Common.Interfaces; using CCE.Application.Content.Commands.UpdateResource; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -8,93 +10,66 @@ namespace CCE.Application.Tests.Content.Commands; public class UpdateResourceCommandHandlerTests { + private static readonly FakeSystemClock Clock = new(); + [Fact] - public async Task Returns_null_when_resource_not_found() + public async Task Returns_not_found_when_resource_missing() { - var service = Substitute.For(); - service.FindAsync(Arg.Any(), Arg.Any()).Returns((Resource?)null); - var sut = new UpdateResourceCommandHandler(service); + var (sut, _) = BuildSut(null); var result = await sut.Handle(BuildCommand(System.Guid.NewGuid()), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Updates_content_and_calls_UpdateAsync_with_expected_rowversion() + public async Task Updates_content_and_saves() { - var clock = new FakeSystemClock(); var resource = Resource.Draft( "old-ar", "old-en", "old-desc-ar", "old-desc-en", - ResourceType.Pdf, System.Guid.NewGuid(), null, - System.Guid.NewGuid(), System.Guid.NewGuid(), clock); - - var service = Substitute.For(); - service.FindAsync(resource.Id, Arg.Any()).Returns(resource); + ResourceType.Paper, System.Guid.NewGuid(), null, + System.Guid.NewGuid(), System.Guid.NewGuid(), + System.Array.Empty(), Clock); - var sut = new UpdateResourceCommandHandler(service); - var rowVersion = new byte[8] { 1, 2, 3, 4, 5, 6, 7, 8 }; + var category = ResourceCategory.Create("cat-ar", "cat-en", "cat-1", null, 1); + var (sut, db) = BuildSut(resource, categoryId: category.Id); var cmd = new UpdateResourceCommand( resource.Id, "new-ar", "new-en", "new-desc-ar", "new-desc-en", - ResourceType.Video, System.Guid.NewGuid(), - rowVersion); + ResourceType.Article, category.Id, + System.Array.Empty()); var result = await sut.Handle(cmd, CancellationToken.None); - result.Should().NotBeNull(); - result!.TitleEn.Should().Be("new-en"); - result.ResourceType.Should().Be(ResourceType.Video); - await service.Received(1).UpdateAsync(resource, rowVersion, Arg.Any()); + result.Success.Should().BeTrue(); + result.Data!.TitleEn.Should().Be("new-en"); + result.Data.ResourceType.Should().Be(ResourceType.Article); + await db.Received(1).SaveChangesAsync(Arg.Any()); } - [Fact] - public async Task Propagates_DomainException_from_UpdateContent_when_title_empty() - { - var clock = new FakeSystemClock(); - var resource = Resource.Draft( - "old-ar", "old-en", "old-desc-ar", "old-desc-en", - ResourceType.Pdf, System.Guid.NewGuid(), null, - System.Guid.NewGuid(), System.Guid.NewGuid(), clock); - - var service = Substitute.For(); - service.FindAsync(resource.Id, Arg.Any()).Returns(resource); - - var sut = new UpdateResourceCommandHandler(service); - var cmd = new UpdateResourceCommand( - resource.Id, - "", "new-en", "new-desc-ar", "new-desc-en", - ResourceType.Video, System.Guid.NewGuid(), - new byte[8]); - - var act = async () => await sut.Handle(cmd, CancellationToken.None); - - await act.Should().ThrowAsync(); - } + private static UpdateResourceCommand BuildCommand(System.Guid id) => + new(id, "ar", "en", "desc-ar", "desc-en", ResourceType.Paper, System.Guid.NewGuid(), + System.Array.Empty()); - [Fact] - public async Task Propagates_ConcurrencyException_from_UpdateAsync() + private static (UpdateResourceCommandHandler sut, ICceDbContext db) BuildSut(Resource? resourceToReturn, System.Guid? categoryId = null) { - var clock = new FakeSystemClock(); - var resource = Resource.Draft( - "ar", "en", "desc-ar", "desc-en", - ResourceType.Pdf, System.Guid.NewGuid(), null, - System.Guid.NewGuid(), System.Guid.NewGuid(), clock); - - var service = Substitute.For(); - service.FindAsync(resource.Id, Arg.Any()).Returns(resource); - service.UpdateAsync(default!, default!, default).ReturnsForAnyArgs(_ => - throw new ConcurrencyException("conflict")); - - var sut = new UpdateResourceCommandHandler(service); - var cmd = BuildCommand(resource.Id); - - var act = async () => await sut.Handle(cmd, CancellationToken.None); - - await act.Should().ThrowAsync(); + var db = Substitute.For(); + db.Resources.Returns(resourceToReturn is null ? Array.Empty().AsQueryable() : new[] { resourceToReturn }.AsQueryable()); + + if (categoryId.HasValue) + { + var cat = (ResourceCategory)System.Activator.CreateInstance( + typeof(ResourceCategory), + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, + null, + new object?[] { categoryId.Value, "cat-ar", "cat-en", "cat-1", null, 1 }, + null)!; + db.ResourceCategories.Returns(new[] { cat }.AsQueryable()); + } + + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return (new UpdateResourceCommandHandler(db, new MessageFactory(localization)), db); } - - private static UpdateResourceCommand BuildCommand(System.Guid id) => - new(id, "ar", "en", "desc-ar", "desc-en", ResourceType.Pdf, System.Guid.NewGuid(), new byte[8]); } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateResourceCommandValidatorTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateResourceCommandValidatorTests.cs index cc5f5915..4abe8781 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateResourceCommandValidatorTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateResourceCommandValidatorTests.cs @@ -9,9 +9,9 @@ public class UpdateResourceCommandValidatorTests System.Guid.NewGuid(), "عنوان", "Title", "وصف", "Description", - ResourceType.Pdf, + ResourceType.Paper, System.Guid.NewGuid(), - new byte[8]); + new[] { System.Guid.NewGuid() }); [Fact] public void Valid_command_passes() @@ -35,15 +35,4 @@ public void Empty_Id_is_rejected() result.Errors.Should().Contain(e => e.PropertyName == nameof(UpdateResourceCommand.Id)); } - [Fact] - public void RowVersion_wrong_length_is_rejected() - { - var sut = new UpdateResourceCommandValidator(); - var cmd = ValidCmd() with { RowVersion = new byte[4] }; - - var result = sut.Validate(cmd); - - result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(e => e.PropertyName == nameof(UpdateResourceCommand.RowVersion)); - } } diff --git a/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicEventByIdQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicEventByIdQueryHandlerTests.cs index 1e1d6dbb..25a67e0f 100644 --- a/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicEventByIdQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicEventByIdQueryHandlerTests.cs @@ -1,5 +1,7 @@ using CCE.Application.Common.Interfaces; using CCE.Application.Content.Public.Queries.GetPublicEventById; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -15,38 +17,38 @@ public class GetPublicEventByIdQueryHandlerTests public async Task Returns_dto_when_event_found() { var ev = Event.Schedule("حدث", "Test Event", "وصف", "Description", - BaseTime, BaseTime.AddHours(2), "الرياض", "Riyadh", null, null, Clock); + BaseTime, BaseTime.AddHours(2), "الرياض", "Riyadh", null, null, System.Guid.NewGuid(), Clock); - var db = BuildDb([ev]); - var sut = new GetPublicEventByIdQueryHandler(db); + var sut = BuildSut([ev]); var result = await sut.Handle(new GetPublicEventByIdQuery(ev.Id), CancellationToken.None); - result.Should().NotBeNull(); - result!.Id.Should().Be(ev.Id); - result.TitleEn.Should().Be("Test Event"); - result.StartsOn.Should().Be(BaseTime); - result.EndsOn.Should().Be(BaseTime.AddHours(2)); - result.LocationAr.Should().Be("الرياض"); - result.LocationEn.Should().Be("Riyadh"); - result.ICalUid.Should().NotBeNullOrEmpty(); + result.Success.Should().BeTrue(); + result.Data!.Id.Should().Be(ev.Id); + result.Data.TitleEn.Should().Be("Test Event"); + result.Data.StartsOn.Should().Be(BaseTime); + result.Data.EndsOn.Should().Be(BaseTime.AddHours(2)); + result.Data.LocationAr.Should().Be("الرياض"); + result.Data.LocationEn.Should().Be("Riyadh"); + result.Data.ICalUid.Should().NotBeNullOrEmpty(); } [Fact] - public async Task Returns_null_when_event_not_found() + public async Task Returns_not_found_when_event_missing() { - var db = BuildDb(Array.Empty()); - var sut = new GetPublicEventByIdQueryHandler(db); + var sut = BuildSut(Array.Empty()); var result = await sut.Handle(new GetPublicEventByIdQuery(System.Guid.NewGuid()), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } - private static ICceDbContext BuildDb(IEnumerable events) + private static GetPublicEventByIdQueryHandler BuildSut(IEnumerable events) { var db = Substitute.For(); db.Events.Returns(events.AsQueryable()); - return db; + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new GetPublicEventByIdQueryHandler(db, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicNewsBySlugQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicNewsBySlugQueryHandlerTests.cs index 93cd82f3..3ff134c2 100644 --- a/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicNewsBySlugQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicNewsBySlugQueryHandlerTests.cs @@ -1,5 +1,7 @@ using CCE.Application.Common.Interfaces; using CCE.Application.Content.Public.Queries.GetPublicNewsBySlug; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -12,48 +14,49 @@ public class GetPublicNewsBySlugQueryHandlerTests [Fact] public async Task Returns_dto_when_news_is_published_and_slug_matches() { - var news = News.Draft("عنوان", "Published News", "محتوى", "Content", "published-slug", System.Guid.NewGuid(), null, Clock); + var topicId = System.Guid.NewGuid(); + var authorId = System.Guid.NewGuid(); + var news = News.Draft("عنوان", "Published News", "محتوى", "Content", topicId, authorId, null, Clock); news.Publish(Clock); - var db = BuildDb([news]); - var sut = new GetPublicNewsBySlugQueryHandler(db); + var sut = BuildSut([news]); - var result = await sut.Handle(new GetPublicNewsBySlugQuery("published-slug"), CancellationToken.None); + var result = await sut.Handle(new GetPublicNewsBySlugQuery("published-news"), CancellationToken.None); - result.Should().NotBeNull(); - result!.Slug.Should().Be("published-slug"); - result.TitleEn.Should().Be("Published News"); - result.PublishedOn.Should().NotBe(default); + result.Success.Should().BeTrue(); + result.Data!.Id.Should().Be(news.Id); + result.Data.TitleEn.Should().Be("Published News"); + result.Data.PublishedOn.Should().NotBe(default); } [Fact] - public async Task Returns_null_when_slug_not_found() + public async Task Returns_not_found_when_slug_missing() { - var db = BuildDb(Array.Empty()); - var sut = new GetPublicNewsBySlugQueryHandler(db); + var sut = BuildSut(Array.Empty()); var result = await sut.Handle(new GetPublicNewsBySlugQuery("no-such-slug"), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Returns_null_when_news_found_but_not_published() + public async Task Returns_not_found_when_news_exists_but_not_published() { - var news = News.Draft("مسودة", "Draft News", "محتوى", "Content", "draft-slug", System.Guid.NewGuid(), null, Clock); + var news = News.Draft("مسودة", "Draft News", "محتوى", "Content", System.Guid.NewGuid(), System.Guid.NewGuid(), null, Clock); - var db = BuildDb([news]); - var sut = new GetPublicNewsBySlugQueryHandler(db); + var sut = BuildSut([news]); - var result = await sut.Handle(new GetPublicNewsBySlugQuery("draft-slug"), CancellationToken.None); + var result = await sut.Handle(new GetPublicNewsBySlugQuery("draft-news"), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } - private static ICceDbContext BuildDb(IEnumerable news) + private static GetPublicNewsBySlugQueryHandler BuildSut(IEnumerable news) { var db = Substitute.For(); db.News.Returns(news.AsQueryable()); - return db; + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new GetPublicNewsBySlugQueryHandler(db, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicResourceByIdQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicResourceByIdQueryHandlerTests.cs index f672d528..68b9ec40 100644 --- a/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicResourceByIdQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicResourceByIdQueryHandlerTests.cs @@ -1,5 +1,7 @@ using CCE.Application.Common.Interfaces; using CCE.Application.Content.Public.Queries.GetPublicResourceById; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -17,52 +19,51 @@ public async Task Returns_dto_when_resource_is_published() var asset = System.Guid.NewGuid(); var resource = Resource.Draft("عنوان", "Published Resource", "وصف", "Description", - ResourceType.Document, cat, null, uploader, asset, Clock); + ResourceType.ScientificPaper, cat, null, uploader, asset, System.Array.Empty(), Clock); resource.Publish(Clock); - var db = BuildDb([resource]); - var sut = new GetPublicResourceByIdQueryHandler(db); + var sut = BuildSut([resource]); var result = await sut.Handle(new GetPublicResourceByIdQuery(resource.Id), CancellationToken.None); - result.Should().NotBeNull(); - result!.Id.Should().Be(resource.Id); - result.TitleEn.Should().Be("Published Resource"); + result.Success.Should().BeTrue(); + result.Data!.Id.Should().Be(resource.Id); + result.Data.TitleEn.Should().Be("Published Resource"); } [Fact] - public async Task Returns_null_when_resource_not_found() + public async Task Returns_not_found_when_resource_missing() { - var db = BuildDb(Array.Empty()); - var sut = new GetPublicResourceByIdQueryHandler(db); + var sut = BuildSut(Array.Empty()); var result = await sut.Handle(new GetPublicResourceByIdQuery(System.Guid.NewGuid()), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } [Fact] - public async Task Returns_null_when_resource_exists_but_is_not_published() + public async Task Returns_not_found_when_resource_exists_but_is_not_published() { var cat = System.Guid.NewGuid(); var uploader = System.Guid.NewGuid(); var asset = System.Guid.NewGuid(); var resource = Resource.Draft("مسودة", "Draft Resource", "وصف", "Description", - ResourceType.Document, cat, null, uploader, asset, Clock); + ResourceType.ScientificPaper, cat, null, uploader, asset, System.Array.Empty(), Clock); - var db = BuildDb([resource]); - var sut = new GetPublicResourceByIdQueryHandler(db); + var sut = BuildSut([resource]); var result = await sut.Handle(new GetPublicResourceByIdQuery(resource.Id), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } - private static ICceDbContext BuildDb(IEnumerable resources) + private static GetPublicResourceByIdQueryHandler BuildSut(IEnumerable resources) { var db = Substitute.For(); db.Resources.Returns(resources.AsQueryable()); - return db; + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new GetPublicResourceByIdQueryHandler(db, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicEventsQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicEventsQueryHandlerTests.cs index 2bce748e..d6b08732 100644 --- a/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicEventsQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicEventsQueryHandlerTests.cs @@ -1,5 +1,7 @@ using CCE.Application.Common.Interfaces; using CCE.Application.Content.Public.Queries.ListPublicEvents; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -14,62 +16,64 @@ public class ListPublicEventsQueryHandlerTests [Fact] public async Task Returns_empty_paged_result_when_no_events_exist() { - var db = BuildDb(Array.Empty()); - var sut = new ListPublicEventsQueryHandler(db); + var sut = BuildSut(Array.Empty()); var result = await sut.Handle(new ListPublicEventsQuery(Page: 1, PageSize: 20, From: BaseTime, To: BaseTime.AddDays(30)), CancellationToken.None); - result.Items.Should().BeEmpty(); - result.Total.Should().Be(0); - result.Page.Should().Be(1); - result.PageSize.Should().Be(20); + result.Success.Should().BeTrue(); + result.Data!.Items.Should().BeEmpty(); + result.Data.Total.Should().Be(0); + result.Data.Page.Should().Be(1); + result.Data.PageSize.Should().Be(20); } [Fact] public async Task Returns_events_sorted_by_StartsOn_ascending() { + var topicId = System.Guid.NewGuid(); var earlier = Event.Schedule("أ", "Earlier Event", "وصف", "Description A", - BaseTime, BaseTime.AddHours(2), null, null, null, null, Clock); + BaseTime, BaseTime.AddHours(2), null, null, null, null, topicId, Clock); var later = Event.Schedule("ب", "Later Event", "وصف ب", "Description B", - BaseTime.AddDays(1), BaseTime.AddDays(1).AddHours(2), null, null, null, null, Clock); + BaseTime.AddDays(1), BaseTime.AddDays(1).AddHours(2), null, null, null, null, topicId, Clock); - var db = BuildDb([earlier, later]); - var sut = new ListPublicEventsQueryHandler(db); + var sut = BuildSut([earlier, later]); var result = await sut.Handle(new ListPublicEventsQuery(Page: 1, PageSize: 20, From: BaseTime.AddMinutes(-1), To: BaseTime.AddDays(2)), CancellationToken.None); - result.Total.Should().Be(2); - result.Items.Should().HaveCount(2); - result.Items[0].TitleEn.Should().Be("Earlier Event"); - result.Items[1].TitleEn.Should().Be("Later Event"); + result.Data!.Total.Should().Be(2); + result.Data.Items.Should().HaveCount(2); + result.Data.Items[0].TitleEn.Should().Be("Earlier Event"); + result.Data.Items[1].TitleEn.Should().Be("Later Event"); } [Fact] public async Task From_to_range_filter_returns_only_events_in_range() { + var topicId = System.Guid.NewGuid(); var inRange = Event.Schedule("داخل النطاق", "In Range", "وصف", "Description", - BaseTime.AddDays(5), BaseTime.AddDays(5).AddHours(1), null, null, null, null, Clock); + BaseTime.AddDays(5), BaseTime.AddDays(5).AddHours(1), null, null, null, null, topicId, Clock); var tooEarly = Event.Schedule("مبكر", "Too Early", "وصف", "Description", - BaseTime.AddDays(-1), BaseTime.AddDays(-1).AddHours(1), null, null, null, null, Clock); + BaseTime.AddDays(-1), BaseTime.AddDays(-1).AddHours(1), null, null, null, null, topicId, Clock); var tooLate = Event.Schedule("متأخر", "Too Late", "وصف", "Description", - BaseTime.AddDays(12), BaseTime.AddDays(12).AddHours(1), null, null, null, null, Clock); + BaseTime.AddDays(12), BaseTime.AddDays(12).AddHours(1), null, null, null, null, topicId, Clock); - var db = BuildDb([inRange, tooEarly, tooLate]); - var sut = new ListPublicEventsQueryHandler(db); + var sut = BuildSut([inRange, tooEarly, tooLate]); var result = await sut.Handle(new ListPublicEventsQuery(Page: 1, PageSize: 20, From: BaseTime, To: BaseTime.AddDays(10)), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().TitleEn.Should().Be("In Range"); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().TitleEn.Should().Be("In Range"); } - private static ICceDbContext BuildDb(IEnumerable events) + private static ListPublicEventsQueryHandler BuildSut(IEnumerable events) { var db = Substitute.For(); db.Events.Returns(events.AsQueryable()); - return db; + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new ListPublicEventsQueryHandler(db, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicNewsQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicNewsQueryHandlerTests.cs index 8c23d3fa..c802920e 100644 --- a/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicNewsQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicNewsQueryHandlerTests.cs @@ -1,5 +1,7 @@ using CCE.Application.Common.Interfaces; using CCE.Application.Content.Public.Queries.ListPublicNews; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -12,58 +14,60 @@ public class ListPublicNewsQueryHandlerTests [Fact] public async Task Returns_empty_paged_result_when_no_news_exist() { - var db = BuildDb(Array.Empty()); - var sut = new ListPublicNewsQueryHandler(db); + var sut = BuildSut(Array.Empty()); var result = await sut.Handle(new ListPublicNewsQuery(Page: 1, PageSize: 20), CancellationToken.None); - result.Items.Should().BeEmpty(); - result.Total.Should().Be(0); - result.Page.Should().Be(1); - result.PageSize.Should().Be(20); + result.Success.Should().BeTrue(); + result.Data!.Items.Should().BeEmpty(); + result.Data.Total.Should().Be(0); + result.Data.Page.Should().Be(1); + result.Data.PageSize.Should().Be(20); } [Fact] public async Task Only_published_news_are_returned() { - var published = News.Draft("منشور", "Published", "محتوى", "Content", "published-slug", System.Guid.NewGuid(), null, Clock); + var topicId = System.Guid.NewGuid(); + var published = News.Draft("منشور", "Published", "محتوى", "Content", topicId, System.Guid.NewGuid(), null, Clock); published.Publish(Clock); - var draft = News.Draft("مسودة", "Draft", "محتوى", "Content", "draft-slug", System.Guid.NewGuid(), null, Clock); + var draft = News.Draft("مسودة", "Draft", "محتوى", "Content", topicId, System.Guid.NewGuid(), null, Clock); - var db = BuildDb([published, draft]); - var sut = new ListPublicNewsQueryHandler(db); + var sut = BuildSut([published, draft]); var result = await sut.Handle(new ListPublicNewsQuery(Page: 1, PageSize: 20), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().TitleEn.Should().Be("Published"); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().TitleEn.Should().Be("Published"); } [Fact] public async Task IsFeatured_filter_returns_only_featured_published_news() { - var featured = News.Draft("مميز", "Featured", "محتوى", "Content", "featured-slug", System.Guid.NewGuid(), null, Clock); + var topicId = System.Guid.NewGuid(); + var featured = News.Draft("مميز", "Featured", "محتوى", "Content", topicId, System.Guid.NewGuid(), null, Clock); featured.Publish(Clock); featured.MarkFeatured(); - var notFeatured = News.Draft("عادي", "Regular", "محتوى", "Content", "regular-slug", System.Guid.NewGuid(), null, Clock); + var notFeatured = News.Draft("عادي", "Regular", "محتوى", "Content", topicId, System.Guid.NewGuid(), null, Clock); notFeatured.Publish(Clock); - var db = BuildDb([featured, notFeatured]); - var sut = new ListPublicNewsQueryHandler(db); + var sut = BuildSut([featured, notFeatured]); var result = await sut.Handle(new ListPublicNewsQuery(Page: 1, PageSize: 20, IsFeatured: true), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().TitleEn.Should().Be("Featured"); - result.Items.Single().IsFeatured.Should().BeTrue(); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().TitleEn.Should().Be("Featured"); + result.Data.Items.Single().IsFeatured.Should().BeTrue(); } - private static ICceDbContext BuildDb(IEnumerable news) + private static ListPublicNewsQueryHandler BuildSut(IEnumerable news) { var db = Substitute.For(); db.News.Returns(news.AsQueryable()); - return db; + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new ListPublicNewsQueryHandler(db, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicResourcesQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicResourcesQueryHandlerTests.cs index 327c97c1..6373933a 100644 --- a/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicResourcesQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicResourcesQueryHandlerTests.cs @@ -1,7 +1,10 @@ using CCE.Application.Common.Interfaces; using CCE.Application.Content.Public.Queries.ListPublicResources; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; +using DomainCountry = CCE.Domain.Country; namespace CCE.Application.Tests.Content.Public.Queries; @@ -12,15 +15,15 @@ public class ListPublicResourcesQueryHandlerTests [Fact] public async Task Returns_empty_paged_result_when_no_resources_exist() { - var db = BuildDb(Array.Empty()); - var sut = new ListPublicResourcesQueryHandler(db); + var sut = BuildSut(Array.Empty()); var result = await sut.Handle(new ListPublicResourcesQuery(Page: 1, PageSize: 20), CancellationToken.None); - result.Items.Should().BeEmpty(); - result.Total.Should().Be(0); - result.Page.Should().Be(1); - result.PageSize.Should().Be(20); + result.Success.Should().BeTrue(); + result.Data!.Items.Should().BeEmpty(); + result.Data.Total.Should().Be(0); + result.Data.Page.Should().Be(1); + result.Data.PageSize.Should().Be(20); } [Fact] @@ -31,19 +34,18 @@ public async Task Only_published_resources_are_returned() var asset = System.Guid.NewGuid(); var published = Resource.Draft("عنوان", "Published", "وصف", "Description", - ResourceType.Document, cat, null, uploader, asset, Clock); + ResourceType.ScientificPaper, cat, null, uploader, asset, System.Array.Empty(), Clock); published.Publish(Clock); var draft = Resource.Draft("مسودة", "Draft", "وصف", "Description", - ResourceType.Document, cat, null, uploader, asset, Clock); + ResourceType.ScientificPaper, cat, null, uploader, asset, System.Array.Empty(), Clock); - var db = BuildDb([published, draft]); - var sut = new ListPublicResourcesQueryHandler(db); + var sut = BuildSut([published, draft]); var result = await sut.Handle(new ListPublicResourcesQuery(Page: 1, PageSize: 20), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().TitleEn.Should().Be("Published"); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().TitleEn.Should().Be("Published"); } [Fact] @@ -55,21 +57,20 @@ public async Task CategoryId_filter_returns_only_matching_published_resources() var asset = System.Guid.NewGuid(); var match = Resource.Draft("فئة أ", "Category A", "وصف", "Description", - ResourceType.Document, catA, null, uploader, asset, Clock); + ResourceType.ScientificPaper, catA, null, uploader, asset, System.Array.Empty(), Clock); match.Publish(Clock); var noMatch = Resource.Draft("فئة ب", "Category B", "وصف", "Description", - ResourceType.Document, catB, null, uploader, asset, Clock); + ResourceType.ScientificPaper, catB, null, uploader, asset, System.Array.Empty(), Clock); noMatch.Publish(Clock); - var db = BuildDb([match, noMatch]); - var sut = new ListPublicResourcesQueryHandler(db); + var sut = BuildSut([match, noMatch]); var result = await sut.Handle(new ListPublicResourcesQuery(Page: 1, PageSize: 20, CategoryId: catA), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().TitleEn.Should().Be("Category A"); - result.Items.Single().CategoryId.Should().Be(catA); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().TitleEn.Should().Be("Category A"); + result.Data.Items.Single().CategoryId.Should().Be(catA); } [Fact] @@ -80,26 +81,30 @@ public async Task ResourceType_filter_returns_only_matching_published_resources( var asset = System.Guid.NewGuid(); var doc = Resource.Draft("وثيقة", "Document", "وصف", "Description", - ResourceType.Document, cat, null, uploader, asset, Clock); + ResourceType.ScientificPaper, cat, null, uploader, asset, System.Array.Empty(), Clock); doc.Publish(Clock); var video = Resource.Draft("فيديو", "Video", "وصف", "Description", - ResourceType.Video, cat, null, uploader, asset, Clock); + ResourceType.Article, cat, null, uploader, asset, System.Array.Empty(), Clock); video.Publish(Clock); - var db = BuildDb([doc, video]); - var sut = new ListPublicResourcesQueryHandler(db); + var sut = BuildSut([doc, video]); - var result = await sut.Handle(new ListPublicResourcesQuery(Page: 1, PageSize: 20, ResourceType: ResourceType.Video), CancellationToken.None); + var result = await sut.Handle(new ListPublicResourcesQuery(Page: 1, PageSize: 20, ResourceType: ResourceType.Article), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().TitleEn.Should().Be("Video"); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().TitleEn.Should().Be("Video"); } - private static ICceDbContext BuildDb(IEnumerable resources) + private static ListPublicResourcesQueryHandler BuildSut(IEnumerable resources) { var db = Substitute.For(); db.Resources.Returns(resources.AsQueryable()); - return db; + db.ResourceCategories.Returns(Array.Empty().AsQueryable()); + db.AssetFiles.Returns(Array.Empty().AsQueryable()); + db.Countries.Returns(Array.Empty().AsQueryable()); + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new ListPublicResourcesQueryHandler(db, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Queries/GetEventByIdQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Queries/GetEventByIdQueryHandlerTests.cs index 1b3d7c4e..88ec5672 100644 --- a/backend/tests/CCE.Application.Tests/Content/Queries/GetEventByIdQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Queries/GetEventByIdQueryHandlerTests.cs @@ -1,5 +1,7 @@ using CCE.Application.Common.Interfaces; using CCE.Application.Content.Queries.GetEventById; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -12,48 +14,48 @@ public class GetEventByIdQueryHandlerTests new(2026, 6, 1, 10, 0, 0, System.TimeSpan.Zero); [Fact] - public async Task Returns_null_when_event_not_found() + public async Task Returns_not_found_when_event_missing() { - var db = BuildDb(Array.Empty()); - var sut = new GetEventByIdQueryHandler(db); + var sut = BuildSut(Array.Empty()); var result = await sut.Handle(new GetEventByIdQuery(System.Guid.NewGuid()), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } [Fact] public async Task Returns_dto_with_all_fields_when_found() { + var topicId = System.Guid.NewGuid(); var ev = Event.Schedule("حدث تجريبي", "Test Event Title", "وصف عربي", "English description", BaseTime, BaseTime.AddHours(3), "الرياض", "Riyadh", - "https://example.com/meeting", "https://example.com/image.jpg", Clock); + "https://example.com/meeting", "https://example.com/image.jpg", topicId, Clock); - var db = BuildDb([ev]); - var sut = new GetEventByIdQueryHandler(db); + var sut = BuildSut([ev]); var result = await sut.Handle(new GetEventByIdQuery(ev.Id), CancellationToken.None); - result.Should().NotBeNull(); - result!.Id.Should().Be(ev.Id); - result.TitleAr.Should().Be("حدث تجريبي"); - result.TitleEn.Should().Be("Test Event Title"); - result.DescriptionAr.Should().Be("وصف عربي"); - result.DescriptionEn.Should().Be("English description"); - result.StartsOn.Should().Be(BaseTime); - result.EndsOn.Should().Be(BaseTime.AddHours(3)); - result.LocationAr.Should().Be("الرياض"); - result.LocationEn.Should().Be("Riyadh"); - result.OnlineMeetingUrl.Should().Be("https://example.com/meeting"); - result.FeaturedImageUrl.Should().Be("https://example.com/image.jpg"); - result.ICalUid.Should().EndWith("@cce.moenergy.gov.sa"); - result.RowVersion.Should().NotBeNull(); + result.Success.Should().BeTrue(); + result.Data!.Id.Should().Be(ev.Id); + result.Data.TitleAr.Should().Be("حدث تجريبي"); + result.Data.TitleEn.Should().Be("Test Event Title"); + result.Data.DescriptionAr.Should().Be("وصف عربي"); + result.Data.DescriptionEn.Should().Be("English description"); + result.Data.StartsOn.Should().Be(BaseTime); + result.Data.EndsOn.Should().Be(BaseTime.AddHours(3)); + result.Data.LocationAr.Should().Be("الرياض"); + result.Data.LocationEn.Should().Be("Riyadh"); + result.Data.OnlineMeetingUrl.Should().Be("https://example.com/meeting"); + result.Data.FeaturedImageUrl.Should().Be("https://example.com/image.jpg"); + result.Data.ICalUid.Should().EndWith("@cce.moenergy.gov.sa"); } - private static ICceDbContext BuildDb(IEnumerable events) + private static GetEventByIdQueryHandler BuildSut(IEnumerable events) { var db = Substitute.For(); db.Events.Returns(events.AsQueryable()); - return db; + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new GetEventByIdQueryHandler(db, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Queries/GetNewsByIdQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Queries/GetNewsByIdQueryHandlerTests.cs index b8db6c2f..1d8a64be 100644 --- a/backend/tests/CCE.Application.Tests/Content/Queries/GetNewsByIdQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Queries/GetNewsByIdQueryHandlerTests.cs @@ -1,5 +1,7 @@ using CCE.Application.Common.Interfaces; using CCE.Application.Content.Queries.GetNewsById; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -10,49 +12,49 @@ public class GetNewsByIdQueryHandlerTests private static readonly FakeSystemClock Clock = new(); [Fact] - public async Task Returns_null_when_news_not_found() + public async Task Returns_not_found_when_news_missing() { - var db = BuildDb(Array.Empty()); - var sut = new GetNewsByIdQueryHandler(db); + var sut = BuildSut(Array.Empty()); var result = await sut.Handle(new GetNewsByIdQuery(System.Guid.NewGuid()), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); } [Fact] public async Task Returns_dto_with_all_fields_when_found() { var authorId = System.Guid.NewGuid(); + var topicId = System.Guid.NewGuid(); var news = News.Draft("عنوان", "Test News Title", "المحتوى العربي", "English content body", - "test-news-title", authorId, "https://example.com/image.jpg", Clock); + topicId, authorId, "https://example.com/image.jpg", Clock); news.Publish(Clock); news.MarkFeatured(); - var db = BuildDb([news]); - var sut = new GetNewsByIdQueryHandler(db); + var sut = BuildSut([news]); var result = await sut.Handle(new GetNewsByIdQuery(news.Id), CancellationToken.None); - result.Should().NotBeNull(); - result!.Id.Should().Be(news.Id); - result.TitleAr.Should().Be("عنوان"); - result.TitleEn.Should().Be("Test News Title"); - result.ContentAr.Should().Be("المحتوى العربي"); - result.ContentEn.Should().Be("English content body"); - result.Slug.Should().Be("test-news-title"); - result.AuthorId.Should().Be(authorId); - result.FeaturedImageUrl.Should().Be("https://example.com/image.jpg"); - result.IsPublished.Should().BeTrue(); - result.PublishedOn.Should().NotBeNull(); - result.IsFeatured.Should().BeTrue(); - result.RowVersion.Should().NotBeNull(); + result.Success.Should().BeTrue(); + result.Data!.Id.Should().Be(news.Id); + result.Data.TitleAr.Should().Be("عنوان"); + result.Data.TitleEn.Should().Be("Test News Title"); + result.Data.ContentAr.Should().Be("المحتوى العربي"); + result.Data.ContentEn.Should().Be("English content body"); + result.Data.TopicId.Should().Be(topicId); + result.Data.AuthorId.Should().Be(authorId); + result.Data.FeaturedImageUrl.Should().Be("https://example.com/image.jpg"); + result.Data.IsPublished.Should().BeTrue(); + result.Data.PublishedOn.Should().NotBeNull(); + result.Data.IsFeatured.Should().BeTrue(); } - private static ICceDbContext BuildDb(IEnumerable news) + private static GetNewsByIdQueryHandler BuildSut(IEnumerable news) { var db = Substitute.For(); db.News.Returns(news.AsQueryable()); - return db; + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new GetNewsByIdQueryHandler(db, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Queries/ListEventsQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Queries/ListEventsQueryHandlerTests.cs index 9c22a8b6..23db547e 100644 --- a/backend/tests/CCE.Application.Tests/Content/Queries/ListEventsQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Queries/ListEventsQueryHandlerTests.cs @@ -1,5 +1,7 @@ using CCE.Application.Common.Interfaces; using CCE.Application.Content.Queries.ListEvents; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -14,74 +16,76 @@ public class ListEventsQueryHandlerTests [Fact] public async Task Returns_empty_paged_result_when_no_events_exist() { - var db = BuildDb(Array.Empty()); - var sut = new ListEventsQueryHandler(db); + var sut = BuildSut(Array.Empty()); var result = await sut.Handle(new ListEventsQuery(Page: 1, PageSize: 20), CancellationToken.None); - result.Items.Should().BeEmpty(); - result.Total.Should().Be(0); - result.Page.Should().Be(1); - result.PageSize.Should().Be(20); + result.Success.Should().BeTrue(); + result.Data!.Items.Should().BeEmpty(); + result.Data.Total.Should().Be(0); + result.Data.Page.Should().Be(1); + result.Data.PageSize.Should().Be(20); } [Fact] public async Task Returns_events_sorted_by_StartsOn_descending() { + var topicId = System.Guid.NewGuid(); var later = Event.Schedule("ب", "Later Event", "وصف ب", "Description B", - BaseTime.AddDays(1), BaseTime.AddDays(1).AddHours(2), null, null, null, null, Clock); + BaseTime.AddDays(1), BaseTime.AddDays(1).AddHours(2), null, null, null, null, topicId, Clock); var earlier = Event.Schedule("أ", "Earlier Event", "وصف", "Description A", - BaseTime, BaseTime.AddHours(2), null, null, null, null, Clock); + BaseTime, BaseTime.AddHours(2), null, null, null, null, topicId, Clock); - var db = BuildDb([later, earlier]); - var sut = new ListEventsQueryHandler(db); + var sut = BuildSut([later, earlier]); var result = await sut.Handle(new ListEventsQuery(Page: 1, PageSize: 20), CancellationToken.None); - result.Total.Should().Be(2); - result.Items.Should().HaveCount(2); - result.Items[0].TitleEn.Should().Be("Later Event"); - result.Items[1].TitleEn.Should().Be("Earlier Event"); + result.Data!.Total.Should().Be(2); + result.Data.Items.Should().HaveCount(2); + result.Data.Items[0].TitleEn.Should().Be("Later Event"); + result.Data.Items[1].TitleEn.Should().Be("Earlier Event"); } [Fact] public async Task Search_filter_matches_title_ar_or_title_en() { + var topicId = System.Guid.NewGuid(); var ev = Event.Schedule("مطابق", "matching-event", "وصف", "Description", - BaseTime, BaseTime.AddHours(1), null, null, null, null, Clock); + BaseTime, BaseTime.AddHours(1), null, null, null, null, topicId, Clock); - var db = BuildDb([ev]); - var sut = new ListEventsQueryHandler(db); + var sut = BuildSut([ev]); var result = await sut.Handle(new ListEventsQuery(Search: "matching"), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().TitleEn.Should().Be("matching-event"); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().TitleEn.Should().Be("matching-event"); } [Fact] public async Task FromDate_and_ToDate_filters_work() { + var topicId = System.Guid.NewGuid(); var inRange = Event.Schedule("في النطاق", "InRange", "وصف", "Description", - BaseTime.AddDays(5), BaseTime.AddDays(5).AddHours(1), null, null, null, null, Clock); + BaseTime.AddDays(5), BaseTime.AddDays(5).AddHours(1), null, null, null, null, topicId, Clock); var beforeRange = Event.Schedule("قبل", "Before", "وصف", "Description", - BaseTime.AddDays(-1), BaseTime.AddDays(-1).AddHours(1), null, null, null, null, Clock); + BaseTime.AddDays(-1), BaseTime.AddDays(-1).AddHours(1), null, null, null, null, topicId, Clock); var afterRange = Event.Schedule("بعد", "After", "وصف", "Description", - BaseTime.AddDays(10), BaseTime.AddDays(10).AddHours(1), null, null, null, null, Clock); + BaseTime.AddDays(10), BaseTime.AddDays(10).AddHours(1), null, null, null, null, topicId, Clock); - var db = BuildDb([inRange, beforeRange, afterRange]); - var sut = new ListEventsQueryHandler(db); + var sut = BuildSut([inRange, beforeRange, afterRange]); var result = await sut.Handle(new ListEventsQuery(FromDate: BaseTime, ToDate: BaseTime.AddDays(7)), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().TitleEn.Should().Be("InRange"); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().TitleEn.Should().Be("InRange"); } - private static ICceDbContext BuildDb(IEnumerable events) + private static ListEventsQueryHandler BuildSut(IEnumerable events) { var db = Substitute.For(); db.Events.Returns(events.AsQueryable()); - return db; + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new ListEventsQueryHandler(db, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Queries/ListNewsQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Queries/ListNewsQueryHandlerTests.cs index e0388187..37979245 100644 --- a/backend/tests/CCE.Application.Tests/Content/Queries/ListNewsQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Queries/ListNewsQueryHandlerTests.cs @@ -1,5 +1,7 @@ using CCE.Application.Common.Interfaces; using CCE.Application.Content.Queries.ListNews; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; @@ -12,83 +14,85 @@ public class ListNewsQueryHandlerTests [Fact] public async Task Returns_empty_when_no_news() { - var db = BuildDb(Array.Empty()); - var sut = new ListNewsQueryHandler(db); + var sut = BuildSut(Array.Empty()); var result = await sut.Handle(new ListNewsQuery(), CancellationToken.None); - result.Items.Should().BeEmpty(); - result.Total.Should().Be(0); - result.Page.Should().Be(1); - result.PageSize.Should().Be(20); + result.Success.Should().BeTrue(); + result.Data!.Items.Should().BeEmpty(); + result.Data.Total.Should().Be(0); + result.Data.Page.Should().Be(1); + result.Data.PageSize.Should().Be(20); } [Fact] public async Task Returns_news_sorted_by_PublishedOn_descending() { - var older = News.Draft("أ", "Older", "محتوى", "Content A", "older-article", System.Guid.NewGuid(), null, Clock); + var topicId = System.Guid.NewGuid(); + var older = News.Draft("أ", "Older", "محتوى", "Content A", topicId, System.Guid.NewGuid(), null, Clock); older.Publish(Clock); Clock.Advance(System.TimeSpan.FromSeconds(1)); - var newer = News.Draft("ب", "Newer", "محتوى ب", "Content B", "newer-article", System.Guid.NewGuid(), null, Clock); + var newer = News.Draft("ب", "Newer", "محتوى ب", "Content B", topicId, System.Guid.NewGuid(), null, Clock); newer.Publish(Clock); - var db = BuildDb([newer, older]); - var sut = new ListNewsQueryHandler(db); + var sut = BuildSut([newer, older]); var result = await sut.Handle(new ListNewsQuery(Page: 1, PageSize: 20), CancellationToken.None); - result.Total.Should().Be(2); - result.Items.Should().HaveCount(2); - result.Items[0].TitleEn.Should().Be("Newer"); - result.Items[1].TitleEn.Should().Be("Older"); + result.Data!.Total.Should().Be(2); + result.Data.Items.Should().HaveCount(2); + result.Data.Items[0].TitleEn.Should().Be("Newer"); + result.Data.Items[1].TitleEn.Should().Be("Older"); } [Fact] public async Task Search_filter_matches_title_ar_title_en_or_slug() { - var news = News.Draft("مطابق", "matching-title", "محتوى", "content", "matching-slug", System.Guid.NewGuid(), null, Clock); + var topicId = System.Guid.NewGuid(); + var news = News.Draft("مطابق", "matching-title", "محتوى", "content", topicId, System.Guid.NewGuid(), null, Clock); - var db = BuildDb([news]); - var sut = new ListNewsQueryHandler(db); + var sut = BuildSut([news]); var result = await sut.Handle(new ListNewsQuery(Search: "matching"), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().TitleEn.Should().Be("matching-title"); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().TitleEn.Should().Be("matching-title"); } [Fact] public async Task IsPublished_and_IsFeatured_filters_work() { - var published = News.Draft("منشور", "published-news", "محتوى", "content", "published-news", System.Guid.NewGuid(), null, Clock); + var topicId = System.Guid.NewGuid(); + var published = News.Draft("منشور", "published-news", "محتوى", "content", topicId, System.Guid.NewGuid(), null, Clock); published.Publish(Clock); - var featured = News.Draft("مميز", "featured-news", "محتوى", "content", "featured-news", System.Guid.NewGuid(), null, Clock); + var featured = News.Draft("مميز", "featured-news", "محتوى", "content", topicId, System.Guid.NewGuid(), null, Clock); featured.Publish(Clock); featured.MarkFeatured(); - var draft = News.Draft("مسودة", "draft-news", "محتوى", "content", "draft-news", System.Guid.NewGuid(), null, Clock); + var draft = News.Draft("مسودة", "draft-news", "محتوى", "content", topicId, System.Guid.NewGuid(), null, Clock); - var db = BuildDb([published, featured, draft]); - var sut = new ListNewsQueryHandler(db); + var sut = BuildSut([published, featured, draft]); var publishedResult = await sut.Handle(new ListNewsQuery(IsPublished: true), CancellationToken.None); - publishedResult.Total.Should().Be(2); - publishedResult.Items.Should().OnlyContain(n => n.IsPublished); + publishedResult.Data!.Total.Should().Be(2); + publishedResult.Data.Items.Should().OnlyContain(n => n.IsPublished); var featuredResult = await sut.Handle(new ListNewsQuery(IsFeatured: true), CancellationToken.None); - featuredResult.Total.Should().Be(1); - featuredResult.Items.Single().TitleEn.Should().Be("featured-news"); + featuredResult.Data!.Total.Should().Be(1); + featuredResult.Data.Items.Single().TitleEn.Should().Be("featured-news"); var draftResult = await sut.Handle(new ListNewsQuery(IsPublished: false), CancellationToken.None); - draftResult.Total.Should().Be(1); - draftResult.Items.Single().TitleEn.Should().Be("draft-news"); + draftResult.Data!.Total.Should().Be(1); + draftResult.Data.Items.Single().TitleEn.Should().Be("draft-news"); } - private static ICceDbContext BuildDb(IEnumerable news) + private static ListNewsQueryHandler BuildSut(IEnumerable news) { var db = Substitute.For(); db.News.Returns(news.AsQueryable()); - return db; + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new ListNewsQueryHandler(db, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Queries/ListResourcesQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Queries/ListResourcesQueryHandlerTests.cs index 91c9f9a9..fb00345c 100644 --- a/backend/tests/CCE.Application.Tests/Content/Queries/ListResourcesQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Queries/ListResourcesQueryHandlerTests.cs @@ -1,7 +1,10 @@ using CCE.Application.Common.Interfaces; using CCE.Application.Content.Queries.ListResources; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.Domain.Content; using CCE.TestInfrastructure.Time; +using DomainCountry = CCE.Domain.Country; namespace CCE.Application.Tests.Content.Queries; @@ -12,15 +15,15 @@ public class ListResourcesQueryHandlerTests [Fact] public async Task Returns_empty_paged_result_when_no_resources_exist() { - var db = BuildDb(Array.Empty()); - var sut = new ListResourcesQueryHandler(db); + var sut = BuildSut(Array.Empty()); var result = await sut.Handle(new ListResourcesQuery(Page: 1, PageSize: 20), CancellationToken.None); - result.Items.Should().BeEmpty(); - result.Total.Should().Be(0); - result.Page.Should().Be(1); - result.PageSize.Should().Be(20); + result.Success.Should().BeTrue(); + result.Data!.Items.Should().BeEmpty(); + result.Data.Total.Should().Be(0); + result.Data.Page.Should().Be(1); + result.Data.PageSize.Should().Be(20); } [Fact] @@ -31,22 +34,21 @@ public async Task Returns_resources_sorted_by_PublishedOn_descending() var asset = System.Guid.NewGuid(); var older = Resource.Draft("أ", "A", "وصف أ", "Desc A", - ResourceType.Pdf, cat, null, uploader, asset, Clock); + ResourceType.Paper, cat, null, uploader, asset, System.Array.Empty(), Clock); older.Publish(Clock); Clock.Advance(System.TimeSpan.FromSeconds(1)); var newer = Resource.Draft("ب", "B", "وصف ب", "Desc B", - ResourceType.Video, cat, null, uploader, asset, Clock); + ResourceType.Article, cat, null, uploader, asset, System.Array.Empty(), Clock); newer.Publish(Clock); - var db = BuildDb([newer, older]); - var sut = new ListResourcesQueryHandler(db); + var sut = BuildSut([newer, older]); var result = await sut.Handle(new ListResourcesQuery(Page: 1, PageSize: 20), CancellationToken.None); - result.Total.Should().Be(2); - result.Items.Should().HaveCount(2); - result.Items[0].TitleEn.Should().Be("B"); - result.Items[1].TitleEn.Should().Be("A"); + result.Data!.Total.Should().Be(2); + result.Data.Items.Should().HaveCount(2); + result.Data.Items[0].TitleEn.Should().Be("B"); + result.Data.Items[1].TitleEn.Should().Be("A"); } [Fact] @@ -57,15 +59,14 @@ public async Task Search_filter_matches_title_ar_title_en_description_ar_or_desc var asset = System.Guid.NewGuid(); var resource = Resource.Draft("مطابق", "matching", "وصف", "desc", - ResourceType.Pdf, cat, null, uploader, asset, Clock); + ResourceType.Paper, cat, null, uploader, asset, System.Array.Empty(), Clock); - var db = BuildDb([resource]); - var sut = new ListResourcesQueryHandler(db); + var sut = BuildSut([resource]); var result = await sut.Handle(new ListResourcesQuery(Search: "matching"), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().TitleEn.Should().Be("matching"); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().TitleEn.Should().Be("matching"); } [Fact] @@ -76,20 +77,19 @@ public async Task IsPublished_filter_returns_only_published_resources() var asset = System.Guid.NewGuid(); var published = Resource.Draft("منشور", "published", "وصف", "desc", - ResourceType.Pdf, cat, null, uploader, asset, Clock); + ResourceType.Paper, cat, null, uploader, asset, System.Array.Empty(), Clock); published.Publish(Clock); var draft = Resource.Draft("مسودة", "draft", "وصف", "desc", - ResourceType.Pdf, cat, null, uploader, asset, Clock); + ResourceType.Paper, cat, null, uploader, asset, System.Array.Empty(), Clock); - var db = BuildDb([published, draft]); - var sut = new ListResourcesQueryHandler(db); + var sut = BuildSut([published, draft]); var result = await sut.Handle(new ListResourcesQuery(IsPublished: true), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().TitleEn.Should().Be("published"); - result.Items.Single().IsPublished.Should().BeTrue(); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().TitleEn.Should().Be("published"); + result.Data.Items.Single().IsPublished.Should().BeTrue(); } [Fact] @@ -101,23 +101,27 @@ public async Task CategoryId_filter_returns_only_matching_resources() var asset = System.Guid.NewGuid(); var match = Resource.Draft("أ", "Match", "وصف", "desc", - ResourceType.Pdf, catA, null, uploader, asset, Clock); + ResourceType.Paper, catA, null, uploader, asset, System.Array.Empty(), Clock); var noMatch = Resource.Draft("ب", "NoMatch", "وصف", "desc", - ResourceType.Pdf, catB, null, uploader, asset, Clock); + ResourceType.Paper, catB, null, uploader, asset, System.Array.Empty(), Clock); - var db = BuildDb([match, noMatch]); - var sut = new ListResourcesQueryHandler(db); + var sut = BuildSut([match, noMatch]); var result = await sut.Handle(new ListResourcesQuery(CategoryId: catA), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().TitleEn.Should().Be("Match"); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().TitleEn.Should().Be("Match"); } - private static ICceDbContext BuildDb(IEnumerable resources) + private static ListResourcesQueryHandler BuildSut(IEnumerable resources) { var db = Substitute.For(); db.Resources.Returns(resources.AsQueryable()); - return db; + db.ResourceCategories.Returns(Array.Empty().AsQueryable()); + db.AssetFiles.Returns(Array.Empty().AsQueryable()); + db.Countries.Returns(Array.Empty().AsQueryable()); + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new ListResourcesQueryHandler(db, new MessageFactory(localization)); } } diff --git a/backend/tests/CCE.Domain.Tests/Content/EventTests.cs b/backend/tests/CCE.Domain.Tests/Content/EventTests.cs index f9e81e87..f701c6a5 100644 --- a/backend/tests/CCE.Domain.Tests/Content/EventTests.cs +++ b/backend/tests/CCE.Domain.Tests/Content/EventTests.cs @@ -21,6 +21,7 @@ private static Event NewEvent(FakeSystemClock clock) => locationEn: "Riyadh", onlineMeetingUrl: null, featuredImageUrl: null, + topicId: System.Guid.NewGuid(), clock: clock); [Fact] @@ -45,10 +46,11 @@ public void Schedule_raises_EventScheduledEvent() public void EndsOn_must_be_after_StartsOn() { var clock = NewClock(); + var topicId = System.Guid.NewGuid(); var act = () => Event.Schedule("ا", "x", "ا", "x", clock.UtcNow.AddDays(7), clock.UtcNow.AddDays(7), - null, null, null, null, clock); + null, null, null, null, topicId, clock); act.Should().Throw().WithMessage("*EndsOn*"); } @@ -56,10 +58,11 @@ public void EndsOn_must_be_after_StartsOn() public void EndsOn_before_StartsOn_throws() { var clock = NewClock(); + var topicId = System.Guid.NewGuid(); var act = () => Event.Schedule("ا", "x", "ا", "x", clock.UtcNow.AddDays(7), clock.UtcNow.AddDays(6), - null, null, null, null, clock); + null, null, null, null, topicId, clock); act.Should().Throw(); } @@ -67,10 +70,11 @@ public void EndsOn_before_StartsOn_throws() public void OnlineMeetingUrl_must_be_https() { var clock = NewClock(); + var topicId = System.Guid.NewGuid(); var act = () => Event.Schedule("ا", "x", "ا", "x", clock.UtcNow.AddDays(7), clock.UtcNow.AddDays(7).AddHours(2), - null, null, "http://insecure", null, clock); + null, null, "http://insecure", null, topicId, clock); act.Should().Throw().WithMessage("*https*"); } @@ -120,7 +124,8 @@ public void UpdateContent_mutates_editable_fields_when_inputs_valid() "وصف جديد", "New Description", "جدة", "Jeddah", "https://meet.example.com/room", - "https://img.example.com/banner.jpg"); + "https://img.example.com/banner.jpg", + e.TopicId); e.TitleAr.Should().Be("عنوان جديد"); e.TitleEn.Should().Be("New Title"); @@ -142,7 +147,8 @@ public void UpdateContent_throws_DomainException_when_meeting_url_not_https() "ا", "x", "ا", "x", null, null, "http://insecure.example.com", - null); + null, + e.TopicId); act.Should().Throw().WithMessage("*https*"); } diff --git a/backend/tests/CCE.Domain.Tests/Content/NewsTests.cs b/backend/tests/CCE.Domain.Tests/Content/NewsTests.cs index 5c2780e1..b8b8331a 100644 --- a/backend/tests/CCE.Domain.Tests/Content/NewsTests.cs +++ b/backend/tests/CCE.Domain.Tests/Content/NewsTests.cs @@ -15,7 +15,7 @@ private static News NewDraft(FakeSystemClock clock) => titleEn: "News", contentAr: "محتوى", contentEn: "Content", - slug: "first-post", + topicId: System.Guid.NewGuid(), authorId: System.Guid.NewGuid(), featuredImageUrl: null, clock: clock); @@ -30,19 +30,11 @@ public void Draft_creates_unpublished_news() n.IsFeatured.Should().BeFalse(); } - [Fact] - public void Slug_must_be_kebab_case() - { - var clock = NewClock(); - var act = () => News.Draft("ا", "x", "ا", "x", "Bad Slug", System.Guid.NewGuid(), null, clock); - act.Should().Throw().WithMessage("*slug*"); - } - [Fact] public void FeaturedImageUrl_must_be_https() { var clock = NewClock(); - var act = () => News.Draft("ا", "x", "ا", "x", "x", System.Guid.NewGuid(), "http://insecure", clock); + var act = () => News.Draft("ا", "x", "ا", "x", System.Guid.NewGuid(), System.Guid.NewGuid(), "http://insecure", clock); act.Should().Throw().WithMessage("*https*"); } @@ -98,25 +90,15 @@ public void UpdateContent_mutates_editable_fields_when_inputs_valid() titleEn: "New News", contentAr: "محتوى جديد", contentEn: "New Content", - slug: "new-slug", + topicId: n.TopicId, featuredImageUrl: "https://example.com/image.jpg"); n.TitleAr.Should().Be("خبر جديد"); n.TitleEn.Should().Be("New News"); n.ContentAr.Should().Be("محتوى جديد"); n.ContentEn.Should().Be("New Content"); - n.Slug.Should().Be("new-slug"); n.FeaturedImageUrl.Should().Be("https://example.com/image.jpg"); } - [Fact] - public void UpdateContent_throws_DomainException_when_slug_not_kebab_case() - { - var clock = NewClock(); - var n = NewDraft(clock); - var act = () => n.UpdateContent("خبر", "News", "محتوى", "Content", "Bad Slug!", null); - - act.Should().Throw().WithMessage("*slug*"); - } } diff --git a/backend/tests/CCE.Domain.Tests/Content/ResourceTests.cs b/backend/tests/CCE.Domain.Tests/Content/ResourceTests.cs index 5d9de191..5e1a1f63 100644 --- a/backend/tests/CCE.Domain.Tests/Content/ResourceTests.cs +++ b/backend/tests/CCE.Domain.Tests/Content/ResourceTests.cs @@ -9,18 +9,22 @@ public class ResourceTests { private static FakeSystemClock NewClock() => new(); - private static Resource NewDraft(FakeSystemClock clock, System.Guid? countryId = null) => - Resource.Draft( + private static Resource NewDraft(FakeSystemClock clock, System.Guid? countryId = null) + { + var countryIds = countryId.HasValue ? new[] { countryId.Value } : System.Array.Empty(); + return Resource.Draft( titleAr: "مورد", titleEn: "Resource", descriptionAr: "وصف", descriptionEn: "Description", - resourceType: ResourceType.Pdf, + resourceType: ResourceType.Paper, categoryId: System.Guid.NewGuid(), countryId: countryId, uploadedById: System.Guid.NewGuid(), assetFileId: System.Guid.NewGuid(), + countryIds: countryIds, clock: clock); + } [Fact] public void Draft_factory_creates_unpublished_resource() @@ -57,8 +61,9 @@ public void Draft_with_country_marks_country_managed() public void Draft_with_empty_titleAr_throws() { var clock = NewClock(); - var act = () => Resource.Draft("", "x", "x", "x", ResourceType.Pdf, - System.Guid.NewGuid(), null, System.Guid.NewGuid(), System.Guid.NewGuid(), clock); + var act = () => Resource.Draft("", "x", "x", "x", ResourceType.Paper, + System.Guid.NewGuid(), null, System.Guid.NewGuid(), System.Guid.NewGuid(), + System.Array.Empty(), clock); act.Should().Throw().WithMessage("*TitleAr*"); } @@ -122,13 +127,13 @@ public void UpdateContent_mutates_editable_fields_when_inputs_valid() var r = NewDraft(clock); var newCategoryId = System.Guid.NewGuid(); - r.UpdateContent("new-ar", "new-en", "new-desc-ar", "new-desc-en", ResourceType.Video, newCategoryId); + r.UpdateContent("new-ar", "new-en", "new-desc-ar", "new-desc-en", ResourceType.Article, newCategoryId, System.Array.Empty()); r.TitleAr.Should().Be("new-ar"); r.TitleEn.Should().Be("new-en"); r.DescriptionAr.Should().Be("new-desc-ar"); r.DescriptionEn.Should().Be("new-desc-en"); - r.ResourceType.Should().Be(ResourceType.Video); + r.ResourceType.Should().Be(ResourceType.Article); r.CategoryId.Should().Be(newCategoryId); } @@ -138,7 +143,7 @@ public void UpdateContent_throws_DomainException_when_titleAr_empty() var clock = NewClock(); var r = NewDraft(clock); - var act = () => r.UpdateContent("", "en", "desc-ar", "desc-en", ResourceType.Pdf, System.Guid.NewGuid()); + var act = () => r.UpdateContent("", "en", "desc-ar", "desc-en", ResourceType.Paper, System.Guid.NewGuid(), System.Array.Empty()); act.Should().Throw().WithMessage("*TitleAr*"); } diff --git a/backend/tests/CCE.Domain.Tests/Content/RowVersionContractTests.cs b/backend/tests/CCE.Domain.Tests/Content/RowVersionContractTests.cs index 2b90a411..b2462c9d 100644 --- a/backend/tests/CCE.Domain.Tests/Content/RowVersionContractTests.cs +++ b/backend/tests/CCE.Domain.Tests/Content/RowVersionContractTests.cs @@ -26,8 +26,9 @@ public void Aggregate_root_exposes_byte_array_RowVersion(System.Type type) public void Resource_RowVersion_initialised_to_empty_array() { var clock = new FakeSystemClock(); - var r = Resource.Draft("ا", "x", "ا", "x", ResourceType.Pdf, - System.Guid.NewGuid(), null, System.Guid.NewGuid(), System.Guid.NewGuid(), clock); + var r = Resource.Draft("ا", "x", "ا", "x", ResourceType.Paper, + System.Guid.NewGuid(), null, System.Guid.NewGuid(), System.Guid.NewGuid(), + System.Array.Empty(), clock); r.RowVersion.Should().NotBeNull(); r.RowVersion.Should().BeEmpty(); } diff --git a/backend/tests/CCE.Domain.Tests/Country/CountryResourceRequestTests.cs b/backend/tests/CCE.Domain.Tests/Country/CountryResourceRequestTests.cs index a97d9aa2..25e9f8ea 100644 --- a/backend/tests/CCE.Domain.Tests/Country/CountryResourceRequestTests.cs +++ b/backend/tests/CCE.Domain.Tests/Country/CountryResourceRequestTests.cs @@ -16,7 +16,7 @@ private static CountryResourceRequest NewPending(FakeSystemClock clock) => requestedById: System.Guid.NewGuid(), titleAr: "عنوان", titleEn: "Title", descriptionAr: "وصف", descriptionEn: "Description", - resourceType: ResourceType.Pdf, + resourceType: ResourceType.Paper, assetFileId: System.Guid.NewGuid(), clock: clock); diff --git a/backend/tests/CCE.Infrastructure.Tests/Search/MeilisearchIndexerHandlerTests.cs b/backend/tests/CCE.Infrastructure.Tests/Search/MeilisearchIndexerHandlerTests.cs index 44ba4409..4ac47b11 100644 --- a/backend/tests/CCE.Infrastructure.Tests/Search/MeilisearchIndexerHandlerTests.cs +++ b/backend/tests/CCE.Infrastructure.Tests/Search/MeilisearchIndexerHandlerTests.cs @@ -38,7 +38,7 @@ public async Task News_published_handler_upserts_document() "News Title", "محتوى عربي", "English content", - "news-title", + System.Guid.NewGuid(), System.Guid.NewGuid(), null, clock); @@ -76,11 +76,12 @@ public async Task Resource_published_handler_upserts_document() "Resource Title", "وصف عربي", "English description", - ResourceType.Document, + ResourceType.ScientificPaper, categoryId: System.Guid.NewGuid(), countryId: null, uploadedById: System.Guid.NewGuid(), assetFileId: System.Guid.NewGuid(), + countryIds: System.Array.Empty(), clock); resource.Publish(clock); @@ -122,6 +123,7 @@ public async Task Event_scheduled_handler_upserts_document() locationEn: null, onlineMeetingUrl: null, featuredImageUrl: null, + System.Guid.NewGuid(), clock); await using var db = BuildDb(); diff --git a/backend/tests/CCE.Infrastructure.Tests/Seeder/DemoDataSeederTests.cs b/backend/tests/CCE.Infrastructure.Tests/Seeder/DemoDataSeederTests.cs index 61921eac..99b045ff 100644 --- a/backend/tests/CCE.Infrastructure.Tests/Seeder/DemoDataSeederTests.cs +++ b/backend/tests/CCE.Infrastructure.Tests/Seeder/DemoDataSeederTests.cs @@ -8,19 +8,25 @@ namespace CCE.Infrastructure.Tests.Seeder; public class DemoDataSeederTests { - private static (CceDbContext Ctx, DemoDataSeeder Seeder) Build() + private static async Task<(CceDbContext Ctx, DemoDataSeeder Seeder)> BuildAsync() { var ctx = new CceDbContext(new DbContextOptionsBuilder() .UseInMemoryDatabase(System.Guid.NewGuid().ToString()) .Options); - return (ctx, new DemoDataSeeder(ctx, new FakeSystemClock(), + var clock = new FakeSystemClock(); + // Seed reference data (countries, topics, categories, etc.) first, since + // DemoDataSeeder depends on topics for News/Event TopicId associations. + var referenceSeeder = new ReferenceDataSeeder(ctx, clock, + NullLogger.Instance); + await referenceSeeder.SeedAsync(default); + return (ctx, new DemoDataSeeder(ctx, clock, NullLogger.Instance)); } [Fact] public async Task Seeds_news_and_event() { - var (ctx, seeder) = Build(); + var (ctx, seeder) = await BuildAsync(); using (ctx) { await seeder.SeedAsync(); @@ -32,7 +38,7 @@ public async Task Seeds_news_and_event() [Fact] public async Task News_articles_are_published() { - var (ctx, seeder) = Build(); + var (ctx, seeder) = await BuildAsync(); using (ctx) { await seeder.SeedAsync(); @@ -44,13 +50,13 @@ public async Task News_articles_are_published() [Fact] public async Task Idempotent() { - var (ctx, seeder) = Build(); + var (ctx, seeder) = await BuildAsync(); using (ctx) { await seeder.SeedAsync(); var firstNews = await ctx.News.CountAsync(); await seeder.SeedAsync(); - var secondNews = await ctx.News.CountAsync(); + var secondNews = ctx.News.Count(); secondNews.Should().Be(firstNews); } }