From 20a7aa84c0c26bc18215302803b318ca2361a454 Mon Sep 17 00:00:00 2001 From: ahmed Date: Mon, 22 Jun 2026 14:56:34 +0300 Subject: [PATCH 01/10] feat: Report User Registeration --- .../Endpoints/ReportEndpoints.cs | 11 +++++ .../Reports/Dtos/UserRegistrationReportDto.cs | 9 +++++ .../Dtos/UserRegistrationReportUserDto.cs | 11 +++++ .../GetUserRegistrationReportQuery.cs | 8 ++++ .../GetUserRegistrationReportQueryHandler.cs | 40 +++++++++++++++++++ 5 files changed, 79 insertions(+) create mode 100644 backend/src/CCE.Application/Reports/Dtos/UserRegistrationReportDto.cs create mode 100644 backend/src/CCE.Application/Reports/Dtos/UserRegistrationReportUserDto.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQuery.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQueryHandler.cs diff --git a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs index 33bc2363..31e826a1 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs @@ -1,5 +1,8 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Reports; +using CCE.Application.Reports.Queries.GetUserRegistrationReport; using CCE.Domain; +using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -148,6 +151,14 @@ public static IEndpointRouteBuilder MapReportEndpoints(this IEndpointRouteBuilde .RequireAuthorization(Permissions.Report_CountryProfiles) .WithName("CountryProfilesReport"); + reports.MapGet("/user-registration", async (ISender sender) => + { + var result = await sender.Send(new GetUserRegistrationReportQuery()); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.Report_UserRegistrations) + .WithName("UserRegistrationReport"); + return app; } } diff --git a/backend/src/CCE.Application/Reports/Dtos/UserRegistrationReportDto.cs b/backend/src/CCE.Application/Reports/Dtos/UserRegistrationReportDto.cs new file mode 100644 index 00000000..7805d12d --- /dev/null +++ b/backend/src/CCE.Application/Reports/Dtos/UserRegistrationReportDto.cs @@ -0,0 +1,9 @@ +namespace CCE.Application.Reports.Dtos; + +public sealed record UserRegistrationReportDto( + string ReportId, + string ReportTitle, + DateTimeOffset GeneratedAt, + int TotalUsers, + IReadOnlyList Users +); diff --git a/backend/src/CCE.Application/Reports/Dtos/UserRegistrationReportUserDto.cs b/backend/src/CCE.Application/Reports/Dtos/UserRegistrationReportUserDto.cs new file mode 100644 index 00000000..e9512c2e --- /dev/null +++ b/backend/src/CCE.Application/Reports/Dtos/UserRegistrationReportUserDto.cs @@ -0,0 +1,11 @@ +namespace CCE.Application.Reports.Dtos; + +public sealed record UserRegistrationReportUserDto( + Guid Id, + string FirstName, + string LastName, + string? Email, + string JobTitle, + string OrganizationName, + string? PhoneNumber +); diff --git a/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQuery.cs b/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQuery.cs new file mode 100644 index 00000000..2bce26b6 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQuery.cs @@ -0,0 +1,8 @@ +using CCE.Application.Common; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetUserRegistrationReport; + +public sealed record GetUserRegistrationReportQuery() + : IRequest>; diff --git a/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQueryHandler.cs new file mode 100644 index 00000000..2eb0e6d9 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQueryHandler.cs @@ -0,0 +1,40 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; +using CCE.Application.Messages; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetUserRegistrationReport; + +internal sealed class GetUserRegistrationReportQueryHandler( + ICceDbContext _db, + MessageFactory _msg) + : IRequestHandler> +{ + public async Task> Handle( + GetUserRegistrationReportQuery q, CancellationToken ct) + { + var users = await _db.Users + .Where(u => !u.IsDeleted) + .Select(u => new UserRegistrationReportUserDto( + u.Id, + u.FirstName, + u.LastName, + u.Email, + u.JobTitle, + u.OrganizationName, + u.PhoneNumber)) + .ToListAsyncEither(ct) + .ConfigureAwait(false); + + var report = new UserRegistrationReportDto( + ReportId: "RP001", + ReportTitle: "تقرير تسجيل المستخدمين", + GeneratedAt: DateTimeOffset.UtcNow, + TotalUsers: users.Count, + Users: users); + + return _msg.Ok(report, "ITEMS_LISTED"); + } +} From 63dc1581cc3f7c8480b7413899a81cc7add0d051 Mon Sep 17 00:00:00 2001 From: ahmed Date: Mon, 22 Jun 2026 16:55:14 +0300 Subject: [PATCH 02/10] feat: Report User satisfaction-survey --- .../Endpoints/ReportEndpoints.cs | 9 +++++ .../Dtos/SatisfactionSurveyReportDto.cs | 11 +++++++ .../Reports/Dtos/UserRegistrationReportDto.cs | 9 ----- .../GetSatisfactionSurveyReportQuery.cs | 8 +++++ ...GetSatisfactionSurveyReportQueryHandler.cs | 33 +++++++++++++++++++ .../GetUserRegistrationReportQuery.cs | 2 +- .../GetUserRegistrationReportQueryHandler.cs | 13 ++------ 7 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 backend/src/CCE.Application/Reports/Dtos/SatisfactionSurveyReportDto.cs delete mode 100644 backend/src/CCE.Application/Reports/Dtos/UserRegistrationReportDto.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetSatisfactionSurveyReport/GetSatisfactionSurveyReportQuery.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetSatisfactionSurveyReport/GetSatisfactionSurveyReportQueryHandler.cs diff --git a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs index 31e826a1..378eb540 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs @@ -1,5 +1,6 @@ using CCE.Api.Common.Extensions; using CCE.Application.Reports; +using CCE.Application.Reports.Queries.GetSatisfactionSurveyReport; using CCE.Application.Reports.Queries.GetUserRegistrationReport; using CCE.Domain; using MediatR; @@ -159,6 +160,14 @@ public static IEndpointRouteBuilder MapReportEndpoints(this IEndpointRouteBuilde .RequireAuthorization(Permissions.Report_UserRegistrations) .WithName("UserRegistrationReport"); + reports.MapGet("/satisfaction-survey", async (ISender sender) => + { + var result = await sender.Send(new GetSatisfactionSurveyReportQuery()); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.Report_SatisfactionSurvey) + .WithName("SatisfactionSurveyReportJson"); + return app; } } diff --git a/backend/src/CCE.Application/Reports/Dtos/SatisfactionSurveyReportDto.cs b/backend/src/CCE.Application/Reports/Dtos/SatisfactionSurveyReportDto.cs new file mode 100644 index 00000000..2e6e62ef --- /dev/null +++ b/backend/src/CCE.Application/Reports/Dtos/SatisfactionSurveyReportDto.cs @@ -0,0 +1,11 @@ +namespace CCE.Application.Reports.Dtos; + +public sealed record SatisfactionSurveyReportDto( + Guid Id, + int OverallSatisfaction, + int EaseOfUse, + int ContentSuitability, + string Feedback, + Guid? UserId, + DateTimeOffset SubmittedAt +); diff --git a/backend/src/CCE.Application/Reports/Dtos/UserRegistrationReportDto.cs b/backend/src/CCE.Application/Reports/Dtos/UserRegistrationReportDto.cs deleted file mode 100644 index 7805d12d..00000000 --- a/backend/src/CCE.Application/Reports/Dtos/UserRegistrationReportDto.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CCE.Application.Reports.Dtos; - -public sealed record UserRegistrationReportDto( - string ReportId, - string ReportTitle, - DateTimeOffset GeneratedAt, - int TotalUsers, - IReadOnlyList Users -); diff --git a/backend/src/CCE.Application/Reports/Queries/GetSatisfactionSurveyReport/GetSatisfactionSurveyReportQuery.cs b/backend/src/CCE.Application/Reports/Queries/GetSatisfactionSurveyReport/GetSatisfactionSurveyReportQuery.cs new file mode 100644 index 00000000..54616fe4 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetSatisfactionSurveyReport/GetSatisfactionSurveyReportQuery.cs @@ -0,0 +1,8 @@ +using CCE.Application.Common; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetSatisfactionSurveyReport; + +public sealed record GetSatisfactionSurveyReportQuery() + : IRequest>>; diff --git a/backend/src/CCE.Application/Reports/Queries/GetSatisfactionSurveyReport/GetSatisfactionSurveyReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetSatisfactionSurveyReport/GetSatisfactionSurveyReportQueryHandler.cs new file mode 100644 index 00000000..7197a8b7 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetSatisfactionSurveyReport/GetSatisfactionSurveyReportQueryHandler.cs @@ -0,0 +1,33 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; +using CCE.Application.Messages; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetSatisfactionSurveyReport; + +internal sealed class GetSatisfactionSurveyReportQueryHandler( + ICceDbContext _db, + MessageFactory _msg) + : IRequestHandler>> +{ + public async Task>> Handle( + GetSatisfactionSurveyReportQuery q, CancellationToken ct) + { + var items = await _db.ServiceEvaluations + .OrderByDescending(e => e.CreatedOn) + .Select(e => new SatisfactionSurveyReportDto( + e.Id, + (int)e.OverallSatisfaction, + (int)e.EaseOfUse, + (int)e.ContentSuitability, + e.Feedback, + e.UserId, + e.CreatedOn)) + .ToListAsyncEither(ct) + .ConfigureAwait(false); + + return _msg.Ok(items, "ITEMS_LISTED"); + } +} diff --git a/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQuery.cs b/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQuery.cs index 2bce26b6..850be57a 100644 --- a/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQuery.cs +++ b/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQuery.cs @@ -5,4 +5,4 @@ namespace CCE.Application.Reports.Queries.GetUserRegistrationReport; public sealed record GetUserRegistrationReportQuery() - : IRequest>; + : IRequest>>; diff --git a/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQueryHandler.cs index 2eb0e6d9..0953e6ff 100644 --- a/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQueryHandler.cs +++ b/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQueryHandler.cs @@ -10,9 +10,9 @@ namespace CCE.Application.Reports.Queries.GetUserRegistrationReport; internal sealed class GetUserRegistrationReportQueryHandler( ICceDbContext _db, MessageFactory _msg) - : IRequestHandler> + : IRequestHandler>> { - public async Task> Handle( + public async Task>> Handle( GetUserRegistrationReportQuery q, CancellationToken ct) { var users = await _db.Users @@ -28,13 +28,6 @@ public async Task> Handle( .ToListAsyncEither(ct) .ConfigureAwait(false); - var report = new UserRegistrationReportDto( - ReportId: "RP001", - ReportTitle: "تقرير تسجيل المستخدمين", - GeneratedAt: DateTimeOffset.UtcNow, - TotalUsers: users.Count, - Users: users); - - return _msg.Ok(report, "ITEMS_LISTED"); + return _msg.Ok(users, "ITEMS_LISTED"); } } From 0fd7c8f1a86fd04728756aeddb69984abf08f38f Mon Sep 17 00:00:00 2001 From: ahmed Date: Mon, 22 Jun 2026 17:12:04 +0300 Subject: [PATCH 03/10] feat: Report user-preferences --- backend/permissions.yaml | 3 ++ .../Endpoints/ReportEndpoints.cs | 9 +++++ .../Reports/Dtos/UserPreferenceReportDto.cs | 9 +++++ .../GetUserPreferenceReportQuery.cs | 8 +++++ .../GetUserPreferenceReportQueryHandler.cs | 33 +++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 backend/src/CCE.Application/Reports/Dtos/UserPreferenceReportDto.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetUserPreferenceReport/GetUserPreferenceReportQuery.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetUserPreferenceReport/GetUserPreferenceReportQueryHandler.cs diff --git a/backend/permissions.yaml b/backend/permissions.yaml index b5879551..bfa16678 100644 --- a/backend/permissions.yaml +++ b/backend/permissions.yaml @@ -229,6 +229,9 @@ groups: CountryProfiles: description: Generate country profiles report roles: [cce-super-admin, cce-admin] + UserPreferences: + description: Generate user-preference report + roles: [cce-super-admin, cce-admin] InteractiveMap: Manage: description: Create/update/delete interactive maps diff --git a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs index 378eb540..8266f58b 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs @@ -1,6 +1,7 @@ using CCE.Api.Common.Extensions; using CCE.Application.Reports; using CCE.Application.Reports.Queries.GetSatisfactionSurveyReport; +using CCE.Application.Reports.Queries.GetUserPreferenceReport; using CCE.Application.Reports.Queries.GetUserRegistrationReport; using CCE.Domain; using MediatR; @@ -168,6 +169,14 @@ public static IEndpointRouteBuilder MapReportEndpoints(this IEndpointRouteBuilde .RequireAuthorization(Permissions.Report_SatisfactionSurvey) .WithName("SatisfactionSurveyReportJson"); + reports.MapGet("/user-preferences", async (ISender sender) => + { + var result = await sender.Send(new GetUserPreferenceReportQuery()); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.Report_UserPreferences) + .WithName("UserPreferenceReport"); + return app; } } diff --git a/backend/src/CCE.Application/Reports/Dtos/UserPreferenceReportDto.cs b/backend/src/CCE.Application/Reports/Dtos/UserPreferenceReportDto.cs new file mode 100644 index 00000000..57d029e3 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Dtos/UserPreferenceReportDto.cs @@ -0,0 +1,9 @@ +namespace CCE.Application.Reports.Dtos; + +public sealed record UserPreferenceReportDto( + Guid Id, + List AreasOfInterest, + int KnowledgeLevel, + string SectorOfWork, + Guid? CountryId +); diff --git a/backend/src/CCE.Application/Reports/Queries/GetUserPreferenceReport/GetUserPreferenceReportQuery.cs b/backend/src/CCE.Application/Reports/Queries/GetUserPreferenceReport/GetUserPreferenceReportQuery.cs new file mode 100644 index 00000000..0608cae5 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetUserPreferenceReport/GetUserPreferenceReportQuery.cs @@ -0,0 +1,8 @@ +using CCE.Application.Common; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetUserPreferenceReport; + +public sealed record GetUserPreferenceReportQuery() + : IRequest>>; diff --git a/backend/src/CCE.Application/Reports/Queries/GetUserPreferenceReport/GetUserPreferenceReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetUserPreferenceReport/GetUserPreferenceReportQueryHandler.cs new file mode 100644 index 00000000..c9fc7130 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetUserPreferenceReport/GetUserPreferenceReportQueryHandler.cs @@ -0,0 +1,33 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; +using CCE.Application.Messages; +using CCE.Application.Reports.Dtos; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace CCE.Application.Reports.Queries.GetUserPreferenceReport; + +internal sealed class GetUserPreferenceReportQueryHandler( + ICceDbContext _db, + MessageFactory _msg) + : IRequestHandler>> +{ + public async Task>> Handle( + GetUserPreferenceReportQuery q, CancellationToken ct) + { + var items = await _db.Users + .Where(u => !u.IsDeleted) + .OrderBy(u => u.Id) + .Select(u => new UserPreferenceReportDto( + u.Id, + u.UserInterestTopics.Select(uit => uit.InterestTopicId).ToList(), + (int)u.KnowledgeLevel, + u.JobTitle, + u.CountryId)) + .ToListAsyncEither(ct) + .ConfigureAwait(false); + + return _msg.Ok(items, "ITEMS_LISTED"); + } +} From 2d0d41350705d040303eed7046f71b2d56f1e699 Mon Sep 17 00:00:00 2001 From: ahmed Date: Mon, 22 Jun 2026 21:28:33 +0300 Subject: [PATCH 04/10] feat: Report experts --- backend/permissions.yaml | 3 + .../Endpoints/ReportEndpoints.cs | 9 ++ .../Reports/Dtos/ExpertReportDto.cs | 18 ++++ .../GetExpertReport/GetExpertReportQuery.cs | 8 ++ .../GetExpertReportQueryHandler.cs | 85 +++++++++++++++++++ 5 files changed, 123 insertions(+) create mode 100644 backend/src/CCE.Application/Reports/Dtos/ExpertReportDto.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetExpertReport/GetExpertReportQuery.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetExpertReport/GetExpertReportQueryHandler.cs diff --git a/backend/permissions.yaml b/backend/permissions.yaml index bfa16678..3e50a2b2 100644 --- a/backend/permissions.yaml +++ b/backend/permissions.yaml @@ -232,6 +232,9 @@ groups: UserPreferences: description: Generate user-preference report roles: [cce-super-admin, cce-admin] + Experts: + description: Generate expert registration report + roles: [cce-super-admin, cce-admin] InteractiveMap: Manage: description: Create/update/delete interactive maps diff --git a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs index 8266f58b..c2920554 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs @@ -1,5 +1,6 @@ using CCE.Api.Common.Extensions; using CCE.Application.Reports; +using CCE.Application.Reports.Queries.GetExpertReport; using CCE.Application.Reports.Queries.GetSatisfactionSurveyReport; using CCE.Application.Reports.Queries.GetUserPreferenceReport; using CCE.Application.Reports.Queries.GetUserRegistrationReport; @@ -177,6 +178,14 @@ public static IEndpointRouteBuilder MapReportEndpoints(this IEndpointRouteBuilde .RequireAuthorization(Permissions.Report_UserPreferences) .WithName("UserPreferenceReport"); + reports.MapGet("/experts", async (ISender sender) => + { + var result = await sender.Send(new GetExpertReportQuery()); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.Report_Experts) + .WithName("ExpertReport"); + return app; } } diff --git a/backend/src/CCE.Application/Reports/Dtos/ExpertReportDto.cs b/backend/src/CCE.Application/Reports/Dtos/ExpertReportDto.cs new file mode 100644 index 00000000..989eea0a --- /dev/null +++ b/backend/src/CCE.Application/Reports/Dtos/ExpertReportDto.cs @@ -0,0 +1,18 @@ +namespace CCE.Application.Reports.Dtos; + +public sealed record ExpertReportDto( + Guid Id, + Guid UserId, + string FirstName, + string LastName, + string? Email, + string JobTitle, + string OrganizationName, + string CvDescriptionEn, + string CvDescriptionAr, + string? CvAttachmentUrl, + string CvFileFormat, + List ExpertiseTopics, + int Status, + DateTimeOffset SubmittedAt +); diff --git a/backend/src/CCE.Application/Reports/Queries/GetExpertReport/GetExpertReportQuery.cs b/backend/src/CCE.Application/Reports/Queries/GetExpertReport/GetExpertReportQuery.cs new file mode 100644 index 00000000..cfdeff74 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetExpertReport/GetExpertReportQuery.cs @@ -0,0 +1,8 @@ +using CCE.Application.Common; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetExpertReport; + +public sealed record GetExpertReportQuery() + : IRequest>>; diff --git a/backend/src/CCE.Application/Reports/Queries/GetExpertReport/GetExpertReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetExpertReport/GetExpertReportQueryHandler.cs new file mode 100644 index 00000000..22082682 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetExpertReport/GetExpertReportQueryHandler.cs @@ -0,0 +1,85 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; +using CCE.Application.Messages; +using CCE.Application.Reports.Dtos; +using CCE.Domain.Identity; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetExpertReport; + +internal sealed class GetExpertReportQueryHandler( + ICceDbContext _db, + MessageFactory _msg) + : IRequestHandler>> +{ + public async Task>> Handle( + GetExpertReportQuery q, CancellationToken ct) + { + var raw = await ( + from req in _db.ExpertRegistrationRequests + join u in _db.Users on req.RequestedById equals u.Id + from att in req.Attachments + .Where(a => a.AttachmentType == ExpertRequestAttachmentType.Cv) + .DefaultIfEmpty() + join af in _db.AssetFiles on att.AssetFileId equals af.Id into afGroup + from af in afGroup.DefaultIfEmpty() + orderby req.SubmittedOn descending + select new + { + req.Id, + UserId = u.Id, + u.FirstName, + u.LastName, + u.Email, + u.JobTitle, + u.OrganizationName, + req.RequestedBioEn, + req.RequestedBioAr, + CvUrl = af.Url, + CvMimeType = af.MimeType, + req.RequestedTags, + Status = (int)req.Status, + req.SubmittedOn + }) + .ToListAsyncEither(ct) + .ConfigureAwait(false); + + var result = raw.Select(x => new ExpertReportDto( + x.Id, + x.UserId, + x.FirstName, + x.LastName, + x.Email, + x.JobTitle, + x.OrganizationName, + x.RequestedBioEn, + x.RequestedBioAr, + x.CvUrl, + DeriveFileFormat(x.CvMimeType), + x.RequestedTags.ToList(), + x.Status, + x.SubmittedOn + )).ToList(); + + return _msg.Ok(result, "ITEMS_LISTED"); + } + + private static string DeriveFileFormat(string? mimeType) + { + if (string.IsNullOrWhiteSpace(mimeType)) + return string.Empty; + + return mimeType switch + { + "application/pdf" => "PDF", + "application/msword" => "DOC", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => "DOCX", + "application/vnd.ms-excel" => "XLS", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" => "XLSX", + "image/jpeg" => "JPEG", + "image/png" => "PNG", + _ => mimeType.ToUpperInvariant() + }; + } +} From cc014e621819deab1c2cd36d40844b5ae48fa438 Mon Sep 17 00:00:00 2001 From: ahmed Date: Tue, 23 Jun 2026 12:40:43 +0300 Subject: [PATCH 05/10] feat: Report CommunityPost --- .../Endpoints/ReportEndpoints.cs | 14 +++++++ .../Reports/Dtos/CommunityPostReportDto.cs | 9 ++++ .../GetCommunityPostReportQuery.cs | 13 ++++++ .../GetCommunityPostReportQueryHandler.cs | 41 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 backend/src/CCE.Application/Reports/Dtos/CommunityPostReportDto.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetCommunityPostReport/GetCommunityPostReportQuery.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetCommunityPostReport/GetCommunityPostReportQueryHandler.cs diff --git a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs index c2920554..ce178503 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs @@ -1,5 +1,6 @@ using CCE.Api.Common.Extensions; using CCE.Application.Reports; +using CCE.Application.Reports.Queries.GetCommunityPostReport; using CCE.Application.Reports.Queries.GetExpertReport; using CCE.Application.Reports.Queries.GetSatisfactionSurveyReport; using CCE.Application.Reports.Queries.GetUserPreferenceReport; @@ -186,6 +187,19 @@ public static IEndpointRouteBuilder MapReportEndpoints(this IEndpointRouteBuilde .RequireAuthorization(Permissions.Report_Experts) .WithName("ExpertReport"); + reports.MapGet("/community-posts", async ( + ISender sender, + DateTimeOffset? from, + DateTimeOffset? to, + int page = 1, + int pageSize = 20) => + { + var result = await sender.Send(new GetCommunityPostReportQuery(from, to, page, pageSize)); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.Report_CommunityPosts) + .WithName("CommunityPostReportJson"); + return app; } } diff --git a/backend/src/CCE.Application/Reports/Dtos/CommunityPostReportDto.cs b/backend/src/CCE.Application/Reports/Dtos/CommunityPostReportDto.cs new file mode 100644 index 00000000..9e1c5c55 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Dtos/CommunityPostReportDto.cs @@ -0,0 +1,9 @@ +namespace CCE.Application.Reports.Dtos; + +public sealed record CommunityPostReportDto( + Guid Id, + string? PostTitle, + string? PostContent, + int PostType, + DateTimeOffset CreatedAt +); diff --git a/backend/src/CCE.Application/Reports/Queries/GetCommunityPostReport/GetCommunityPostReportQuery.cs b/backend/src/CCE.Application/Reports/Queries/GetCommunityPostReport/GetCommunityPostReportQuery.cs new file mode 100644 index 00000000..09dd52de --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetCommunityPostReport/GetCommunityPostReportQuery.cs @@ -0,0 +1,13 @@ +using CCE.Application.Common; +using CCE.Application.Common.Pagination; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetCommunityPostReport; + +public sealed record GetCommunityPostReportQuery( + DateTimeOffset? From = null, + DateTimeOffset? To = null, + int Page = 1, + int PageSize = 20 +) : IRequest>>; diff --git a/backend/src/CCE.Application/Reports/Queries/GetCommunityPostReport/GetCommunityPostReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetCommunityPostReport/GetCommunityPostReportQueryHandler.cs new file mode 100644 index 00000000..c0205130 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetCommunityPostReport/GetCommunityPostReportQueryHandler.cs @@ -0,0 +1,41 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; +using CCE.Application.Messages; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetCommunityPostReport; + +internal sealed class GetCommunityPostReportQueryHandler( + ICceDbContext _db, + MessageFactory _msg) + : IRequestHandler>> +{ + public async Task>> Handle( + GetCommunityPostReportQuery q, CancellationToken ct) + { + var query = _db.Posts.AsQueryable(); + + if (q.From.HasValue) + query = query.Where(p => p.CreatedOn >= q.From.Value); + if (q.To.HasValue) + query = query.Where(p => p.CreatedOn <= q.To.Value); + + query = query.OrderByDescending(p => p.CreatedOn); + + var paged = await query.ToPagedResultAsync( + p => new CommunityPostReportDto( + p.Id, + p.Title, + p.Content, + (int)p.Type, + p.CreatedOn), + q.Page, + q.PageSize, + ct) + .ConfigureAwait(false); + + return _msg.Ok(paged, "ITEMS_LISTED"); + } +} From 81b483179b8dc79f9235ff88bf23194282b44ce0 Mon Sep 17 00:00:00 2001 From: ahmed Date: Tue, 23 Jun 2026 13:31:10 +0300 Subject: [PATCH 06/10] feat: Report news --- .../Endpoints/ReportEndpoints.cs | 14 ++++++ .../Reports/Dtos/NewsReportDto.cs | 13 +++++ .../GetNewsReport/GetNewsReportQuery.cs | 13 +++++ .../GetNewsReportQueryHandler.cs | 47 +++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 backend/src/CCE.Application/Reports/Dtos/NewsReportDto.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetNewsReport/GetNewsReportQuery.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetNewsReport/GetNewsReportQueryHandler.cs diff --git a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs index ce178503..b97886a5 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs @@ -2,6 +2,7 @@ using CCE.Application.Reports; using CCE.Application.Reports.Queries.GetCommunityPostReport; using CCE.Application.Reports.Queries.GetExpertReport; +using CCE.Application.Reports.Queries.GetNewsReport; using CCE.Application.Reports.Queries.GetSatisfactionSurveyReport; using CCE.Application.Reports.Queries.GetUserPreferenceReport; using CCE.Application.Reports.Queries.GetUserRegistrationReport; @@ -187,6 +188,19 @@ public static IEndpointRouteBuilder MapReportEndpoints(this IEndpointRouteBuilde .RequireAuthorization(Permissions.Report_Experts) .WithName("ExpertReport"); + reports.MapGet("/news", async ( + ISender sender, + DateTimeOffset? from, + DateTimeOffset? to, + int page = 1, + int pageSize = 20) => + { + var result = await sender.Send(new GetNewsReportQuery(from, to, page, pageSize)); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.Report_News) + .WithName("NewsReportJson"); + reports.MapGet("/community-posts", async ( ISender sender, DateTimeOffset? from, diff --git a/backend/src/CCE.Application/Reports/Dtos/NewsReportDto.cs b/backend/src/CCE.Application/Reports/Dtos/NewsReportDto.cs new file mode 100644 index 00000000..6f462a58 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Dtos/NewsReportDto.cs @@ -0,0 +1,13 @@ +namespace CCE.Application.Reports.Dtos; + +public sealed record NewsReportDto( + Guid Id, + string TitleAr, + string TitleEn, + string? ImageUrl, + string TopicNameAr, + string TopicNameEn, + string ContentAr, + string ContentEn, + DateTimeOffset? PublishedAt +); diff --git a/backend/src/CCE.Application/Reports/Queries/GetNewsReport/GetNewsReportQuery.cs b/backend/src/CCE.Application/Reports/Queries/GetNewsReport/GetNewsReportQuery.cs new file mode 100644 index 00000000..e1bbc623 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetNewsReport/GetNewsReportQuery.cs @@ -0,0 +1,13 @@ +using CCE.Application.Common; +using CCE.Application.Common.Pagination; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetNewsReport; + +public sealed record GetNewsReportQuery( + DateTimeOffset? From = null, + DateTimeOffset? To = null, + int Page = 1, + int PageSize = 20 +) : IRequest>>; diff --git a/backend/src/CCE.Application/Reports/Queries/GetNewsReport/GetNewsReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetNewsReport/GetNewsReportQueryHandler.cs new file mode 100644 index 00000000..1280b091 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetNewsReport/GetNewsReportQueryHandler.cs @@ -0,0 +1,47 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; +using CCE.Application.Messages; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetNewsReport; + +internal sealed class GetNewsReportQueryHandler( + ICceDbContext _db, + MessageFactory _msg) + : IRequestHandler>> +{ + public async Task>> Handle( + GetNewsReportQuery q, CancellationToken ct) + { + var query = from n in _db.News + join t in _db.Topics on n.TopicId equals t.Id + select new { n, TopicNameEn = t.NameEn, TopicNameAr = t.NameAr }; + + if (q.From.HasValue) + query = query.Where(x => x.n.PublishedOn >= q.From.Value); + if (q.To.HasValue) + query = query.Where(x => x.n.PublishedOn <= q.To.Value); + + query = query.OrderByDescending(x => x.n.PublishedOn); + + var paged = await query.ToPagedResultAsync( + x => new NewsReportDto( + x.n.Id, + x.n.TitleAr, + x.n.TitleEn, + x.n.FeaturedImageUrl, + x.TopicNameEn, + x.TopicNameAr, + x.n.ContentAr, + x.n.ContentEn, + x.n.PublishedOn), + q.Page, + q.PageSize, + ct) + .ConfigureAwait(false); + + return _msg.Ok(paged, "ITEMS_LISTED"); + } +} From 16113ae1187e7928acaf7baa367c28b65055cf01 Mon Sep 17 00:00:00 2001 From: ahmed Date: Tue, 23 Jun 2026 13:43:29 +0300 Subject: [PATCH 07/10] feat: Report events --- .../Endpoints/ReportEndpoints.cs | 14 ++++++ .../Reports/Dtos/EventsReportDto.cs | 14 ++++++ .../GetEventsReport/GetEventsReportQuery.cs | 13 +++++ .../GetEventsReportQueryHandler.cs | 48 +++++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 backend/src/CCE.Application/Reports/Dtos/EventsReportDto.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetEventsReport/GetEventsReportQuery.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetEventsReport/GetEventsReportQueryHandler.cs diff --git a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs index b97886a5..c7153d6b 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs @@ -1,6 +1,7 @@ using CCE.Api.Common.Extensions; using CCE.Application.Reports; using CCE.Application.Reports.Queries.GetCommunityPostReport; +using CCE.Application.Reports.Queries.GetEventsReport; using CCE.Application.Reports.Queries.GetExpertReport; using CCE.Application.Reports.Queries.GetNewsReport; using CCE.Application.Reports.Queries.GetSatisfactionSurveyReport; @@ -214,6 +215,19 @@ public static IEndpointRouteBuilder MapReportEndpoints(this IEndpointRouteBuilde .RequireAuthorization(Permissions.Report_CommunityPosts) .WithName("CommunityPostReportJson"); + reports.MapGet("/events", async ( + ISender sender, + DateTimeOffset? from, + DateTimeOffset? to, + int page = 1, + int pageSize = 20) => + { + var result = await sender.Send(new GetEventsReportQuery(from, to, page, pageSize)); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.Report_Events) + .WithName("EventsReportJson"); + return app; } } diff --git a/backend/src/CCE.Application/Reports/Dtos/EventsReportDto.cs b/backend/src/CCE.Application/Reports/Dtos/EventsReportDto.cs new file mode 100644 index 00000000..71ec36da --- /dev/null +++ b/backend/src/CCE.Application/Reports/Dtos/EventsReportDto.cs @@ -0,0 +1,14 @@ +namespace CCE.Application.Reports.Dtos; + +public sealed record EventsReportDto( + Guid Id, + string Title, + string EventDescription, + string? Location, + string Topic, + DateTimeOffset StartsOn, + DateTimeOffset EndsOn, + string? FeaturedImageUrl, + string? OnlineMeetingUrl, + DateTimeOffset CreatedAt +); diff --git a/backend/src/CCE.Application/Reports/Queries/GetEventsReport/GetEventsReportQuery.cs b/backend/src/CCE.Application/Reports/Queries/GetEventsReport/GetEventsReportQuery.cs new file mode 100644 index 00000000..9e76ccd1 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetEventsReport/GetEventsReportQuery.cs @@ -0,0 +1,13 @@ +using CCE.Application.Common; +using CCE.Application.Common.Pagination; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetEventsReport; + +public sealed record GetEventsReportQuery( + DateTimeOffset? From = null, + DateTimeOffset? To = null, + int Page = 1, + int PageSize = 20 +) : IRequest>>; diff --git a/backend/src/CCE.Application/Reports/Queries/GetEventsReport/GetEventsReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetEventsReport/GetEventsReportQueryHandler.cs new file mode 100644 index 00000000..12f9fe29 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetEventsReport/GetEventsReportQueryHandler.cs @@ -0,0 +1,48 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; +using CCE.Application.Messages; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetEventsReport; + +internal sealed class GetEventsReportQueryHandler( + ICceDbContext _db, + MessageFactory _msg) + : IRequestHandler>> +{ + public async Task>> Handle( + GetEventsReportQuery q, CancellationToken ct) + { + var query = from e in _db.Events + join t in _db.Topics on e.TopicId equals t.Id + select new { e, TopicName = t.NameEn }; + + if (q.From.HasValue) + query = query.Where(x => x.e.StartsOn >= q.From.Value); + if (q.To.HasValue) + query = query.Where(x => x.e.StartsOn <= q.To.Value); + + query = query.OrderByDescending(x => x.e.StartsOn); + + var paged = await query.ToPagedResultAsync( + x => new EventsReportDto( + x.e.Id, + x.e.TitleEn, + x.e.DescriptionEn, + x.e.LocationEn, + x.TopicName, + x.e.StartsOn, + x.e.EndsOn, + x.e.FeaturedImageUrl, + x.e.OnlineMeetingUrl, + x.e.CreatedOn), + q.Page, + q.PageSize, + ct) + .ConfigureAwait(false); + + return _msg.Ok(paged, "ITEMS_LISTED"); + } +} From feedab8dc651bed6628020f9fcaaa7dd9da658f9 Mon Sep 17 00:00:00 2001 From: ahmed Date: Tue, 23 Jun 2026 15:31:06 +0300 Subject: [PATCH 08/10] feat: Report resources --- .../Endpoints/ReportEndpoints.cs | 14 +++++ .../Reports/Dtos/ResourcesReportDto.cs | 12 ++++ .../GetResourcesReportQuery.cs | 13 ++++ .../GetResourcesReportQueryHandler.cs | 63 +++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 backend/src/CCE.Application/Reports/Dtos/ResourcesReportDto.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetResourcesReport/GetResourcesReportQuery.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetResourcesReport/GetResourcesReportQueryHandler.cs diff --git a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs index c7153d6b..81693fc9 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs @@ -3,6 +3,7 @@ using CCE.Application.Reports.Queries.GetCommunityPostReport; using CCE.Application.Reports.Queries.GetEventsReport; using CCE.Application.Reports.Queries.GetExpertReport; +using CCE.Application.Reports.Queries.GetResourcesReport; using CCE.Application.Reports.Queries.GetNewsReport; using CCE.Application.Reports.Queries.GetSatisfactionSurveyReport; using CCE.Application.Reports.Queries.GetUserPreferenceReport; @@ -228,6 +229,19 @@ public static IEndpointRouteBuilder MapReportEndpoints(this IEndpointRouteBuilde .RequireAuthorization(Permissions.Report_Events) .WithName("EventsReportJson"); + reports.MapGet("/resources", async ( + ISender sender, + DateTimeOffset? from, + DateTimeOffset? to, + int page = 1, + int pageSize = 20) => + { + var result = await sender.Send(new GetResourcesReportQuery(from, to, page, pageSize)); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.Report_Resources) + .WithName("ResourcesReportJson"); + return app; } } diff --git a/backend/src/CCE.Application/Reports/Dtos/ResourcesReportDto.cs b/backend/src/CCE.Application/Reports/Dtos/ResourcesReportDto.cs new file mode 100644 index 00000000..8986a29f --- /dev/null +++ b/backend/src/CCE.Application/Reports/Dtos/ResourcesReportDto.cs @@ -0,0 +1,12 @@ +namespace CCE.Application.Reports.Dtos; + +public sealed record ResourcesReportDto( + Guid Id, + string Title, + string Description, + Guid CategoryId, + string Category, + int PostType, + Guid[] CoveredCountries, + DateTimeOffset CreatedAt +); diff --git a/backend/src/CCE.Application/Reports/Queries/GetResourcesReport/GetResourcesReportQuery.cs b/backend/src/CCE.Application/Reports/Queries/GetResourcesReport/GetResourcesReportQuery.cs new file mode 100644 index 00000000..65c0119b --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetResourcesReport/GetResourcesReportQuery.cs @@ -0,0 +1,13 @@ +using CCE.Application.Common; +using CCE.Application.Common.Pagination; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetResourcesReport; + +public sealed record GetResourcesReportQuery( + DateTimeOffset? From = null, + DateTimeOffset? To = null, + int Page = 1, + int PageSize = 20 +) : IRequest>>; diff --git a/backend/src/CCE.Application/Reports/Queries/GetResourcesReport/GetResourcesReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetResourcesReport/GetResourcesReportQueryHandler.cs new file mode 100644 index 00000000..c1240ab9 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetResourcesReport/GetResourcesReportQueryHandler.cs @@ -0,0 +1,63 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; +using CCE.Application.Messages; +using CCE.Application.Reports.Dtos; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace CCE.Application.Reports.Queries.GetResourcesReport; + +internal sealed class GetResourcesReportQueryHandler( + ICceDbContext _db, + MessageFactory _msg) + : IRequestHandler>> +{ + public async Task>> Handle( + GetResourcesReportQuery q, CancellationToken ct) + { + var query = from r in _db.Resources + join cat in _db.ResourceCategories on r.CategoryId equals cat.Id + select new { r, CategoryName = cat.NameEn }; + + if (q.From.HasValue) + query = query.Where(x => x.r.CreatedOn >= q.From.Value); + if (q.To.HasValue) + query = query.Where(x => x.r.CreatedOn <= q.To.Value); + + query = query.OrderByDescending(x => x.r.CreatedOn); + + var page = Math.Max(1, q.Page); + var pageSize = Math.Clamp(q.PageSize, 1, PaginationExtensions.MaxPageSize); + + var total = await query.LongCountAsync(ct).ConfigureAwait(false); + + var items = await query + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(ct) + .ConfigureAwait(false); + + var resourceIds = items.Select(x => x.r.Id).ToList(); + var countryMap = await _db.Resources + .Where(r => resourceIds.Contains(r.Id)) + .SelectMany(r => r.Countries.Select(rc => new { r.Id, rc.CountryId })) + .GroupBy(x => x.Id) + .ToDictionaryAsync(g => g.Key, g => g.Select(x => x.CountryId).ToArray(), ct) + .ConfigureAwait(false); + + var dtos = items.Select(x => new ResourcesReportDto( + x.r.Id, + x.r.TitleEn, + x.r.DescriptionEn, + x.r.CategoryId, + x.CategoryName, + (int)x.r.ResourceType, + countryMap.GetValueOrDefault(x.r.Id, []), + x.r.CreatedOn + )).ToList(); + + var paged = new PagedResult(dtos, page, pageSize, total); + return _msg.Ok(paged, "ITEMS_LISTED"); + } +} From 313fcad56ac58e83e250eaa93b0224b6f4f99d97 Mon Sep 17 00:00:00 2001 From: ahmed Date: Tue, 23 Jun 2026 16:49:12 +0300 Subject: [PATCH 09/10] feat: Report country profiles --- .../Endpoints/ReportEndpoints.cs | 14 +++++ .../Reports/Dtos/CountryProfilesReportDto.cs | 12 ++++ .../GetCountryProfilesReportQuery.cs | 13 ++++ .../GetCountryProfilesReportQueryHandler.cs | 59 +++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 backend/src/CCE.Application/Reports/Dtos/CountryProfilesReportDto.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetCountryProfilesReport/GetCountryProfilesReportQuery.cs create mode 100644 backend/src/CCE.Application/Reports/Queries/GetCountryProfilesReport/GetCountryProfilesReportQueryHandler.cs diff --git a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs index 81693fc9..a9010706 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/ReportEndpoints.cs @@ -1,6 +1,7 @@ using CCE.Api.Common.Extensions; using CCE.Application.Reports; using CCE.Application.Reports.Queries.GetCommunityPostReport; +using CCE.Application.Reports.Queries.GetCountryProfilesReport; using CCE.Application.Reports.Queries.GetEventsReport; using CCE.Application.Reports.Queries.GetExpertReport; using CCE.Application.Reports.Queries.GetResourcesReport; @@ -242,6 +243,19 @@ public static IEndpointRouteBuilder MapReportEndpoints(this IEndpointRouteBuilde .RequireAuthorization(Permissions.Report_Resources) .WithName("ResourcesReportJson"); + reports.MapGet("/country-profiles", async ( + ISender sender, + DateTimeOffset? from, + DateTimeOffset? to, + int page = 1, + int pageSize = 20) => + { + var result = await sender.Send(new GetCountryProfilesReportQuery(from, to, page, pageSize)); + return result.ToHttpResult(); + }) + .RequireAuthorization(Permissions.Report_CountryProfiles) + .WithName("CountryProfilesReportJson"); + return app; } } diff --git a/backend/src/CCE.Application/Reports/Dtos/CountryProfilesReportDto.cs b/backend/src/CCE.Application/Reports/Dtos/CountryProfilesReportDto.cs new file mode 100644 index 00000000..7094d4bb --- /dev/null +++ b/backend/src/CCE.Application/Reports/Dtos/CountryProfilesReportDto.cs @@ -0,0 +1,12 @@ +namespace CCE.Application.Reports.Dtos; + +public sealed record CountryProfilesReportDto( + Guid Id, + string CountryName, + int? Population, + decimal? Area, + decimal? GdpPerCapita, + string? NdcAttachmentUrl, + string? CceClassification, + decimal? CcePerformanceIndex +); diff --git a/backend/src/CCE.Application/Reports/Queries/GetCountryProfilesReport/GetCountryProfilesReportQuery.cs b/backend/src/CCE.Application/Reports/Queries/GetCountryProfilesReport/GetCountryProfilesReportQuery.cs new file mode 100644 index 00000000..64fdb119 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetCountryProfilesReport/GetCountryProfilesReportQuery.cs @@ -0,0 +1,13 @@ +using CCE.Application.Common; +using CCE.Application.Common.Pagination; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetCountryProfilesReport; + +public sealed record GetCountryProfilesReportQuery( + DateTimeOffset? From = null, + DateTimeOffset? To = null, + int Page = 1, + int PageSize = 20 +) : IRequest>>; diff --git a/backend/src/CCE.Application/Reports/Queries/GetCountryProfilesReport/GetCountryProfilesReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetCountryProfilesReport/GetCountryProfilesReportQueryHandler.cs new file mode 100644 index 00000000..55e23566 --- /dev/null +++ b/backend/src/CCE.Application/Reports/Queries/GetCountryProfilesReport/GetCountryProfilesReportQueryHandler.cs @@ -0,0 +1,59 @@ +using CCE.Application.Common; +using CCE.Application.Common.Interfaces; +using CCE.Application.Common.Pagination; +using CCE.Application.Messages; +using CCE.Application.Reports.Dtos; +using MediatR; + +namespace CCE.Application.Reports.Queries.GetCountryProfilesReport; + +internal sealed class GetCountryProfilesReportQueryHandler( + ICceDbContext _db, + MessageFactory _msg) + : IRequestHandler>> +{ + public async Task>> Handle( + GetCountryProfilesReportQuery q, CancellationToken ct) + { + var query = from c in _db.Countries.WithoutSoftDeleteFilter() + where c.IsCceCountry + join p in _db.CountryProfiles on c.Id equals p.CountryId into pJoin + from p in pJoin.DefaultIfEmpty() + join asset in _db.AssetFiles on p.NationallyDeterminedContributionAssetId equals asset.Id into assetJoin + from asset in assetJoin.DefaultIfEmpty() + join snap in _db.CountryKapsarcSnapshots on c.LatestKapsarcSnapshotId equals snap.Id into snapJoin + from snap in snapJoin.DefaultIfEmpty() + select new + { + c, + p, + NdcUrl = (string?)asset.Url, + snap + }; + + if (q.From.HasValue) + query = query.Where(x => x.p != null && (x.p.LastModifiedOn ?? x.p.CreatedOn) >= q.From.Value); + if (q.To.HasValue) + query = query.Where(x => x.p != null && (x.p.LastModifiedOn ?? x.p.CreatedOn) <= q.To.Value); + + query = query.OrderBy(x => x.c.NameEn); + + var paged = await query.ToPagedResultAsync( + x => new CountryProfilesReportDto( + x.p != null ? x.p.Id : x.c.Id, + x.c.NameEn, + x.p != null ? x.p.Population : null, + x.p != null ? x.p.AreaSqKm : null, + x.p != null ? x.p.GdpPerCapita : null, + x.NdcUrl, + x.snap != null ? x.snap.Classification : null, + x.snap != null ? x.snap.PerformanceScore : null + ), + q.Page, + q.PageSize, + ct) + .ConfigureAwait(false); + + return _msg.Ok(paged, "ITEMS_LISTED"); + } +} From ace66b2394676ec2d4584a3a88edfb8154af51cf Mon Sep 17 00:00:00 2001 From: ahmed Date: Thu, 25 Jun 2026 13:51:53 +0300 Subject: [PATCH 10/10] refactor: migrate Reports handlers to MessageKeys constants + update CRUD guide --- .../GetCommunityPostReportQueryHandler.cs | 2 +- .../GetCountryProfilesReportQueryHandler.cs | 2 +- .../GetEventsReportQueryHandler.cs | 2 +- .../GetExpertReportQueryHandler.cs | 2 +- .../GetNewsReportQueryHandler.cs | 2 +- .../GetResourcesReportQueryHandler.cs | 2 +- ...GetSatisfactionSurveyReportQueryHandler.cs | 2 +- .../GetUserPreferenceReportQueryHandler.cs | 2 +- .../GetUserRegistrationReportQueryHandler.cs | 2 +- backend/src/docs/crud-implementation-guide.md | 92 ++++++++++++------- 10 files changed, 68 insertions(+), 42 deletions(-) diff --git a/backend/src/CCE.Application/Reports/Queries/GetCommunityPostReport/GetCommunityPostReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetCommunityPostReport/GetCommunityPostReportQueryHandler.cs index c0205130..cdd16080 100644 --- a/backend/src/CCE.Application/Reports/Queries/GetCommunityPostReport/GetCommunityPostReportQueryHandler.cs +++ b/backend/src/CCE.Application/Reports/Queries/GetCommunityPostReport/GetCommunityPostReportQueryHandler.cs @@ -36,6 +36,6 @@ public async Task>> Handle( ct) .ConfigureAwait(false); - return _msg.Ok(paged, "ITEMS_LISTED"); + return _msg.Ok(paged, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Reports/Queries/GetCountryProfilesReport/GetCountryProfilesReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetCountryProfilesReport/GetCountryProfilesReportQueryHandler.cs index 55e23566..68a809e5 100644 --- a/backend/src/CCE.Application/Reports/Queries/GetCountryProfilesReport/GetCountryProfilesReportQueryHandler.cs +++ b/backend/src/CCE.Application/Reports/Queries/GetCountryProfilesReport/GetCountryProfilesReportQueryHandler.cs @@ -54,6 +54,6 @@ from snap in snapJoin.DefaultIfEmpty() ct) .ConfigureAwait(false); - return _msg.Ok(paged, "ITEMS_LISTED"); + return _msg.Ok(paged, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Reports/Queries/GetEventsReport/GetEventsReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetEventsReport/GetEventsReportQueryHandler.cs index 12f9fe29..bae6370a 100644 --- a/backend/src/CCE.Application/Reports/Queries/GetEventsReport/GetEventsReportQueryHandler.cs +++ b/backend/src/CCE.Application/Reports/Queries/GetEventsReport/GetEventsReportQueryHandler.cs @@ -43,6 +43,6 @@ join t in _db.Topics on e.TopicId equals t.Id ct) .ConfigureAwait(false); - return _msg.Ok(paged, "ITEMS_LISTED"); + return _msg.Ok(paged, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Reports/Queries/GetExpertReport/GetExpertReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetExpertReport/GetExpertReportQueryHandler.cs index 22082682..549bc6fc 100644 --- a/backend/src/CCE.Application/Reports/Queries/GetExpertReport/GetExpertReportQueryHandler.cs +++ b/backend/src/CCE.Application/Reports/Queries/GetExpertReport/GetExpertReportQueryHandler.cs @@ -62,7 +62,7 @@ orderby req.SubmittedOn descending x.SubmittedOn )).ToList(); - return _msg.Ok(result, "ITEMS_LISTED"); + return _msg.Ok(result, MessageKeys.General.ITEMS_LISTED); } private static string DeriveFileFormat(string? mimeType) diff --git a/backend/src/CCE.Application/Reports/Queries/GetNewsReport/GetNewsReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetNewsReport/GetNewsReportQueryHandler.cs index 1280b091..73ae19db 100644 --- a/backend/src/CCE.Application/Reports/Queries/GetNewsReport/GetNewsReportQueryHandler.cs +++ b/backend/src/CCE.Application/Reports/Queries/GetNewsReport/GetNewsReportQueryHandler.cs @@ -42,6 +42,6 @@ join t in _db.Topics on n.TopicId equals t.Id ct) .ConfigureAwait(false); - return _msg.Ok(paged, "ITEMS_LISTED"); + return _msg.Ok(paged, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Reports/Queries/GetResourcesReport/GetResourcesReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetResourcesReport/GetResourcesReportQueryHandler.cs index c1240ab9..bcce36af 100644 --- a/backend/src/CCE.Application/Reports/Queries/GetResourcesReport/GetResourcesReportQueryHandler.cs +++ b/backend/src/CCE.Application/Reports/Queries/GetResourcesReport/GetResourcesReportQueryHandler.cs @@ -58,6 +58,6 @@ join cat in _db.ResourceCategories on r.CategoryId equals cat.Id )).ToList(); var paged = new PagedResult(dtos, page, pageSize, total); - return _msg.Ok(paged, "ITEMS_LISTED"); + return _msg.Ok(paged, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Reports/Queries/GetSatisfactionSurveyReport/GetSatisfactionSurveyReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetSatisfactionSurveyReport/GetSatisfactionSurveyReportQueryHandler.cs index 7197a8b7..b0ea52a1 100644 --- a/backend/src/CCE.Application/Reports/Queries/GetSatisfactionSurveyReport/GetSatisfactionSurveyReportQueryHandler.cs +++ b/backend/src/CCE.Application/Reports/Queries/GetSatisfactionSurveyReport/GetSatisfactionSurveyReportQueryHandler.cs @@ -28,6 +28,6 @@ public async Task>> Handle( .ToListAsyncEither(ct) .ConfigureAwait(false); - return _msg.Ok(items, "ITEMS_LISTED"); + return _msg.Ok(items, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Reports/Queries/GetUserPreferenceReport/GetUserPreferenceReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetUserPreferenceReport/GetUserPreferenceReportQueryHandler.cs index c9fc7130..d82d05f0 100644 --- a/backend/src/CCE.Application/Reports/Queries/GetUserPreferenceReport/GetUserPreferenceReportQueryHandler.cs +++ b/backend/src/CCE.Application/Reports/Queries/GetUserPreferenceReport/GetUserPreferenceReportQueryHandler.cs @@ -28,6 +28,6 @@ public async Task>> Handle( .ToListAsyncEither(ct) .ConfigureAwait(false); - return _msg.Ok(items, "ITEMS_LISTED"); + return _msg.Ok(items, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQueryHandler.cs b/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQueryHandler.cs index 0953e6ff..62f75bb5 100644 --- a/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQueryHandler.cs +++ b/backend/src/CCE.Application/Reports/Queries/GetUserRegistrationReport/GetUserRegistrationReportQueryHandler.cs @@ -28,6 +28,6 @@ public async Task>> Handle( .ToListAsyncEither(ct) .ConfigureAwait(false); - return _msg.Ok(users, "ITEMS_LISTED"); + return _msg.Ok(users, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/docs/crud-implementation-guide.md b/backend/src/docs/crud-implementation-guide.md index ea60b7a4..96de0f7f 100644 --- a/backend/src/docs/crud-implementation-guide.md +++ b/backend/src/docs/crud-implementation-guide.md @@ -13,7 +13,7 @@ This document captures the **architectural patterns and conventions** used in th | **Unit of Work** | Repository tracks, handler commits (`ICceDbContext.SaveChangesAsync`) | | **Reads → ICceDbContext** | All read operations inject `ICceDbContext` directly, no repository | | **Writes → Repository** | Write operations use repository interface + domain factory | -| **Response Envelope** | Every endpoint returns `Response` via `MessageFactory` | +| **Response Envelope** | Every endpoint returns `Response` via `MessageFactory` + `MessageKeys` constants | | **No validation in endpoints** | All validation is in FluentValidation validators only | --- @@ -30,7 +30,7 @@ This document captures the **architectural patterns and conventions** used in th 8. [Pattern: Pagination with PagedResult\](#pattern-pagination-with-pagedresultt) 9. [Pattern: Enum Handling (int Request, String Response)](#pattern-enum-handling-int-request-string-response) 10. [Pattern: Anonymous Users + Nullable CreatedById](#pattern-anonymous-users--nullable-createdbyid) -11. [Pattern: Error/Success Codes (SystemCode, ApplicationErrors, Resources.yaml)](#pattern-errorsuccess-codes) +11. [Pattern: Message Keys, System Codes & Localization](#step-6--message-keys-system-codes--localization) 12. [Pattern: LocalizedText Value Object](#pattern-localizedtext-value-object) 13. [Pattern: SuperAdmin Authorization](#pattern-superadmin-authorization) 14. [Pattern: Domain Factory + Mutation Methods](#pattern-domain-factory--mutation-methods) @@ -163,13 +163,13 @@ internal sealed class CreateYourEntityCommandHandler( // For endpoints with [AllowAnonymous], userId may be null // Domain factory requires non-null, so handle accordingly: if (userId is null) - return _msg.Unauthorized("NOT_AUTHENTICATED"); + return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var entity = YourEntity.Create(cmd.Name, cmd.Order, userId.Value, _clock); await _repo.AddAsync(entity, ct); await _db.SaveChangesAsync(ct); // Unit of Work — single commit point - return _msg.Ok("YOUR_ENTITY_CREATED"); + return _msg.Ok(MessageKeys.YourEntity.YOUR_ENTITY_CREATED); } } ``` @@ -188,7 +188,7 @@ public async Task> Handle(CreateYourEntityCommand cmd, Cancel await _repo.AddAsync(entity, ct); await _db.SaveChangesAsync(ct); - return _msg.Ok("YOUR_ENTITY_CREATED"); + return _msg.Ok(MessageKeys.YourEntity.YOUR_ENTITY_CREATED); } ``` @@ -242,18 +242,18 @@ internal sealed class UpdateYourEntityCommandHandler( { var entity = await _repo.GetByIdAsync(cmd.Id, ct); if (entity is null) - return _msg.NotFound("YOUR_ENTITY_NOT_FOUND"); + return _msg.NotFound(MessageKeys.YourEntity.YOUR_ENTITY_NOT_FOUND); var userId = _currentUser.GetUserId(); if (userId is null) - return _msg.Unauthorized("NOT_AUTHENTICATED"); + return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); entity.Update(cmd.Name, cmd.Order, userId.Value, _clock); // No need to call _repo.Update() — EF tracks changes automatically // when the entity was fetched via GetByIdAsync (same DbContext) await _db.SaveChangesAsync(ct); - return _msg.Ok("YOUR_ENTITY_UPDATED"); + return _msg.Ok(MessageKeys.YourEntity.YOUR_ENTITY_UPDATED); } } ``` @@ -270,12 +270,12 @@ internal sealed class DeleteYourEntityCommandHandler( { var entity = await _repo.GetByIdAsync(cmd.Id, ct); if (entity is null) - return _msg.NotFound("YOUR_ENTITY_NOT_FOUND"); + return _msg.NotFound(MessageKeys.YourEntity.YOUR_ENTITY_NOT_FOUND); _repo.Delete(entity); // Marks for deletion await _db.SaveChangesAsync(ct); // Unit of Work commit - return _msg.Ok("YOUR_ENTITY_DELETED"); + return _msg.Ok(MessageKeys.YourEntity.YOUR_ENTITY_DELETED); } } ``` @@ -318,9 +318,9 @@ internal sealed class GetYourEntityByIdQueryHandler( .FirstOrDefaultAsync(ct); if (entity is null) - return _msg.NotFound("YOUR_ENTITY_NOT_FOUND"); + return _msg.NotFound(MessageKeys.YourEntity.YOUR_ENTITY_NOT_FOUND); - return _msg.Ok(entity, "ITEMS_LISTED"); + return _msg.Ok(entity, MessageKeys.General.ITEMS_LISTED); } } ``` @@ -348,7 +348,7 @@ internal sealed class GetAllYourEntitiesQueryHandler( e.Id, e.Name, e.Order, e.CreatedOn, e.CreatedById)) .ToPagedResultAsync(q.Page, q.PageSize, ct); - return _msg.Ok(result, "ITEMS_LISTED"); + return _msg.Ok(result, MessageKeys.General.ITEMS_LISTED); } } ``` @@ -366,7 +366,7 @@ public async Task>> Handle( e.Id, e.Name, e.Order, e.CreatedOn, e.CreatedById)) .ToListAsync(ct); - return _msg.Ok(items, "ITEMS_LISTED"); + return _msg.Ok(items, MessageKeys.General.ITEMS_LISTED); } ``` @@ -453,17 +453,18 @@ dotnet ef migrations add AddYourEntity --context CceDbContext --startup-project --- -### Step 6 — Error/Success Codes & Localization +### Step 6 — Message Keys, System Codes & Localization -**ApplicationErrors.cs — constant domain keys:** +**MessageKeys.cs — add domain key constants:** ```csharp -// CCE.Application\Errors\ApplicationErrors.cs +// CCE.Application\Messages\MessageKeys.cs +// Add a new nested class inside the existing public static class MessageKeys: public static class YourEntity { public const string YOUR_ENTITY_NOT_FOUND = "YOUR_ENTITY_NOT_FOUND"; - public const string YOUR_ENTITY_CREATED = "YOUR_ENTITY_CREATED"; - public const string YOUR_ENTITY_UPDATED = "YOUR_ENTITY_UPDATED"; - public const string YOUR_ENTITY_DELETED = "YOUR_ENTITY_DELETED"; + public const string YOUR_ENTITY_CREATED = "YOUR_ENTITY_CREATED"; + public const string YOUR_ENTITY_UPDATED = "YOUR_ENTITY_UPDATED"; + public const string YOUR_ENTITY_DELETED = "YOUR_ENTITY_DELETED"; } ``` @@ -485,16 +486,41 @@ public const string CON999 = "CON999"; // YourEntity created/updated/deleted ["YOUR_ENTITY_DELETED"] = SystemCode.CON999, ``` -**MessageFactory.cs — convenience shortcuts:** +**Usage in handlers — call `MessageFactory` methods directly with `MessageKeys` constants:** ```csharp -// CCE.Application\Messages\MessageFactory.cs -// ─── Convenience shortcuts (YourEntity) ─── -public Response YourEntityCreated() => Ok(ApplicationErrors.YourEntity.YOUR_ENTITY_CREATED); -public Response YourEntityUpdated() => Ok(ApplicationErrors.YourEntity.YOUR_ENTITY_UPDATED); -public Response YourEntityDeleted() => Ok(ApplicationErrors.YourEntity.YOUR_ENTITY_DELETED); -public Response YourEntityNotFound() => NotFound(ApplicationErrors.YourEntity.YOUR_ENTITY_NOT_FOUND); +// ✅ Use MessageKeys constants directly — no convenience shortcuts on MessageFactory +// _msg is injected MessageFactory + +// Success (no data): +return _msg.Ok(MessageKeys.YourEntity.YOUR_ENTITY_CREATED); + +// Success (with data): +return _msg.Ok(entity, MessageKeys.General.ITEMS_LISTED); + +// Not found: +return _msg.NotFound(MessageKeys.YourEntity.YOUR_ENTITY_NOT_FOUND); + +// Unauthorized: +return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); + +// Conflict: +return _msg.Conflict(MessageKeys.General.DUPLICATE_VALUE); + +// Forbidden: +return _msg.Forbidden(MessageKeys.General.FORBIDDEN_ACCESS); + +// Business rule violation: +return _msg.BusinessRule(MessageKeys.General.BUSINESS_RULE_VIOLATION); + +// Validation with field errors: +return _msg.ValidationError(MessageKeys.General.VALIDATION_ERROR, new[] +{ + _msg.Field("fieldName", MessageKeys.Validation.REQUIRED_FIELD) +}); ``` +> **Note:** Do NOT add convenience shortcut methods to `MessageFactory` (e.g., `YourEntityCreated()`). Always call the base `_msg.Ok()`, `_msg.NotFound()`, etc. directly. This keeps intent explicit in the handler and avoids hidden behavior. + **Resources.yaml — bilingual messages:** ```yaml YOUR_ENTITY_NOT_FOUND: @@ -688,7 +714,7 @@ app.MapYourEntityEndpoints(); │ .FirstOrDefaultAsync / .ToListAsync │ │ .ToPagedResultAsync(...) — pagination │ │ │ -│ Returns _msg.Ok(data, "ITEMS_LISTED") │ +│ Returns _msg.Ok(data, MessageKeys.General.ITEMS_LISTED)│ └─────────────────────────────────────────────────────┘ ``` @@ -733,7 +759,7 @@ app.MapYourEntityEndpoints(); | `_msg.ValidationError(domainKey, errors)` | 400 | Validation errors | ### How it works: -1. Handler passes a **domain key** (e.g., `"YOUR_ENTITY_CREATED"`) +1. Handler passes a **domain key** (e.g., `MessageKeys.YourEntity.YOUR_ENTITY_CREATED`) 2. `MessageFactory` calls `SystemCodeMap.ToSystemCode(key)` → e.g., `"CON999"` 3. `MessageFactory` calls `ILocalizationService.GetString(key)` → localized message 4. Returns `Response` with code + message @@ -838,7 +864,7 @@ public async Task>> Handle( .Select(e => new YourEntityDto(e.Id, e.Name, e.CreatedOn)) .ToPagedResultAsync(q.Page, q.PageSize, ct); - return _msg.Ok(result, "ITEMS_LISTED"); + return _msg.Ok(result, MessageKeys.General.ITEMS_LISTED); } ``` @@ -1120,10 +1146,10 @@ Use this checklist when creating a new CRUD feature. - [ ] `DTOs\YourEntityDto.cs` ### Application Layer — Error/Success Codes -- [ ] `Errors\ApplicationErrors.cs` — add `YourEntity` static class with constants +- [ ] `Messages\MessageKeys.cs` — add `YourEntity` nested class with domain key constants - [ ] `Messages\SystemCode.cs` — add ERR/CON constants - [ ] `Messages\SystemCodeMap.cs` — map domain keys to codes -- [ ] `Messages\MessageFactory.cs` — add convenience shortcut methods +- [ ] (No shortcut methods on `MessageFactory` — call `_msg.Ok(MessageKeys.X)` directly) ### Infrastructure Layer - [ ] `Persistence\Configurations\YourDomain\YourEntityConfiguration.cs` — EF Core config @@ -1156,7 +1182,7 @@ Use this checklist when creating a new CRUD feature. | Using `None=0` enum as valid value | Validator must reject `None` via `.NotEqual(None)` | | Not updating `created_by_id` to nullable for anonymous entities | `CreatedById` is `Guid?` — migration must reflect that | | Forgetting to add `MapYourEntityEndpoints()` to Program.cs | Both External and Internal Program.cs need it | -| Using `Result` instead of `Response` | Use `Response` everywhere — `Result` is legacy | +| Adding convenience shortcuts to `MessageFactory` | Call `_msg.Ok(MessageKeys.X)` directly — don't add `YourEntityCreated()` wrappers | | `.WithErrorCode("REQUIRED_FIELD")` vs `.WithMessage("...")` | Use `.WithErrorCode()` with domain keys, not inline messages | | Not adding `ISystemClock` to handler DI | Domain factory methods need `ISystemClock` for audit timestamps | | `IRepository` import from wrong namespace | Import from `CCE.Application.Common.Interfaces` |