From 62a9bd4177f91f058a3e4a2fc2547f231489a13e Mon Sep 17 00:00:00 2001 From: MOHAMED RASHED Date: Wed, 24 Jun 2026 14:58:29 +0300 Subject: [PATCH 1/2] feat: unified error envelope, Response migration, and validation hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 — Envelope correctness (no contract change) - ExceptionHandlingMiddleware: locale resolved from Accept-Language header - UnauthorizedAccessException maps to 401; correlationId added to envelope body - BAD_REQUEST message made distinct from VALIDATION_ERROR - General.NOT_FOUND renamed to RESOURCE_NOT_FOUND_GENERIC - GetShareLinkQueryHandler consumer fixed - 6/6 middleware tests pass Phase 2 — Contract changes + handler migration - DomainException → 422 BUSINESS_RULE_VIOLATION (ERR910) - Rate-limit 429 envelope via EnvelopeWriter (replaced External plain-text, added Internal OnRejected) - JWT 401/403 envelope via OnChallenge/OnForbidden - EnvelopeWriter, EnvelopeResult, EnvelopeResults created as single serialization/status-mapping source of truth - ~40 handlers migrated to Response; all endpoint files processed Phase 3 — Dead Result deletion - Deleted: Result.cs, Error.cs, OkApiResult.cs, CreatedApiResult.cs, NoContentApiResult.cs, ResultExtensions.cs, ResultValidationBehavior.cs, ValidationBehavior.cs - All source projects build Phase 4 — Cleanup and hardening - ApplicationErrors → DomainKeys; moved Errors/ → Messages/; namespace CCE.Application.Errors → CCE.Application.Messages - Bulk-replaced 193 ApplicationErrors. references and 51 using directives - Hellang ProblemDetails dependency removed - LoggingBehavior + ValidationBehavior registrations removed - CSP hardened: X-Frame-Options, base-uri, form-action, object-src added - Fixed middleware order comment in External/Program.cs Phase 5 — Build integrity + DomainKeys completeness - Added DomainKeysIntegrityTests (3 tests) in CCE.ArchitectureTests: bidirectional consistency between DomainKeys and SystemCodeMap — all pass - Fixed missing using Microsoft.Extensions.Configuration in EnvelopeWriter.cs - Filled 33 orphaned SystemCodeMap entries as new DomainKeys constants (General, Identity, Content, Verification, Validation, new InterestTopic class) - Added 5 missing SystemCodeMap entries + SystemCode constants (ERR032, CON073–CON076) Phase 6 — Validator hardening + SystemCodeMap completion (code review fixes) - Add .WithErrorCode(MessageKeys.Validation.*) to Login, ForgotPassword, RefreshToken, Logout, UpdateMyProfile, CreateUser, SubmitExpertRequest validators — all were emitting ERR900 on validation failure - Replace all remaining .WithMessage() with .WithErrorCode() - RegisterUserCommandValidator: add RuleLevelCascadeMode = CascadeMode.Stop to prevent double REQUIRED_FIELD + INVALID_FORMAT on empty fields; add .NotEmpty() guards before Password Must() and ConfirmPassword Equal() - Rename MatchStoryPasswordPolicy → MatchStrongPasswordPolicy (typo) - ResetPasswordCommandValidator: add .NotEmpty() guards on NewPassword and ConfirmPassword; update method reference to renamed policy check - Add ERR410 ROLE_NOT_FOUND, ERR411 INVALID_RESET_TOKEN, ERR412 EMAIL_CHANGE_FAILED, ERR127 OTP_UNAUTHORIZED to SystemCode + SystemCodeMap + Resources.yaml - Add CON077–CON082 claims/permissions grant/revoke/update; CON083 AD_LOGIN_SUCCESS; CON084 NEWSLETTER_SUBSCRIBED; CON085 TOPICS_LISTED; CON086 SECTION_REORDERED — all branch feature keys that were falling back to ERR900 - ExceptionHandlingMiddleware: remove ex.Message fallback from UnauthorizedAccessException to prevent internal message leakage - ListRedisKeysQuery: replace raw "ITEMS_LISTED" with MessageKeys.General.ITEMS_LISTED All source projects build with 0 errors, 0 warnings. --- .../Auth/CceJwtAuthRegistration.cs | 24 ++- .../CCE.Api.Common/Auth/DevAuthEndpoints.cs | 15 +- .../src/CCE.Api.Common/CCE.Api.Common.csproj | 1 - .../Extensions/ResponseExtensions.cs | 8 +- .../Extensions/ResultExtensions.cs | 24 --- .../Localization/Resources.yaml | 86 ++++++++++- .../Middleware/ExceptionHandlingMiddleware.cs | 102 ++++--------- .../Middleware/LocalizationMiddleware.cs | 30 ++-- .../Middleware/SecurityHeadersMiddleware.cs | 4 +- .../CceRateLimiterRegistration.cs | 13 ++ .../TieredRateLimiterRegistration.cs | 10 +- .../Results/CreatedApiResponse.cs | 62 -------- .../Results/CreatedApiResult.cs | 51 +++---- .../CCE.Api.Common/Results/EnvelopeResult.cs | 23 +++ .../CCE.Api.Common/Results/EnvelopeResults.cs | 32 ++++ .../CCE.Api.Common/Results/EnvelopeWriter.cs | 69 +++++++++ .../Results/MessageTypeStatusCodes.cs | 18 +++ .../Results/NoContentApiResponse.cs | 62 -------- .../Results/NoContentApiResult.cs | 49 +++--- .../CCE.Api.Common/Results/OkApiResponse.cs | 62 -------- .../src/CCE.Api.Common/Results/OkApiResult.cs | 51 +++---- .../Endpoints/AssetEndpoints.cs | 7 +- .../Endpoints/CategoriesPublicEndpoints.cs | 3 +- .../Endpoints/CommunityPublicEndpoints.cs | 9 +- .../Endpoints/CommunityWriteEndpoints.cs | 15 +- .../Endpoints/CountriesPublicEndpoints.cs | 11 +- .../HomepageSectionsPublicEndpoints.cs | 3 +- .../Endpoints/InteractiveCityEndpoints.cs | 18 ++- .../Endpoints/KapsarcEndpoints.cs | 5 +- .../Endpoints/KnowledgeMapEndpoints.cs | 11 +- .../Endpoints/NotificationsEndpoints.cs | 15 +- .../Endpoints/PagesPublicEndpoints.cs | 5 +- .../Endpoints/ProfileEndpoints.cs | 19 +-- .../Endpoints/ResourcesPublicEndpoints.cs | 11 +- .../Endpoints/SearchEndpoints.cs | 3 +- .../Endpoints/StateRepresentativeEndpoints.cs | 2 +- .../Endpoints/SurveysEndpoints.cs | 5 +- .../Endpoints/UserInterestEndpoints.cs | 7 +- backend/src/CCE.Api.External/Program.cs | 4 +- .../Endpoints/AssetEndpoints.cs | 7 +- .../Endpoints/AuditEndpoints.cs | 3 +- .../Endpoints/CommunityModerationEndpoints.cs | 10 +- .../Endpoints/CountryCodeEndpoints.cs | 9 +- .../Endpoints/CountryEndpoints.cs | 8 +- .../Endpoints/CountryProfileEndpoints.cs | 4 +- .../Endpoints/ExpertEndpoints.cs | 4 +- .../Endpoints/HomepageSectionEndpoints.cs | 19 +-- .../Endpoints/IdentityEndpoints.cs | 2 +- .../Endpoints/PageEndpoints.cs | 19 +-- .../ListAuditEvents/ListAuditEventsQuery.cs | 3 +- .../ListAuditEventsQueryHandler.cs | 14 +- .../Cache/EvictCacheKeyCommandHandler.cs | 4 +- .../Cache/EvictCacheRegionCommandHandler.cs | 4 +- .../Cache/FlushCacheCommandHandler.cs | 4 +- .../Cache/GetCacheRegionsQueryHandler.cs | 4 +- .../Cache/ListRedisKeysQuery.cs | 2 +- .../Common/Behaviors/LoggingBehavior.cs | 38 ----- .../Behaviors/ResponseValidationBehavior.cs | 47 +----- .../Behaviors/ResultValidationBehavior.cs | 82 ---------- .../Common/Behaviors/ValidationBehavior.cs | 40 ----- .../src/CCE.Application/Common/Response.cs | 2 +- backend/src/CCE.Application/Common/Result.cs | 51 ------- .../ApproveJoinRequestCommandHandler.cs | 8 +- .../CastPollVoteCommandHandler.cs | 16 +- ...ChangeCommunityVisibilityCommandHandler.cs | 8 +- .../CreateCommunityCommandHandler.cs | 8 +- .../CreateCommunityCommandValidator.cs | 14 +- .../CreatePost/CreatePostCommandHandler.cs | 16 +- .../CreatePost/CreatePostCommandValidator.cs | 16 +- .../CreateReply/CreateReplyCommandHandler.cs | 12 +- .../CreateTopic/CreateTopicCommandHandler.cs | 4 +- .../DeleteDraft/DeleteDraftCommandHandler.cs | 12 +- .../DeleteTopic/DeleteTopicCommandHandler.cs | 4 +- .../Commands/EditReply/EditReplyCommand.cs | 3 +- .../EditReply/EditReplyCommandHandler.cs | 13 +- .../JoinCommunityCommandHandler.cs | 12 +- .../LeaveCommunityCommandHandler.cs | 6 +- .../MarkPostAnsweredCommand.cs | 3 +- .../MarkPostAnsweredCommandHandler.cs | 13 +- .../PublishPost/PublishPostCommandHandler.cs | 12 +- .../RebuildHotLeaderboardCommandHandler.cs | 4 +- .../RejectJoinRequestCommandHandler.cs | 8 +- .../SetCommunityFollowCommandHandler.cs | 8 +- .../SetPostFollowCommandHandler.cs | 8 +- .../SetTopicFollowCommandHandler.cs | 8 +- .../SetUserFollowCommandHandler.cs | 8 +- .../SoftDeletePost/SoftDeletePostCommand.cs | 3 +- .../SoftDeletePostCommandHandler.cs | 13 +- .../SoftDeleteReply/SoftDeleteReplyCommand.cs | 3 +- .../SoftDeleteReplyCommandHandler.cs | 13 +- .../UpdateCommunityCommandHandler.cs | 8 +- .../UpdateDraft/UpdateDraftCommandHandler.cs | 12 +- .../UpdateDraftCommandValidator.cs | 10 +- .../UpdateTopic/UpdateTopicCommandHandler.cs | 4 +- .../VotePost/VotePostCommandHandler.cs | 8 +- .../VotePost/VotePostCommandValidator.cs | 6 +- .../VoteReply/VoteReplyCommandHandler.cs | 8 +- .../VoteReply/VoteReplyCommandValidator.cs | 6 +- .../GetCommunityBySlugQueryHandler.cs | 8 +- .../GetCommunityRolesQueryHandler.cs | 4 +- .../GetCommunityUserProfileQueryHandler.cs | 6 +- .../GetMyFollows/GetMyFollowsQueryHandler.cs | 4 +- .../GetMyTopics/GetMyTopicsQueryHandler.cs | 6 +- .../GetPollResultsQueryHandler.cs | 8 +- .../GetPostActivityQueryHandler.cs | 8 +- .../GetPostShareLinkQueryHandler.cs | 8 +- .../GetPublicPostByIdQueryHandler.cs | 8 +- .../GetPublicTopicBySlugQueryHandler.cs | 4 +- .../GetReplyThreadQueryHandler.cs | 8 +- .../ListCommunityFeedQueryHandler.cs | 8 +- .../ListExpertLeaderboardQueryHandler.cs | 6 +- .../ListFeaturedPostsQueryHandler.cs | 6 +- .../ListMyDrafts/ListMyDraftsQueryHandler.cs | 4 +- .../ListMyMentionsQueryHandler.cs | 4 +- .../ListPublicCommunitiesQueryHandler.cs | 4 +- .../ListPublicPostRepliesQueryHandler.cs | 4 +- .../ListPublicPostsInTopicQueryHandler.cs | 6 +- .../ListPublicTopicsQueryHandler.cs | 4 +- .../ListPublicTopicsPaginatedQueryHandler.cs | 10 +- .../ListUserFeed/ListUserFeedQueryHandler.cs | 14 +- .../GetTopicById/GetTopicByIdQueryHandler.cs | 4 +- .../ListAdminPosts/ListAdminPostsQuery.cs | 3 +- .../ListAdminPostsQueryHandler.cs | 16 +- .../ListJoinRequestsQueryHandler.cs | 4 +- .../ListTopics/ListTopicsQueryHandler.cs | 4 +- .../CreateEvent/CreateEventCommandHandler.cs | 6 +- .../CreateHomepageSectionCommand.cs | 3 +- .../CreateHomepageSectionCommandHandler.cs | 12 +- .../CreateNews/CreateNewsCommandHandler.cs | 6 +- .../Commands/CreatePage/CreatePageCommand.cs | 3 +- .../CreatePage/CreatePageCommandHandler.cs | 12 +- .../CreateResourceCommandHandler.cs | 6 +- .../CreateResourceCategoryCommandHandler.cs | 4 +- .../DeleteEvent/DeleteEventCommandHandler.cs | 4 +- .../DeleteHomepageSectionCommand.cs | 3 +- .../DeleteHomepageSectionCommandHandler.cs | 23 ++- .../DeleteNews/DeleteNewsCommandHandler.cs | 4 +- .../Commands/DeletePage/DeletePageCommand.cs | 3 +- .../DeletePage/DeletePageCommandHandler.cs | 23 ++- .../DeleteResourceCommandHandler.cs | 4 +- .../DeleteResourceCategoryCommandHandler.cs | 4 +- .../PublishNews/PublishNewsCommandHandler.cs | 4 +- .../PublishResourceCommandHandler.cs | 4 +- .../ReorderHomepageSectionsCommand.cs | 3 +- .../ReorderHomepageSectionsCommandHandler.cs | 12 +- .../RescheduleEventCommandHandler.cs | 4 +- ...bmitCountryContentRequestCommandHandler.cs | 6 +- .../SubscribeNewsletterCommandHandler.cs | 6 +- .../Tags/CreateTag/CreateTagCommandHandler.cs | 4 +- .../Tags/DeleteTag/DeleteTagCommandHandler.cs | 6 +- .../Tags/UpdateTag/UpdateTagCommandHandler.cs | 6 +- .../UpdateEvent/UpdateEventCommandHandler.cs | 6 +- .../UpdateHomepageSectionCommand.cs | 3 +- .../UpdateHomepageSectionCommandHandler.cs | 14 +- .../UpdateNews/UpdateNewsCommandHandler.cs | 6 +- .../Commands/UpdatePage/UpdatePageCommand.cs | 3 +- .../UpdatePage/UpdatePageCommandHandler.cs | 14 +- .../UpdateResourceCommandHandler.cs | 6 +- .../UpdateResourceCategoryCommandHandler.cs | 4 +- .../UploadAsset/UploadAssetCommandHandler.cs | 4 +- .../GetPublicEventByIdQueryHandler.cs | 4 +- .../GetPublicNewsByIdQueryHandler.cs | 4 +- .../GetPublicPageBySlugQuery.cs | 3 +- .../GetPublicPageBySlugQueryHandler.cs | 17 ++- .../GetPublicResourceByIdQueryHandler.cs | 4 +- .../GetShareLink/GetShareLinkQueryHandler.cs | 8 +- .../ListHomepageFeedQueryHandler.cs | 4 +- .../ListPublicEventsQueryHandler.cs | 4 +- .../ListPublicHomepageSectionsQuery.cs | 3 +- .../ListPublicHomepageSectionsQueryHandler.cs | 15 +- .../ListPublicNewsQueryHandler.cs | 4 +- .../ListPublicResourceCategoriesQuery.cs | 3 +- ...istPublicResourceCategoriesQueryHandler.cs | 15 +- .../ListPublicResourcesQueryHandler.cs | 4 +- .../ListPublicTagsQueryHandler.cs | 4 +- .../ListResourceTypesQueryHandler.cs | 4 +- .../DownloadFile/DownloadFileQueryHandler.cs | 6 +- .../GetAssetById/GetAssetByIdQueryHandler.cs | 4 +- .../GetCountryContentRequestQueryHandler.cs | 6 +- .../GetEventById/GetEventByIdQueryHandler.cs | 4 +- .../GetNewsById/GetNewsByIdQueryHandler.cs | 4 +- .../Queries/GetPageById/GetPageByIdQuery.cs | 3 +- .../GetPageById/GetPageByIdQueryHandler.cs | 17 ++- .../GetResourceByIdQueryHandler.cs | 4 +- .../GetResourceCategoryByIdQueryHandler.cs | 4 +- .../ListCountryContentRequestsQueryHandler.cs | 8 +- .../ListEvents/ListEventsQueryHandler.cs | 4 +- .../ListHomepageSectionsQuery.cs | 3 +- .../ListHomepageSectionsQueryHandler.cs | 15 +- .../Queries/ListNews/ListNewsQueryHandler.cs | 4 +- .../Queries/ListPages/ListPagesQuery.cs | 3 +- .../ListPages/ListPagesQueryHandler.cs | 15 +- .../ListResourceCategoriesQueryHandler.cs | 4 +- .../ListResourcesQueryHandler.cs | 4 +- .../Tags/GetTagById/GetTagByIdQueryHandler.cs | 6 +- .../Tags/ListTags/ListTagsQueryHandler.cs | 4 +- .../UpdateCountry/UpdateCountryCommand.cs | 3 +- .../UpdateCountryCommandHandler.cs | 15 +- .../UpsertCountryProfileCommandHandler.cs | 6 +- .../GetCountryById/GetCountryByIdQuery.cs | 3 +- .../GetCountryByIdQueryHandler.cs | 15 +- .../GetCountryProfileQuery.cs | 3 +- .../GetCountryProfileQueryHandler.cs | 15 +- .../GetMyCountryProfileQueryHandler.cs | 4 +- .../ListCountriesQueryHandler.cs | 8 +- .../GetPublicCountryProfileQueryHandler.cs | 6 +- .../ListPublicCountriesQueryHandler.cs | 8 +- .../CCE.Application/DependencyInjection.cs | 3 - .../SubmitEvaluationCommandHandler.cs | 6 +- .../GetAllEvaluationsQueryHandler.cs | 4 +- .../GetEvaluationByIdQueryHandler.cs | 4 +- .../Auth/AdLogin/AdLoginCommandHandler.cs | 4 +- .../Auth/AdLogin/AdLoginCommandValidator.cs | 10 +- .../Identity/Auth/Common/LocalAuthOptions.cs | 8 +- .../ForgotPasswordCommandHandler.cs | 4 +- .../ForgotPasswordCommandValidator.cs | 8 +- .../Auth/Login/LoginCommandHandler.cs | 4 +- .../Auth/Login/LoginCommandValidator.cs | 9 +- .../Auth/Logout/LogoutCommandHandler.cs | 4 +- .../Auth/Logout/LogoutCommandValidator.cs | 4 +- .../RefreshTokenCommandHandler.cs | 6 +- .../RefreshTokenCommandValidator.cs | 4 +- .../Register/RegisterUserCommandHandler.cs | 6 +- .../Register/RegisterUserCommandValidator.cs | 48 ++++-- .../ResetPasswordCommandHandler.cs | 8 +- .../ResetPasswordCommandValidator.cs | 22 ++- .../ApproveExpertRequestCommandHandler.cs | 4 +- .../AssignUserRolesCommandHandler.cs | 4 +- .../ChangeUserStatusCommandHandler.cs | 4 +- .../CreateStateRepAssignmentCommandHandler.cs | 6 +- .../CreateUser/CreateUserCommandHandler.cs | 6 +- .../CreateUser/CreateUserCommandValidator.cs | 27 +++- .../DeleteUser/DeleteUserCommandHandler.cs | 4 +- .../RejectExpertRequestCommandHandler.cs | 4 +- .../RevokeStateRepAssignmentCommandHandler.cs | 6 +- .../GrantRolePermissionsCommandHandler.cs | 6 +- .../Commands/GrantUserClaimsCommandHandler.cs | 6 +- .../RevokeRolePermissionsCommandHandler.cs | 6 +- .../RevokeUserClaimsCommandHandler.cs | 6 +- .../UpsertRolePermissionsCommandHandler.cs | 6 +- .../UpsertUserClaimsCommandHandler.cs | 6 +- .../GetPermissionMatrixQueryHandler.cs | 4 +- .../Queries/GetPermissionsQueryHandler.cs | 4 +- .../Queries/GetUserClaimsQueryHandler.cs | 6 +- .../ConfirmEmailChangeCommandHandler.cs | 8 +- .../ConfirmPhoneChangeCommandHandler.cs | 4 +- .../RequestEmailChangeCommandHandler.cs | 4 +- .../RequestPhoneChangeCommandHandler.cs | 4 +- .../SubmitExpertRequestCommandHandler.cs | 4 +- .../SubmitExpertRequestCommandValidator.cs | 20 ++- .../UpdateMyProfileCommandHandler.cs | 4 +- .../UpdateMyProfileCommandValidator.cs | 24 ++- .../UpsertUserInterestCommandHandler.cs | 6 +- .../GetMyExpertStatusQueryHandler.cs | 4 +- .../GetMyInterestsQueryHandler.cs | 4 +- .../GetMyProfile/GetMyProfileQueryHandler.cs | 4 +- .../GetExpertRequestByIdQueryHandler.cs | 4 +- .../GetUserById/GetUserByIdQueryHandler.cs | 4 +- .../ListExpertProfilesQuery.cs | 3 +- .../ListExpertProfilesQueryHandler.cs | 19 ++- .../ListExpertRequestsQuery.cs | 3 +- .../ListExpertRequestsQueryHandler.cs | 19 ++- .../ListStateRepAssignmentsQuery.cs | 3 +- .../ListStateRepAssignmentsQueryHandler.cs | 19 ++- .../ListUsers/ListUsersQueryHandler.cs | 4 +- .../DeleteMyScenarioCommand.cs | 3 +- .../DeleteMyScenarioCommandHandler.cs | 12 +- .../RunScenario/RunScenarioCommand.cs | 3 +- .../RunScenario/RunScenarioCommandHandler.cs | 24 +-- .../SaveScenario/SaveScenarioCommand.cs | 3 +- .../SaveScenarioCommandHandler.cs | 13 +- .../ListCityTechnologiesQuery.cs | 3 +- .../ListCityTechnologiesQueryHandler.cs | 13 +- .../ListMyScenarios/ListMyScenariosQuery.cs | 3 +- .../ListMyScenariosQueryHandler.cs | 13 +- .../CreateInteractiveMapCommandValidator.cs | 14 +- ...reateInteractiveMapNodeCommandValidator.cs | 20 +-- .../UpdateInteractiveMapCommandValidator.cs | 14 +- ...pdateInteractiveMapNodeCommandValidator.cs | 18 +-- .../GetInteractiveMapBySlugQueryHandler.cs | 4 +- ...etInteractiveMapNodeDetailsQueryHandler.cs | 4 +- .../ListInteractiveMapsQueryHandler.cs | 4 +- .../GetInteractiveMapByIdQueryHandler.cs | 4 +- .../ListInteractiveMapNodesQueryHandler.cs | 4 +- .../ListInteractiveMapsQueryHandler.cs | 4 +- .../GetInterestQuestionsQueryHandler.cs | 4 +- .../ListInterestTopicsQueryHandler.cs | 4 +- .../GetLatestKapsarcSnapshotQuery.cs | 3 +- .../GetLatestKapsarcSnapshotQueryHandler.cs | 15 +- .../GetKnowledgeMapByIdQuery.cs | 3 +- .../GetKnowledgeMapByIdQueryHandler.cs | 18 ++- .../ListKnowledgeMapEdgesQuery.cs | 3 +- .../ListKnowledgeMapEdgesQueryHandler.cs | 16 +- .../ListKnowledgeMapNodesQuery.cs | 3 +- .../ListKnowledgeMapNodesQueryHandler.cs | 16 +- .../ListKnowledgeMapsQuery.cs | 3 +- .../ListKnowledgeMapsQueryHandler.cs | 16 +- .../UpsertCountryCodeCommandHandler.cs | 6 +- .../GetCountryCodeByIdQueryHandler.cs | 4 +- .../ListCountryCodesQueryHandler.cs | 4 +- .../DeleteMedia/DeleteMediaCommandHandler.cs | 4 +- .../UpdateMediaMetadataCommandHandler.cs | 4 +- .../UploadMedia/UploadMediaCommandHandler.cs | 4 +- .../GetMediaById/GetMediaByIdQueryHandler.cs | 4 +- .../Messages/MessageFactory.cs | 144 +++++++++--------- .../MessageKeys.cs} | 61 +++++++- .../CCE.Application/Messages/SystemCode.cs | 23 +++ .../CCE.Application/Messages/SystemCodeMap.cs | 27 ++++ .../GetNotificationLogByIdQueryHandler.cs | 4 +- .../ListNotificationLogsQueryHandler.cs | 4 +- ...RegistrationApprovedNotificationHandler.cs | 3 +- ...RegistrationRejectedNotificationHandler.cs | 3 +- .../GetMyNotificationSettingsQueryHandler.cs | 4 +- .../GetMyUnreadCountQueryHandler.cs | 4 +- .../ListMyNotificationsQueryHandler.cs | 4 +- ...GetNotificationTemplateByIdQueryHandler.cs | 4 +- .../ListNotificationTemplatesQueryHandler.cs | 4 +- .../CreateGlossaryEntryCommandHandler.cs | 4 +- .../CreateKnowledgePartnerCommandHandler.cs | 4 +- .../CreatePolicySectionCommandHandler.cs | 4 +- .../DeleteGlossaryEntryCommandHandler.cs | 4 +- .../DeleteKnowledgePartnerCommandHandler.cs | 4 +- .../DeletePolicySectionCommandHandler.cs | 4 +- .../ReorderPolicySectionCommandHandler.cs | 4 +- .../UpdateAboutSettingsCommandHandler.cs | 4 +- .../UpdateGlossaryEntryCommandHandler.cs | 4 +- .../UpdateHomepageSettingsCommandHandler.cs | 4 +- .../UpdateKnowledgePartnerCommandHandler.cs | 4 +- .../UpdatePolicySectionCommandHandler.cs | 4 +- .../GetPublicAboutSettingsQueryHandler.cs | 4 +- .../GetPublicHomepageQueryHandler.cs | 4 +- .../GetPublicPoliciesSettingsQueryHandler.cs | 4 +- .../GetAboutSettingsQueryHandler.cs | 4 +- .../GetHomepageSettingsQueryHandler.cs | 4 +- .../GetPoliciesSettingsQueryHandler.cs | 4 +- .../Search/Queries/SearchQuery.cs | 3 +- .../Search/Queries/SearchQueryHandler.cs | 14 +- .../SubmitServiceRatingCommand.cs | 3 +- .../SubmitServiceRatingCommandHandler.cs | 13 +- .../RequestVerificationCommandHandler.cs | 4 +- .../VerifyOtp/VerifyOtpCommandHandler.cs | 4 +- .../src/CCE.Domain/Common/DomainException.cs | 3 +- backend/src/CCE.Domain/Common/Error.cs | 23 --- .../ExceptionHandlingMiddlewareTests.cs | 67 +++++++- .../UpdateCountryCommandHandlerTests.cs | 32 ++-- .../GetCountryByIdQueryHandlerTests.cs | 39 +++-- .../GetCountryProfileQueryHandlerTests.cs | 43 ++++-- .../ListExpertProfilesQueryHandlerTests.cs | 27 ++-- .../ListExpertRequestsQueryHandlerTests.cs | 36 +++-- ...istStateRepAssignmentsQueryHandlerTests.cs | 37 +++-- .../MessageKeysIntegrityTests.cs | 83 ++++++++++ 351 files changed, 2165 insertions(+), 1753 deletions(-) delete mode 100644 backend/src/CCE.Api.Common/Extensions/ResultExtensions.cs delete mode 100644 backend/src/CCE.Api.Common/Results/CreatedApiResponse.cs create mode 100644 backend/src/CCE.Api.Common/Results/EnvelopeResult.cs create mode 100644 backend/src/CCE.Api.Common/Results/EnvelopeResults.cs create mode 100644 backend/src/CCE.Api.Common/Results/EnvelopeWriter.cs create mode 100644 backend/src/CCE.Api.Common/Results/MessageTypeStatusCodes.cs delete mode 100644 backend/src/CCE.Api.Common/Results/NoContentApiResponse.cs delete mode 100644 backend/src/CCE.Api.Common/Results/OkApiResponse.cs delete mode 100644 backend/src/CCE.Application/Common/Behaviors/LoggingBehavior.cs delete mode 100644 backend/src/CCE.Application/Common/Behaviors/ResultValidationBehavior.cs delete mode 100644 backend/src/CCE.Application/Common/Behaviors/ValidationBehavior.cs delete mode 100644 backend/src/CCE.Application/Common/Result.cs rename backend/src/CCE.Application/{Errors/ApplicationErrors.cs => Messages/MessageKeys.cs} (74%) delete mode 100644 backend/src/CCE.Domain/Common/Error.cs create mode 100644 backend/tests/CCE.ArchitectureTests/MessageKeysIntegrityTests.cs diff --git a/backend/src/CCE.Api.Common/Auth/CceJwtAuthRegistration.cs b/backend/src/CCE.Api.Common/Auth/CceJwtAuthRegistration.cs index f61d0b36..28080699 100644 --- a/backend/src/CCE.Api.Common/Auth/CceJwtAuthRegistration.cs +++ b/backend/src/CCE.Api.Common/Auth/CceJwtAuthRegistration.cs @@ -1,8 +1,11 @@ -using System.Text; +using System.Text; +using CCE.Api.Common.Results; using CCE.Application.Identity.Auth.Common; +using CCE.Application.Messages; using CCE.Infrastructure.Identity; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; @@ -65,6 +68,8 @@ public static IServiceCollection AddCceJwtAuth( }; // SignalR browser WebSocket clients can't set the Authorization header — they pass the JWT // via ?access_token=. Accept it for hub requests so the hub authenticates over WebSockets. + // OnChallenge/OnForbidden write the standard CCE error envelope instead of the default + // empty 401/403 body, keeping the WWW-Authenticate header on 401 (RFC 6750). jwt.Events = new JwtBearerEvents { OnMessageReceived = context => @@ -76,6 +81,23 @@ public static IServiceCollection AddCceJwtAuth( context.Token = accessToken; } return Task.CompletedTask; + }, + OnChallenge = async context => + { + context.HandleResponse(); + context.Response.Headers.WWWAuthenticate = "Bearer"; + await EnvelopeWriter.WriteAsync( + context.HttpContext, + StatusCodes.Status401Unauthorized, + MessageKeys.General.UNAUTHORIZED, + context.AuthenticateFailure?.Message).ConfigureAwait(false); + }, + OnForbidden = async context => + { + await EnvelopeWriter.WriteAsync( + context.HttpContext, + StatusCodes.Status403Forbidden, + MessageKeys.General.FORBIDDEN).ConfigureAwait(false); } }; }); diff --git a/backend/src/CCE.Api.Common/Auth/DevAuthEndpoints.cs b/backend/src/CCE.Api.Common/Auth/DevAuthEndpoints.cs index fdd71c3e..c8fad951 100644 --- a/backend/src/CCE.Api.Common/Auth/DevAuthEndpoints.cs +++ b/backend/src/CCE.Api.Common/Auth/DevAuthEndpoints.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using HttpResults = Microsoft.AspNetCore.Http.Results; namespace CCE.Api.Common.Auth; @@ -28,7 +29,7 @@ public static IEndpointRouteBuilder MapDevAuthEndpoints(this IEndpointRouteBuild var roleValue = role ?? "cce-admin"; if (!DevAuthHandler.RoleToUserId.ContainsKey(roleValue)) { - return Results.BadRequest(new + return HttpResults.BadRequest(new { error = $"Unknown dev role '{roleValue}'.", validRoles = DevAuthHandler.RoleToUserId.Keys, @@ -47,15 +48,15 @@ public static IEndpointRouteBuilder MapDevAuthEndpoints(this IEndpointRouteBuild // Redirect to returnUrl if relative + safe; else home. if (!string.IsNullOrEmpty(returnUrl) && returnUrl.StartsWith('/')) { - return Results.Redirect(returnUrl); + return HttpResults.Redirect(returnUrl); } - return Results.Redirect("/"); + return HttpResults.Redirect("/"); }).AllowAnonymous().WithName("DevSignIn"); dev.MapPost("/sign-out", (HttpContext ctx) => { ctx.Response.Cookies.Delete(DevAuthHandler.DevCookieName); - return Results.Ok(new { signedOut = true }); + return HttpResults.Ok(new { signedOut = true }); }).AllowAnonymous().WithName("DevSignOut"); dev.MapGet("/whoami", (HttpContext ctx) => @@ -63,7 +64,7 @@ public static IEndpointRouteBuilder MapDevAuthEndpoints(this IEndpointRouteBuild var name = ctx.User.Identity?.Name ?? "(anonymous)"; var roles = ctx.User.FindAll("roles").Select(c => c.Value).ToArray(); var sub = ctx.User.FindFirst("sub")?.Value ?? "(none)"; - return Results.Ok(new { name, sub, roles }); + return HttpResults.Ok(new { name, sub, roles }); }).AllowAnonymous().WithName("DevWhoAmI"); // ─── Frontend-compat shims at /auth/* ─────────────────────────── @@ -82,13 +83,13 @@ public static IEndpointRouteBuilder MapDevAuthEndpoints(this IEndpointRouteBuild } var rurl = string.IsNullOrEmpty(returnUrl) || !returnUrl.StartsWith('/') ? "/" : returnUrl; var target = $"/dev/sign-in?role={Uri.EscapeDataString(defaultRole)}&returnUrl={Uri.EscapeDataString(rurl)}"; - return Results.Redirect(target); + return HttpResults.Redirect(target); }).AllowAnonymous().WithName("AuthLoginShim"); app.MapPost("/auth/logout", (HttpContext ctx) => { ctx.Response.Cookies.Delete(DevAuthHandler.DevCookieName); - return Results.Ok(new { signedOut = true }); + return HttpResults.Ok(new { signedOut = true }); }).AllowAnonymous().WithName("AuthLogoutShim"); return app; diff --git a/backend/src/CCE.Api.Common/CCE.Api.Common.csproj b/backend/src/CCE.Api.Common/CCE.Api.Common.csproj index 05aba267..859a77a7 100644 --- a/backend/src/CCE.Api.Common/CCE.Api.Common.csproj +++ b/backend/src/CCE.Api.Common/CCE.Api.Common.csproj @@ -28,7 +28,6 @@ - diff --git a/backend/src/CCE.Api.Common/Extensions/ResponseExtensions.cs b/backend/src/CCE.Api.Common/Extensions/ResponseExtensions.cs index 1d51aedc..69f3f8fe 100644 --- a/backend/src/CCE.Api.Common/Extensions/ResponseExtensions.cs +++ b/backend/src/CCE.Api.Common/Extensions/ResponseExtensions.cs @@ -1,4 +1,4 @@ -using CCE.Api.Common.HttpResults; +using CCE.Api.Common.Results; using CCE.Application.Common; using CCE.Domain.Common; using Microsoft.AspNetCore.Http; @@ -11,14 +11,14 @@ public static class ResponseExtensions /// Maps a to a typed with correct HTTP status, /// injecting traceId and timestamp, and registering Swashbuckle metadata. /// - public static OkApiResponse ToHttpResult(this Response response) + public static OkApiResult ToHttpResult(this Response response) => new(response); /// Shorthand for 201 Created with Swashbuckle metadata. - public static CreatedApiResponse ToCreatedHttpResult(this Response response) + public static CreatedApiResult ToCreatedHttpResult(this Response response) => new(response); /// Shorthand for 204 No Content with Swashbuckle metadata. - public static NoContentApiResponse ToNoContentHttpResult(this Response response) + public static NoContentApiResult ToNoContentHttpResult(this Response response) => new(response); } diff --git a/backend/src/CCE.Api.Common/Extensions/ResultExtensions.cs b/backend/src/CCE.Api.Common/Extensions/ResultExtensions.cs deleted file mode 100644 index d6a98b05..00000000 --- a/backend/src/CCE.Api.Common/Extensions/ResultExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using CCE.Api.Common.HttpResults; -using CCE.Application.Common; -using CCE.Domain.Common; -using Microsoft.AspNetCore.Http; - -namespace CCE.Api.Common.Extensions; - -public static class ResultExtensions -{ - /// - /// Maps a to a typed with the correct HTTP status - /// and registers Swashbuckle metadata. - /// - public static OkApiResult ToHttpResult(this Result result) - => new(result); - - /// Shorthand for 201 Created with Swashbuckle metadata. - public static CreatedApiResult ToCreatedHttpResult(this Result result) - => new(result); - - /// Shorthand for 204 No Content (void commands) with Swashbuckle metadata. - public static NoContentApiResult ToNoContentHttpResult(this Result result) - => new(result); -} diff --git a/backend/src/CCE.Api.Common/Localization/Resources.yaml b/backend/src/CCE.Api.Common/Localization/Resources.yaml index 29e1ecfa..5a0584d7 100644 --- a/backend/src/CCE.Api.Common/Localization/Resources.yaml +++ b/backend/src/CCE.Api.Common/Localization/Resources.yaml @@ -125,8 +125,8 @@ INTERNAL_ERROR: en: "An unexpected error occurred" BAD_REQUEST: - ar: "عذرًا، البيانات المدخلة غير صحيحة" - en: "Sorry, the entered data is invalid" + ar: "تعذر معالجة الطلب" + en: "The request could not be processed" RESOURCE_NOT_FOUND_GENERIC: ar: "المورد غير موجود" @@ -140,6 +140,14 @@ DUPLICATE_VALUE: ar: "القيمة موجودة بالفعل" en: "Value already exists" +RATE_LIMIT_EXCEEDED: + ar: "تم تجاوز الحد المسموح من الطلبات. يرجى المحاولة مرة أخرى لاحقًا" + en: "Too many requests. Please try again later." + +BUSINESS_RULE_VIOLATION: + ar: "تعذر إتمام العملية بسبب مخالفة قاعدة العمل" + en: "The operation could not be completed due to a business rule violation." + SCENARIO_NOT_FOUND: ar: "السيناريو غير موجود" en: "Scenario not found" @@ -658,6 +666,14 @@ PASSWORD_NUMBER: ar: "يجب أن تحتوي كلمة المرور على رقم على الأقل" en: "Password must contain at least one number" +PASSWORD_POLICY: + ar: "كلمة المرور يجب أن تتراوح بين 12 و20 حرفاً وتحتوي على أحرف كبيرة وصغيرة وأرقام" + en: "Password must be 12-20 characters and contain uppercase, lowercase, and numbers" + +PASSWORDS_MUST_MATCH: + ar: "كلمتا المرور غير متطابقتين" + en: "Passwords do not match" + # ─── General Errors ─── EXTERNAL_API_ERROR: @@ -725,3 +741,69 @@ INTERACTIVE_MAP_NODE_DELETED: ar: "تم حذف العنصر من الخريطة التفاعلية بنجاح" en: "Interactive map node deleted successfully" +# ─── Identity / Permissions / Claims ─── + +ROLE_NOT_FOUND: + ar: "الدور غير موجود" + en: "Role not found" + +INVALID_RESET_TOKEN: + ar: "رمز إعادة تعيين كلمة المرور غير صالح أو منتهي الصلاحية" + en: "Password reset token is invalid or expired" + +EMAIL_CHANGE_FAILED: + ar: "حدث خطأ أثناء تغيير البريد الإلكتروني" + en: "An error occurred while changing the email address" + +AD_LOGIN_SUCCESS: + ar: "تم تسجيل الدخول عبر Active Directory بنجاح" + en: "Logged in via Active Directory successfully" + +PERMISSIONS_GRANTED: + ar: "تم منح الصلاحيات للدور بنجاح" + en: "Permissions granted to role successfully" + +PERMISSIONS_REVOKED: + ar: "تم سحب الصلاحيات من الدور بنجاح" + en: "Permissions revoked from role successfully" + +PERMISSIONS_UPDATED: + ar: "تم تحديث صلاحيات الدور بنجاح" + en: "Role permissions updated successfully" + +CLAIMS_GRANTED: + ar: "تم منح المطالبات للمستخدم بنجاح" + en: "Claims granted to user successfully" + +CLAIMS_REVOKED: + ar: "تم سحب المطالبات من المستخدم بنجاح" + en: "Claims revoked from user successfully" + +USER_CLAIMS_UPDATED: + ar: "تم تحديث مطالبات المستخدم بنجاح" + en: "User claims updated successfully" + +# ─── Verification ─── + +OTP_UNAUTHORIZED: + ar: "غير مصرح لك بالتحقق من هذا الرمز" + en: "You are not authorized to verify this code" + +# ─── Content / Community / Platform Settings ─── + +TAG_NOT_FOUND: + ar: "الوسم غير موجود" + en: "Tag not found" + +NEWSLETTER_SUBSCRIBED: + ar: "تم الاشتراك في النشرة البريدية بنجاح" + en: "Subscribed to newsletter successfully" + +TOPICS_LISTED: + ar: "تم جلب الموضوعات بنجاح" + en: "Topics listed successfully" + +SECTION_REORDERED: + ar: "تم إعادة ترتيب الأقسام بنجاح" + en: "Sections reordered successfully" + diff --git a/backend/src/CCE.Api.Common/Middleware/ExceptionHandlingMiddleware.cs b/backend/src/CCE.Api.Common/Middleware/ExceptionHandlingMiddleware.cs index 354e17b3..2dec20f2 100644 --- a/backend/src/CCE.Api.Common/Middleware/ExceptionHandlingMiddleware.cs +++ b/backend/src/CCE.Api.Common/Middleware/ExceptionHandlingMiddleware.cs @@ -1,14 +1,13 @@ -using CCE.Application.Common; +using CCE.Api.Common.Results; using CCE.Application.Localization; using CCE.Application.Messages; using CCE.Domain.Common; using FluentValidation; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using System.Diagnostics; using System.Text.Json; -using System.Text.Json.Serialization; namespace CCE.Api.Common.Middleware; @@ -29,109 +28,72 @@ public async Task InvokeAsync(HttpContext context) { await _next(context).ConfigureAwait(false); } + catch (OperationCanceledException) + { + // Client disconnected — not a server error. + } catch (ValidationException ex) { await WriteValidationResultAsync(context, ex).ConfigureAwait(false); } catch (ConcurrencyException ex) { - await WriteErrorAsync(context, StatusCodes.Status409Conflict, - "CONCURRENCY_CONFLICT", MessageType.Conflict, ex.Message).ConfigureAwait(false); + await EnvelopeWriter.WriteAsync(context, StatusCodes.Status409Conflict, + MessageKeys.General.CONCURRENCY_CONFLICT, ex.Message).ConfigureAwait(false); } catch (DuplicateException ex) { - await WriteErrorAsync(context, StatusCodes.Status409Conflict, - "DUPLICATE_VALUE", MessageType.Conflict, ex.Message).ConfigureAwait(false); + await EnvelopeWriter.WriteAsync(context, StatusCodes.Status409Conflict, + MessageKeys.General.DUPLICATE_VALUE, ex.Message).ConfigureAwait(false); } catch (DomainException ex) { - await WriteErrorAsync(context, StatusCodes.Status400BadRequest, - "BAD_REQUEST", MessageType.BusinessRule, ex.Message).ConfigureAwait(false); + await EnvelopeWriter.WriteAsync(context, StatusCodes.Status422UnprocessableEntity, + MessageKeys.General.BUSINESS_RULE_VIOLATION, ex.Message).ConfigureAwait(false); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogInformation(ex, "Unauthorized access"); + await EnvelopeWriter.WriteAsync(context, StatusCodes.Status401Unauthorized, + MessageKeys.General.UNAUTHORIZED).ConfigureAwait(false); } catch (System.Collections.Generic.KeyNotFoundException ex) { - await WriteErrorAsync(context, StatusCodes.Status404NotFound, - "RESOURCE_NOT_FOUND_GENERIC", MessageType.NotFound, ex.Message).ConfigureAwait(false); + await EnvelopeWriter.WriteAsync(context, StatusCodes.Status404NotFound, + MessageKeys.General.RESOURCE_NOT_FOUND_GENERIC, ex.Message).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "Unhandled exception"); - await WriteErrorAsync(context, StatusCodes.Status500InternalServerError, - "INTERNAL_ERROR", MessageType.Internal, null).ConfigureAwait(false); + await EnvelopeWriter.WriteAsync(context, StatusCodes.Status500InternalServerError, + MessageKeys.General.INTERNAL_ERROR).ConfigureAwait(false); } } - private static async Task WriteErrorAsync( - HttpContext ctx, int statusCode, string domainKey, MessageType type, string? fallbackMessage) - { - var l = ctx.RequestServices.GetService(); - var msg = l?.GetString(domainKey) ?? fallbackMessage ?? "خطأ"; - var code = SystemCodeMap.ToSystemCode(domainKey); - - var envelope = new - { - success = false, - code, - message = msg, - data = (object?)null, - errors = Array.Empty(), - traceId = Activity.Current?.Id ?? ctx.TraceIdentifier, - timestamp = DateTimeOffset.UtcNow, - }; - - ctx.Response.StatusCode = statusCode; - ctx.Response.ContentType = "application/json"; - await JsonSerializer.SerializeAsync(ctx.Response.Body, envelope, JsonOptions) - .ConfigureAwait(false); - } - private static async Task WriteValidationResultAsync(HttpContext ctx, ValidationException ex) { var l = ctx.RequestServices.GetService(); - var headerMsg = l?.GetString("VALIDATION_ERROR") ?? "عذرًا، البيانات المدخلة غير صحيحة"; - var headerCode = SystemCodeMap.ToSystemCode("VALIDATION_ERROR"); + var config = ctx.RequestServices.GetService(); + var supported = config?.GetSection("Localization:Supported").Get(); + var defaultLocale = config?.GetValue("Localization:Default"); + var locale = LocalizationMiddleware.PickLocale( + ctx.Request.Headers.AcceptLanguage.ToString(), supported, defaultLocale); var fieldErrors = ex.Errors.Select(e => { var domainKey = e.ErrorCode; var valCode = SystemCodeMap.ToSystemCode(domainKey); - var valMsg = l?.GetString(domainKey) ?? domainKey; + var valMsg = l?.GetString(domainKey, locale) ?? domainKey; if (valMsg == domainKey) valMsg = e.ErrorMessage; return new { - field = ToCamelCase(e.PropertyName), + field = JsonNamingPolicy.CamelCase.ConvertName(e.PropertyName ?? string.Empty), code = valCode, message = valMsg }; - }).ToList(); + }).ToList(); - var envelope = new - { - success = false, - code = headerCode, - message = headerMsg, - data = (object?)null, - errors = fieldErrors, - traceId = Activity.Current?.Id ?? ctx.TraceIdentifier, - timestamp = DateTimeOffset.UtcNow, - }; - - ctx.Response.StatusCode = StatusCodes.Status400BadRequest; - ctx.Response.ContentType = "application/json"; - await JsonSerializer.SerializeAsync(ctx.Response.Body, envelope, JsonOptions) - .ConfigureAwait(false); + await EnvelopeWriter.WriteAsync(ctx, StatusCodes.Status400BadRequest, + MessageKeys.General.VALIDATION_ERROR, errors: fieldErrors).ConfigureAwait(false); } - - private static string ToCamelCase(string name) - { - if (string.IsNullOrEmpty(name)) return name; - return char.ToLowerInvariant(name[0]) + name[1..]; - } - - private static readonly JsonSerializerOptions JsonOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } - }; } diff --git a/backend/src/CCE.Api.Common/Middleware/LocalizationMiddleware.cs b/backend/src/CCE.Api.Common/Middleware/LocalizationMiddleware.cs index b7c6d1a8..e307b8f8 100644 --- a/backend/src/CCE.Api.Common/Middleware/LocalizationMiddleware.cs +++ b/backend/src/CCE.Api.Common/Middleware/LocalizationMiddleware.cs @@ -1,20 +1,25 @@ using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using System.Globalization; namespace CCE.Api.Common.Middleware; public sealed class LocalizationMiddleware { - private static readonly string[] Supported = ["ar", "en"]; - private const string DefaultLocale = "ar"; - + private readonly string[] _supported; + private readonly string _defaultLocale; private readonly RequestDelegate _next; - public LocalizationMiddleware(RequestDelegate next) => _next = next; + public LocalizationMiddleware(RequestDelegate next, IConfiguration? configuration = null) + { + _next = next; + _supported = configuration?.GetSection("Localization:Supported").Get() ?? ["ar", "en"]; + _defaultLocale = configuration?.GetValue("Localization:Default") ?? "ar"; + } public async Task InvokeAsync(HttpContext context) { - var locale = PickLocale(context.Request.Headers.AcceptLanguage.ToString()); + var locale = PickLocale(context.Request.Headers.AcceptLanguage.ToString(), _supported, _defaultLocale); var culture = CultureInfo.GetCultureInfo(locale); var prevCulture = CultureInfo.CurrentCulture; @@ -32,23 +37,24 @@ public async Task InvokeAsync(HttpContext context) } } - private static string PickLocale(string acceptLanguage) + internal static string PickLocale(string acceptLanguage, string[]? supported = null, string? defaultLocale = null) { + supported ??= ["ar", "en"]; + defaultLocale ??= "ar"; + if (string.IsNullOrWhiteSpace(acceptLanguage)) { - return DefaultLocale; + return defaultLocale; } - // Parse comma-separated entries, trim quality factors, take first matching supported tag. foreach (var entry in acceptLanguage.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { var tag = entry.Split(';', 2)[0].Trim(); - // "en-US" -> "en" var primary = tag.Split('-', 2)[0].ToLowerInvariant(); - if (Array.IndexOf(Supported, primary) >= 0) + if (Array.IndexOf(supported, primary) >= 0) { return primary; } } - return DefaultLocale; + return defaultLocale; } -} +} \ No newline at end of file diff --git a/backend/src/CCE.Api.Common/Middleware/SecurityHeadersMiddleware.cs b/backend/src/CCE.Api.Common/Middleware/SecurityHeadersMiddleware.cs index 9c7457a1..64c1198a 100644 --- a/backend/src/CCE.Api.Common/Middleware/SecurityHeadersMiddleware.cs +++ b/backend/src/CCE.Api.Common/Middleware/SecurityHeadersMiddleware.cs @@ -20,12 +20,14 @@ public async Task InvokeAsync(HttpContext context) { var h = context.Response.Headers; h["X-Content-Type-Options"] = "nosniff"; + h["X-Frame-Options"] = "DENY"; h["Referrer-Policy"] = "strict-origin-when-cross-origin"; h["Permissions-Policy"] = "camera=(), microphone=(), geolocation=(), payment=()"; h["Cross-Origin-Opener-Policy"] = "same-origin"; h["Content-Security-Policy"] = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " + - "img-src 'self' data:; connect-src 'self'; frame-ancestors 'none';"; + "img-src 'self' data:; connect-src 'self'; " + + "frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none';"; if (_hstsEnabled) { h["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"; diff --git a/backend/src/CCE.Api.Common/RateLimiting/CceRateLimiterRegistration.cs b/backend/src/CCE.Api.Common/RateLimiting/CceRateLimiterRegistration.cs index 7aa9370c..37a625e1 100644 --- a/backend/src/CCE.Api.Common/RateLimiting/CceRateLimiterRegistration.cs +++ b/backend/src/CCE.Api.Common/RateLimiting/CceRateLimiterRegistration.cs @@ -1,3 +1,5 @@ +using CCE.Api.Common.Results; +using CCE.Application.Messages; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.RateLimiting; @@ -36,6 +38,17 @@ private static IServiceCollection AddCceRateLimiterCore(IServiceCollection servi services.AddRateLimiter(opts => { opts.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + opts.OnRejected = async (context, ct) => + { + if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) + { + context.HttpContext.Response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString(System.Globalization.CultureInfo.InvariantCulture); + } + await EnvelopeWriter.WriteAsync( + context.HttpContext, + StatusCodes.Status429TooManyRequests, + MessageKeys.General.RATE_LIMIT_EXCEEDED).ConfigureAwait(false); + }; opts.GlobalLimiter = PartitionedRateLimiter.Create(ctx => RateLimitPartition.GetFixedWindowLimiter( partitionKey: ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown", diff --git a/backend/src/CCE.Api.Common/RateLimiting/TieredRateLimiterRegistration.cs b/backend/src/CCE.Api.Common/RateLimiting/TieredRateLimiterRegistration.cs index 68b77a1c..4632d61a 100644 --- a/backend/src/CCE.Api.Common/RateLimiting/TieredRateLimiterRegistration.cs +++ b/backend/src/CCE.Api.Common/RateLimiting/TieredRateLimiterRegistration.cs @@ -1,5 +1,7 @@ -using System.Threading.RateLimiting; +using System.Threading.RateLimiting; using CCE.Api.Common.Auth; +using CCE.Application.Messages; +using CCE.Api.Common.Results; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.RateLimiting; @@ -30,8 +32,10 @@ public static IServiceCollection AddCceTieredRateLimiter(this IServiceCollection { context.HttpContext.Response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString(System.Globalization.CultureInfo.InvariantCulture); } - context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; - await context.HttpContext.Response.WriteAsync("Too many requests", ct).ConfigureAwait(false); + await EnvelopeWriter.WriteAsync( + context.HttpContext, + StatusCodes.Status429TooManyRequests, + MessageKeys.General.RATE_LIMIT_EXCEEDED).ConfigureAwait(false); }; o.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => diff --git a/backend/src/CCE.Api.Common/Results/CreatedApiResponse.cs b/backend/src/CCE.Api.Common/Results/CreatedApiResponse.cs deleted file mode 100644 index 219eec2c..00000000 --- a/backend/src/CCE.Api.Common/Results/CreatedApiResponse.cs +++ /dev/null @@ -1,62 +0,0 @@ -using CCE.Application.Common; -using CCE.Domain.Common; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Metadata; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace CCE.Api.Common.HttpResults; - -/// -/// Typed that wraps a created envelope -/// and registers 201 Created response metadata for Swashbuckle. -/// -[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Required by IEndpointMetadataProvider interface.")] -[SuppressMessage("Design", "CA1815:Override equals and operator equals on value types", Justification = "Result wrapper is never compared.")] -public readonly struct CreatedApiResponse : IResult, IEndpointMetadataProvider -{ - private readonly Response _payload; - - public CreatedApiResponse(Response payload) => _payload = payload; - - public Task ExecuteAsync(HttpContext httpContext) - { - var stamped = _payload with - { - TraceId = Activity.Current?.Id ?? string.Empty, - Timestamp = DateTimeOffset.UtcNow, - }; - - if (stamped.Success) - { - return Results.Json(stamped, statusCode: StatusCodes.Status201Created).ExecuteAsync(httpContext); - } - - var statusCode = stamped.Type switch - { - MessageType.NotFound => StatusCodes.Status404NotFound, - MessageType.Validation => StatusCodes.Status400BadRequest, - MessageType.Conflict => StatusCodes.Status409Conflict, - MessageType.Unauthorized => StatusCodes.Status401Unauthorized, - MessageType.Forbidden => StatusCodes.Status403Forbidden, - MessageType.BusinessRule => StatusCodes.Status422UnprocessableEntity, - _ => StatusCodes.Status500InternalServerError, - }; - - return Results.Json(stamped, statusCode: statusCode).ExecuteAsync(httpContext); - } - - public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) - { - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status201Created, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status401Unauthorized, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status403Forbidden, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status500InternalServerError, typeof(Response), ["application/json"])); - } -} diff --git a/backend/src/CCE.Api.Common/Results/CreatedApiResult.cs b/backend/src/CCE.Api.Common/Results/CreatedApiResult.cs index fe297d4a..4ab14878 100644 --- a/backend/src/CCE.Api.Common/Results/CreatedApiResult.cs +++ b/backend/src/CCE.Api.Common/Results/CreatedApiResult.cs @@ -1,5 +1,4 @@ -using CCE.Application.Common; -using CCE.Domain.Common; +using CCE.Application.Common; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; @@ -7,50 +6,48 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; -namespace CCE.Api.Common.HttpResults; +namespace CCE.Api.Common.Results; /// -/// Typed that wraps a created envelope +/// Typed that wraps a created envelope /// and registers 201 Created response metadata for Swashbuckle. /// [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Required by IEndpointMetadataProvider interface.")] [SuppressMessage("Design", "CA1815:Override equals and operator equals on value types", Justification = "Result wrapper is never compared.")] public readonly struct CreatedApiResult : IResult, IEndpointMetadataProvider { - private readonly Result _payload; + private readonly Response _payload; - public CreatedApiResult(Result payload) => _payload = payload; + public CreatedApiResult(Response payload) => _payload = payload; public Task ExecuteAsync(HttpContext httpContext) { - if (_payload.IsSuccess) - { - return Results.Json(_payload, statusCode: StatusCodes.Status201Created).ExecuteAsync(httpContext); - } + var correlationId = httpContext.Items.TryGetValue(Middleware.CorrelationIdMiddleware.ItemKey, out var cid) + ? cid?.ToString() ?? string.Empty + : string.Empty; - var statusCode = _payload.Error!.Type switch + var stamped = _payload with { - ErrorType.NotFound => StatusCodes.Status404NotFound, - ErrorType.Validation => StatusCodes.Status400BadRequest, - ErrorType.Conflict => StatusCodes.Status409Conflict, - ErrorType.Unauthorized => StatusCodes.Status401Unauthorized, - ErrorType.Forbidden => StatusCodes.Status403Forbidden, - ErrorType.BusinessRule => StatusCodes.Status422UnprocessableEntity, - _ => StatusCodes.Status500InternalServerError, + TraceId = Activity.Current?.Id ?? string.Empty, + CorrelationId = correlationId, + Timestamp = DateTimeOffset.UtcNow, }; - return Results.Json(_payload, statusCode: statusCode).ExecuteAsync(httpContext); + if (stamped.Success) + return TypedResults.Json(stamped, options: EnvelopeWriter.JsonOptions, statusCode: StatusCodes.Status201Created).ExecuteAsync(httpContext); + + return TypedResults.Json(stamped, options: EnvelopeWriter.JsonOptions, statusCode: MessageTypeStatusCodes.ToStatusCode(stamped.Type)).ExecuteAsync(httpContext); } public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status201Created, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status401Unauthorized, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status403Forbidden, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status500InternalServerError, typeof(Result), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status201Created, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status401Unauthorized, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status403Forbidden, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status500InternalServerError, typeof(Response), ["application/json"])); } } diff --git a/backend/src/CCE.Api.Common/Results/EnvelopeResult.cs b/backend/src/CCE.Api.Common/Results/EnvelopeResult.cs new file mode 100644 index 00000000..ef33a320 --- /dev/null +++ b/backend/src/CCE.Api.Common/Results/EnvelopeResult.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Http; + +namespace CCE.Api.Common.Results; + +/// +/// that writes a failure envelope with the correct HTTP status. +/// Used by factory methods for endpoints that need to return +/// an enveloped error without going through a MediatR handler. +/// +internal sealed class EnvelopeResult : IResult +{ + private readonly int _statusCode; + private readonly string _domainKey; + + public EnvelopeResult(int statusCode, string domainKey) + { + _statusCode = statusCode; + _domainKey = domainKey; + } + + public Task ExecuteAsync(HttpContext httpContext) + => EnvelopeWriter.WriteAsync(httpContext, _statusCode, _domainKey); +} diff --git a/backend/src/CCE.Api.Common/Results/EnvelopeResults.cs b/backend/src/CCE.Api.Common/Results/EnvelopeResults.cs new file mode 100644 index 00000000..80d521b4 --- /dev/null +++ b/backend/src/CCE.Api.Common/Results/EnvelopeResults.cs @@ -0,0 +1,32 @@ +using CCE.Application.Messages; +using Microsoft.AspNetCore.Http; + +namespace CCE.Api.Common.Results; + +/// +/// Enveloped equivalents of the raw Results.Unauthorized(), Results.NotFound(), +/// Results.BadRequest() helpers. Use in endpoints that short-circuit before reaching +/// a MediatR handler (e.g. when ICurrentUserAccessor.GetUserId() is empty). +/// +public static class EnvelopeResults +{ + /// 401 Unauthorized — enveloped with ERR901 / UNAUTHORIZED_ACCESS. + public static IResult Unauthorized() + => new EnvelopeResult(StatusCodes.Status401Unauthorized, MessageKeys.General.UNAUTHORIZED); + + /// 403 Forbidden — enveloped with ERR902 / FORBIDDEN_ACCESS. + public static IResult Forbidden() + => new EnvelopeResult(StatusCodes.Status403Forbidden, MessageKeys.General.FORBIDDEN); + + /// 404 Not Found — enveloped with ERR903 / RESOURCE_NOT_FOUND_GENERIC by default. + public static IResult NotFound(string domainKey = MessageKeys.General.RESOURCE_NOT_FOUND_GENERIC) + => new EnvelopeResult(StatusCodes.Status404NotFound, domainKey); + + /// 400 Bad Request — enveloped with ERR904 / BAD_REQUEST by default. + public static IResult BadRequest(string domainKey = MessageKeys.General.BAD_REQUEST) + => new EnvelopeResult(StatusCodes.Status400BadRequest, domainKey); + + /// 409 Conflict — pass CONCURRENCY_CONFLICT or DUPLICATE_VALUE explicitly. + public static IResult Conflict(string domainKey) + => new EnvelopeResult(StatusCodes.Status409Conflict, domainKey); +} diff --git a/backend/src/CCE.Api.Common/Results/EnvelopeWriter.cs b/backend/src/CCE.Api.Common/Results/EnvelopeWriter.cs new file mode 100644 index 00000000..d938e328 --- /dev/null +++ b/backend/src/CCE.Api.Common/Results/EnvelopeWriter.cs @@ -0,0 +1,69 @@ +using CCE.Application.Localization; +using CCE.Application.Messages; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CCE.Api.Common.Results; + +/// +/// Single source of truth for the error envelope shape written by +/// , the rate-limiter +/// OnRejected handlers, and the JWT OnChallenge/OnForbidden events. +/// +public static class EnvelopeWriter +{ + internal static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + }; + + /// + /// Writes a failure envelope with the given status code, localized message, and system code. + /// Resolves locale from Accept-Language so the envelope is correct regardless of + /// middleware ordering. Includes traceId and correlationId. + /// + public static async Task WriteAsync( + HttpContext ctx, + int statusCode, + string domainKey, + string? fallbackMessage = null, + object? errors = null) + { + if (ctx.Response.HasStarted) + return; + + var l = ctx.RequestServices.GetService(); + var config = ctx.RequestServices.GetService(); + var supported = config?.GetSection("Localization:Supported").Get(); + var defaultLocale = config?.GetValue("Localization:Default"); + var locale = Middleware.LocalizationMiddleware.PickLocale( + ctx.Request.Headers.AcceptLanguage.ToString(), supported, defaultLocale); + var msg = l?.GetString(domainKey, locale) ?? fallbackMessage ?? "خطأ"; + var code = SystemCodeMap.ToSystemCode(domainKey); + + var envelope = new + { + success = false, + code, + message = msg, + data = (object?)null, + errors = errors ?? Array.Empty(), + traceId = Activity.Current?.Id ?? ctx.TraceIdentifier, + correlationId = ctx.Items.TryGetValue(Middleware.CorrelationIdMiddleware.ItemKey, out var cid) + ? cid?.ToString() ?? string.Empty + : string.Empty, + timestamp = DateTimeOffset.UtcNow, + }; + + ctx.Response.StatusCode = statusCode; + ctx.Response.ContentType = "application/json; charset=utf-8"; + await JsonSerializer.SerializeAsync(ctx.Response.Body, envelope, JsonOptions) + .ConfigureAwait(false); + } +} diff --git a/backend/src/CCE.Api.Common/Results/MessageTypeStatusCodes.cs b/backend/src/CCE.Api.Common/Results/MessageTypeStatusCodes.cs new file mode 100644 index 00000000..1d1fe5fb --- /dev/null +++ b/backend/src/CCE.Api.Common/Results/MessageTypeStatusCodes.cs @@ -0,0 +1,18 @@ +using CCE.Domain.Common; +using Microsoft.AspNetCore.Http; + +namespace CCE.Api.Common.Results; + +internal static class MessageTypeStatusCodes +{ + internal static int ToStatusCode(MessageType type) => type switch + { + MessageType.NotFound => StatusCodes.Status404NotFound, + MessageType.Validation => StatusCodes.Status400BadRequest, + MessageType.Conflict => StatusCodes.Status409Conflict, + MessageType.Unauthorized => StatusCodes.Status401Unauthorized, + MessageType.Forbidden => StatusCodes.Status403Forbidden, + MessageType.BusinessRule => StatusCodes.Status422UnprocessableEntity, + _ => StatusCodes.Status500InternalServerError, + }; +} diff --git a/backend/src/CCE.Api.Common/Results/NoContentApiResponse.cs b/backend/src/CCE.Api.Common/Results/NoContentApiResponse.cs deleted file mode 100644 index 36e21e46..00000000 --- a/backend/src/CCE.Api.Common/Results/NoContentApiResponse.cs +++ /dev/null @@ -1,62 +0,0 @@ -using CCE.Application.Common; -using CCE.Domain.Common; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Metadata; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace CCE.Api.Common.HttpResults; - -/// -/// Typed that wraps a no-content envelope -/// and registers 204 No Content response metadata for Swashbuckle. -/// -[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Required by IEndpointMetadataProvider interface.")] -[SuppressMessage("Design", "CA1815:Override equals and operator equals on value types", Justification = "Result wrapper is never compared.")] -public readonly struct NoContentApiResponse : IResult, IEndpointMetadataProvider -{ - private readonly Response _payload; - - public NoContentApiResponse(Response payload) => _payload = payload; - - public Task ExecuteAsync(HttpContext httpContext) - { - var stamped = _payload with - { - TraceId = Activity.Current?.Id ?? string.Empty, - Timestamp = DateTimeOffset.UtcNow, - }; - - if (stamped.Success) - { - return Results.NoContent().ExecuteAsync(httpContext); - } - - var statusCode = stamped.Type switch - { - MessageType.NotFound => StatusCodes.Status404NotFound, - MessageType.Validation => StatusCodes.Status400BadRequest, - MessageType.Conflict => StatusCodes.Status409Conflict, - MessageType.Unauthorized => StatusCodes.Status401Unauthorized, - MessageType.Forbidden => StatusCodes.Status403Forbidden, - MessageType.BusinessRule => StatusCodes.Status422UnprocessableEntity, - _ => StatusCodes.Status500InternalServerError, - }; - - return Results.Json(stamped, statusCode: statusCode).ExecuteAsync(httpContext); - } - - public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) - { - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status204NoContent, typeof(void), [])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status401Unauthorized, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status403Forbidden, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status500InternalServerError, typeof(Response), ["application/json"])); - } -} diff --git a/backend/src/CCE.Api.Common/Results/NoContentApiResult.cs b/backend/src/CCE.Api.Common/Results/NoContentApiResult.cs index 325d956f..ff4db99c 100644 --- a/backend/src/CCE.Api.Common/Results/NoContentApiResult.cs +++ b/backend/src/CCE.Api.Common/Results/NoContentApiResult.cs @@ -1,5 +1,4 @@ -using CCE.Application.Common; -using CCE.Domain.Common; +using CCE.Application.Common; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; @@ -7,50 +6,48 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; -namespace CCE.Api.Common.HttpResults; +namespace CCE.Api.Common.Results; /// -/// Typed that wraps a no-content envelope +/// Typed that wraps a no-content envelope /// and registers 204 No Content response metadata for Swashbuckle. /// [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Required by IEndpointMetadataProvider interface.")] [SuppressMessage("Design", "CA1815:Override equals and operator equals on value types", Justification = "Result wrapper is never compared.")] public readonly struct NoContentApiResult : IResult, IEndpointMetadataProvider { - private readonly Result _payload; + private readonly Response _payload; - public NoContentApiResult(Result payload) => _payload = payload; + public NoContentApiResult(Response payload) => _payload = payload; public Task ExecuteAsync(HttpContext httpContext) { - if (_payload.IsSuccess) - { - return Results.NoContent().ExecuteAsync(httpContext); - } + var correlationId = httpContext.Items.TryGetValue(Middleware.CorrelationIdMiddleware.ItemKey, out var cid) + ? cid?.ToString() ?? string.Empty + : string.Empty; - var statusCode = _payload.Error!.Type switch + var stamped = _payload with { - ErrorType.NotFound => StatusCodes.Status404NotFound, - ErrorType.Validation => StatusCodes.Status400BadRequest, - ErrorType.Conflict => StatusCodes.Status409Conflict, - ErrorType.Unauthorized => StatusCodes.Status401Unauthorized, - ErrorType.Forbidden => StatusCodes.Status403Forbidden, - ErrorType.BusinessRule => StatusCodes.Status422UnprocessableEntity, - _ => StatusCodes.Status500InternalServerError, + TraceId = Activity.Current?.Id ?? string.Empty, + CorrelationId = correlationId, + Timestamp = DateTimeOffset.UtcNow, }; - return Results.Json(_payload, statusCode: statusCode).ExecuteAsync(httpContext); + if (stamped.Success) + return TypedResults.NoContent().ExecuteAsync(httpContext); + + return TypedResults.Json(stamped, options: EnvelopeWriter.JsonOptions, statusCode: MessageTypeStatusCodes.ToStatusCode(stamped.Type)).ExecuteAsync(httpContext); } public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status204NoContent, typeof(void), [])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status401Unauthorized, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status403Forbidden, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status500InternalServerError, typeof(Result), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status401Unauthorized, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status403Forbidden, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status500InternalServerError, typeof(Response), ["application/json"])); } } diff --git a/backend/src/CCE.Api.Common/Results/OkApiResponse.cs b/backend/src/CCE.Api.Common/Results/OkApiResponse.cs deleted file mode 100644 index 43d6eef2..00000000 --- a/backend/src/CCE.Api.Common/Results/OkApiResponse.cs +++ /dev/null @@ -1,62 +0,0 @@ -using CCE.Application.Common; -using CCE.Domain.Common; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Metadata; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace CCE.Api.Common.HttpResults; - -/// -/// Typed that wraps a success envelope -/// and registers 200 OK response metadata for Swashbuckle. -/// -[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Required by IEndpointMetadataProvider interface.")] -[SuppressMessage("Design", "CA1815:Override equals and operator equals on value types", Justification = "Result wrapper is never compared.")] -public readonly struct OkApiResponse : IResult, IEndpointMetadataProvider -{ - private readonly Response _payload; - - public OkApiResponse(Response payload) => _payload = payload; - - public Task ExecuteAsync(HttpContext httpContext) - { - var stamped = _payload with - { - TraceId = Activity.Current?.Id ?? string.Empty, - Timestamp = DateTimeOffset.UtcNow, - }; - - if (stamped.Success) - { - return Results.Json(stamped, statusCode: StatusCodes.Status200OK).ExecuteAsync(httpContext); - } - - var statusCode = stamped.Type switch - { - MessageType.NotFound => StatusCodes.Status404NotFound, - MessageType.Validation => StatusCodes.Status400BadRequest, - MessageType.Conflict => StatusCodes.Status409Conflict, - MessageType.Unauthorized => StatusCodes.Status401Unauthorized, - MessageType.Forbidden => StatusCodes.Status403Forbidden, - MessageType.BusinessRule => StatusCodes.Status422UnprocessableEntity, - _ => StatusCodes.Status500InternalServerError, - }; - - return Results.Json(stamped, statusCode: statusCode).ExecuteAsync(httpContext); - } - - public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) - { - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status401Unauthorized, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status403Forbidden, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity, typeof(Response), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status500InternalServerError, typeof(Response), ["application/json"])); - } -} diff --git a/backend/src/CCE.Api.Common/Results/OkApiResult.cs b/backend/src/CCE.Api.Common/Results/OkApiResult.cs index 3701f79d..861a562b 100644 --- a/backend/src/CCE.Api.Common/Results/OkApiResult.cs +++ b/backend/src/CCE.Api.Common/Results/OkApiResult.cs @@ -1,5 +1,4 @@ -using CCE.Application.Common; -using CCE.Domain.Common; +using CCE.Application.Common; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; @@ -7,50 +6,48 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; -namespace CCE.Api.Common.HttpResults; +namespace CCE.Api.Common.Results; /// -/// Typed that wraps a success envelope +/// Typed that wraps a success envelope /// and registers 200 OK response metadata for Swashbuckle. /// [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Required by IEndpointMetadataProvider interface.")] [SuppressMessage("Design", "CA1815:Override equals and operator equals on value types", Justification = "Result wrapper is never compared.")] public readonly struct OkApiResult : IResult, IEndpointMetadataProvider { - private readonly Result _payload; + private readonly Response _payload; - public OkApiResult(Result payload) => _payload = payload; + public OkApiResult(Response payload) => _payload = payload; public Task ExecuteAsync(HttpContext httpContext) { - if (_payload.IsSuccess) - { - return Results.Json(_payload, statusCode: StatusCodes.Status200OK).ExecuteAsync(httpContext); - } + var correlationId = httpContext.Items.TryGetValue(Middleware.CorrelationIdMiddleware.ItemKey, out var cid) + ? cid?.ToString() ?? string.Empty + : string.Empty; - var statusCode = _payload.Error!.Type switch + var stamped = _payload with { - ErrorType.NotFound => StatusCodes.Status404NotFound, - ErrorType.Validation => StatusCodes.Status400BadRequest, - ErrorType.Conflict => StatusCodes.Status409Conflict, - ErrorType.Unauthorized => StatusCodes.Status401Unauthorized, - ErrorType.Forbidden => StatusCodes.Status403Forbidden, - ErrorType.BusinessRule => StatusCodes.Status422UnprocessableEntity, - _ => StatusCodes.Status500InternalServerError, + TraceId = Activity.Current?.Id ?? string.Empty, + CorrelationId = correlationId, + Timestamp = DateTimeOffset.UtcNow, }; - return Results.Json(_payload, statusCode: statusCode).ExecuteAsync(httpContext); + if (stamped.Success) + return TypedResults.Json(stamped, options: EnvelopeWriter.JsonOptions, statusCode: StatusCodes.Status200OK).ExecuteAsync(httpContext); + + return TypedResults.Json(stamped, options: EnvelopeWriter.JsonOptions, statusCode: MessageTypeStatusCodes.ToStatusCode(stamped.Type)).ExecuteAsync(httpContext); } public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status401Unauthorized, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status403Forbidden, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity, typeof(Result), ["application/json"])); - builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status500InternalServerError, typeof(Result), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status401Unauthorized, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status403Forbidden, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity, typeof(Response), ["application/json"])); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status500InternalServerError, typeof(Response), ["application/json"])); } } diff --git a/backend/src/CCE.Api.External/Endpoints/AssetEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/AssetEndpoints.cs index 49cc60cc..744510bf 100644 --- a/backend/src/CCE.Api.External/Endpoints/AssetEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/AssetEndpoints.cs @@ -1,5 +1,6 @@ -using CCE.Api.Common.Auth; +using CCE.Api.Common.Auth; using CCE.Api.Common.Extensions; +using CCE.Api.Common.Results; using CCE.Application.Common.Interfaces; using CCE.Application.Content; using CCE.Application.Content.Commands.UploadAsset; @@ -27,10 +28,10 @@ public static IEndpointRouteBuilder MapAssetEndpoints(this IEndpointRouteBuilder IOptions infraOpts, CancellationToken ct) => { - if (currentUser.GetUserId() is null) return Results.Unauthorized(); + if (currentUser.GetUserId() is null) return EnvelopeResults.Unauthorized(); if (file is null || file.Length == 0) - return Results.BadRequest(new { error = "Upload requires a non-empty file." }); + return EnvelopeResults.BadRequest(); var allowed = infraOpts.Value.AllowedAssetMimeTypes; if (!allowed.Contains(file.ContentType, System.StringComparer.OrdinalIgnoreCase)) diff --git a/backend/src/CCE.Api.External/Endpoints/CategoriesPublicEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/CategoriesPublicEndpoints.cs index 3641956e..dbb348a4 100644 --- a/backend/src/CCE.Api.External/Endpoints/CategoriesPublicEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/CategoriesPublicEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Content.Public.Queries.ListPublicResourceCategories; using MediatR; using Microsoft.AspNetCore.Builder; @@ -15,7 +16,7 @@ public static IEndpointRouteBuilder MapCategoriesPublicEndpoints(this IEndpointR categories.MapGet("", async (IMediator mediator, CancellationToken ct) => { var result = await mediator.Send(new ListPublicResourceCategoriesQuery(), ct).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .AllowAnonymous() .WithName("ListPublicCategories"); diff --git a/backend/src/CCE.Api.External/Endpoints/CommunityPublicEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/CommunityPublicEndpoints.cs index 7afe5b61..1b837905 100644 --- a/backend/src/CCE.Api.External/Endpoints/CommunityPublicEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/CommunityPublicEndpoints.cs @@ -1,4 +1,5 @@ -using CCE.Api.Common.Extensions; +using CCE.Api.Common.Extensions; +using CCE.Api.Common.Results; using CCE.Application.Common.Interfaces; using CCE.Application.Community.Public.Queries.GetCommunityBySlug; using CCE.Application.Community.Public.Queries.GetCommunityUserProfile; @@ -187,7 +188,7 @@ public static IEndpointRouteBuilder MapCommunityPublicEndpoints(this IEndpointRo IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send(new GetMyFollowsQuery(userId), ct).ConfigureAwait(false); return result.ToHttpResult(); }).WithName("GetMyFollows"); @@ -201,7 +202,7 @@ public static IEndpointRouteBuilder MapCommunityPublicEndpoints(this IEndpointRo ICurrentUserAccessor currentUser, IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId(); - if (!userId.HasValue) return Results.Unauthorized(); + if (!userId.HasValue) return EnvelopeResults.Unauthorized(); var result = await mediator.Send( new ListUserFeedQuery( userId.Value, @@ -224,7 +225,7 @@ public static IEndpointRouteBuilder MapCommunityPublicEndpoints(this IEndpointRo { var userId = currentUser.GetUserId(); if (userId is null || userId == System.Guid.Empty) - return Results.Unauthorized(); + return EnvelopeResults.Unauthorized(); var query = new ListCommunityFeedQuery( sort ?? PostFeedSort.Newest, tagIds ?? System.Array.Empty(), diff --git a/backend/src/CCE.Api.External/Endpoints/CommunityWriteEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/CommunityWriteEndpoints.cs index 3dd724d4..c04ca3df 100644 --- a/backend/src/CCE.Api.External/Endpoints/CommunityWriteEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/CommunityWriteEndpoints.cs @@ -1,4 +1,5 @@ -using CCE.Api.Common.Extensions; +using CCE.Api.Common.Extensions; +using CCE.Api.Common.Results; using CCE.Application.Common.Interfaces; using CCE.Application.Community.Commands.CastPollVote; using CCE.Application.Community.Commands.CreatePost; @@ -119,11 +120,11 @@ public static IEndpointRouteBuilder MapCommunityWriteEndpoints(this IEndpointRou CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var cmd = new MarkPostAnsweredCommand(id, body.ReplyId); - await mediator.Send(cmd, ct).ConfigureAwait(false); - return Results.Ok(); + var result = await mediator.Send(cmd, ct).ConfigureAwait(false); + return result.ToNoContentHttpResult(); }).WithName("MarkPostAnswered"); // PUT /api/community/replies/{id} @@ -135,11 +136,11 @@ public static IEndpointRouteBuilder MapCommunityWriteEndpoints(this IEndpointRou CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var cmd = new EditReplyCommand(id, body.Content); - await mediator.Send(cmd, ct).ConfigureAwait(false); - return Results.Ok(); + var result = await mediator.Send(cmd, ct).ConfigureAwait(false); + return result.ToNoContentHttpResult(); }).WithName("EditReply"); // POST /api/community/polls/{id}/vote — cast a poll vote diff --git a/backend/src/CCE.Api.External/Endpoints/CountriesPublicEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/CountriesPublicEndpoints.cs index d6ca6695..26fb5d5b 100644 --- a/backend/src/CCE.Api.External/Endpoints/CountriesPublicEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/CountriesPublicEndpoints.cs @@ -1,4 +1,5 @@ -using CCE.Api.Common.Extensions; +using CCE.Api.Common.Extensions; +using CCE.Api.Common.Results; using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content; @@ -55,20 +56,20 @@ public static IEndpointRouteBuilder MapCountriesPublicEndpoints(this IEndpointRo var country = await db.Countries .FirstOrDefaultAsync(c => c.Id == id && c.IsActive, ct).ConfigureAwait(false); if (country is null) - return Results.NotFound(); + return EnvelopeResults.NotFound(); var profile = await db.CountryProfiles .FirstOrDefaultAsync(p => p.CountryId == id, ct).ConfigureAwait(false); if (profile?.NationallyDeterminedContributionAssetId is null) - return Results.NotFound(); + return EnvelopeResults.NotFound(); var asset = await db.AssetFiles .FirstOrDefaultAsync(a => a.Id == profile.NationallyDeterminedContributionAssetId.Value, ct) .ConfigureAwait(false); if (asset is null) - return Results.NotFound(); + return EnvelopeResults.NotFound(); if (asset.VirusScanStatus != VirusScanStatus.Clean) - return Results.StatusCode(StatusCodes.Status403Forbidden); + return EnvelopeResults.Forbidden(); httpContext.Response.ContentType = asset.MimeType; httpContext.Response.Headers.ContentDisposition = diff --git a/backend/src/CCE.Api.External/Endpoints/HomepageSectionsPublicEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/HomepageSectionsPublicEndpoints.cs index f6fa9b30..783991e7 100644 --- a/backend/src/CCE.Api.External/Endpoints/HomepageSectionsPublicEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/HomepageSectionsPublicEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Content.Public.Queries.ListPublicHomepageSections; using MediatR; using Microsoft.AspNetCore.Builder; @@ -15,7 +16,7 @@ public static IEndpointRouteBuilder MapHomepageSectionsPublicEndpoints(this IEnd sections.MapGet("", async (IMediator mediator, CancellationToken ct) => { var result = await mediator.Send(new ListPublicHomepageSectionsQuery(), ct).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .AllowAnonymous() .WithName("ListPublicHomepageSections"); diff --git a/backend/src/CCE.Api.External/Endpoints/InteractiveCityEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/InteractiveCityEndpoints.cs index 4b3f50bb..6342361a 100644 --- a/backend/src/CCE.Api.External/Endpoints/InteractiveCityEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/InteractiveCityEndpoints.cs @@ -1,3 +1,5 @@ +using CCE.Api.Common.Extensions; +using CCE.Api.Common.Results; using CCE.Application.Common.Interfaces; using CCE.Application.InteractiveCity.Public.Commands.DeleteMyScenario; using CCE.Application.InteractiveCity.Public.Commands.RunScenario; @@ -24,7 +26,7 @@ public static IEndpointRouteBuilder MapInteractiveCityEndpoints(this IEndpointRo { var result = await mediator.Send(new ListCityTechnologiesQuery(), cancellationToken) .ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .AllowAnonymous() .WithName("ListCityTechnologies"); @@ -35,7 +37,7 @@ public static IEndpointRouteBuilder MapInteractiveCityEndpoints(this IEndpointRo { var cmd = new RunScenarioCommand(body.CityType, body.TargetYear, body.ConfigurationJson); var result = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .AllowAnonymous() .WithName("RunScenario"); @@ -52,8 +54,8 @@ public static IEndpointRouteBuilder MapInteractiveCityEndpoints(this IEndpointRo ?? throw new System.InvalidOperationException("User identity required."); var cmd = new SaveScenarioCommand( userId, body.NameAr, body.NameEn, body.CityType, body.TargetYear, body.ConfigurationJson); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return Results.Created($"/api/me/interactive-city/scenarios/{dto.Id}", dto); + var result = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return result.ToCreatedHttpResult(); }) .RequireAuthorization() .WithName("SaveMyScenario"); @@ -66,7 +68,7 @@ public static IEndpointRouteBuilder MapInteractiveCityEndpoints(this IEndpointRo ?? throw new System.InvalidOperationException("User identity required."); var result = await mediator.Send(new ListMyScenariosQuery(userId), cancellationToken) .ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .RequireAuthorization() .WithName("ListMyScenarios"); @@ -80,13 +82,13 @@ public static IEndpointRouteBuilder MapInteractiveCityEndpoints(this IEndpointRo ?? throw new System.InvalidOperationException("User identity required."); try { - await mediator.Send(new DeleteMyScenarioCommand(id, userId), cancellationToken) + var result = await mediator.Send(new DeleteMyScenarioCommand(id, userId), cancellationToken) .ConfigureAwait(false); - return Results.NoContent(); + return result.ToNoContentHttpResult(); } catch (System.Collections.Generic.KeyNotFoundException) { - return Results.NotFound(); + return EnvelopeResults.NotFound(); } }) .RequireAuthorization() diff --git a/backend/src/CCE.Api.External/Endpoints/KapsarcEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/KapsarcEndpoints.cs index a5791aff..4279215c 100644 --- a/backend/src/CCE.Api.External/Endpoints/KapsarcEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/KapsarcEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Kapsarc.Queries.GetLatestKapsarcSnapshot; using MediatR; using Microsoft.AspNetCore.Builder; @@ -18,8 +19,8 @@ public static IEndpointRouteBuilder MapKapsarcEndpoints(this IEndpointRouteBuild IMediator mediator, CancellationToken ct) => { - var dto = await mediator.Send(new GetLatestKapsarcSnapshotQuery(countryId), ct).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var result = await mediator.Send(new GetLatestKapsarcSnapshotQuery(countryId), ct).ConfigureAwait(false); + return result.ToHttpResult(); }) .AllowAnonymous() .WithName("GetLatestKapsarcSnapshot"); diff --git a/backend/src/CCE.Api.External/Endpoints/KnowledgeMapEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/KnowledgeMapEndpoints.cs index ebd7a498..c7333a85 100644 --- a/backend/src/CCE.Api.External/Endpoints/KnowledgeMapEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/KnowledgeMapEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.KnowledgeMaps.Public.Queries.GetKnowledgeMapById; using CCE.Application.KnowledgeMaps.Public.Queries.ListKnowledgeMapEdges; using CCE.Application.KnowledgeMaps.Public.Queries.ListKnowledgeMapNodes; @@ -19,7 +20,7 @@ public static IEndpointRouteBuilder MapKnowledgeMapEndpoints(this IEndpointRoute IMediator mediator, CancellationToken cancellationToken) => { var result = await mediator.Send(new ListKnowledgeMapsQuery(), cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .AllowAnonymous() .WithName("ListKnowledgeMaps"); @@ -28,8 +29,8 @@ public static IEndpointRouteBuilder MapKnowledgeMapEndpoints(this IEndpointRoute System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - var dto = await mediator.Send(new GetKnowledgeMapByIdQuery(id), cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var result = await mediator.Send(new GetKnowledgeMapByIdQuery(id), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); }) .AllowAnonymous() .WithName("GetKnowledgeMapById"); @@ -39,7 +40,7 @@ public static IEndpointRouteBuilder MapKnowledgeMapEndpoints(this IEndpointRoute IMediator mediator, CancellationToken cancellationToken) => { var result = await mediator.Send(new ListKnowledgeMapNodesQuery(id), cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .AllowAnonymous() .WithName("ListKnowledgeMapNodes"); @@ -49,7 +50,7 @@ public static IEndpointRouteBuilder MapKnowledgeMapEndpoints(this IEndpointRoute IMediator mediator, CancellationToken cancellationToken) => { var result = await mediator.Send(new ListKnowledgeMapEdgesQuery(id), cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .AllowAnonymous() .WithName("ListKnowledgeMapEdges"); diff --git a/backend/src/CCE.Api.External/Endpoints/NotificationsEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/NotificationsEndpoints.cs index c941a216..4c5f4ed7 100644 --- a/backend/src/CCE.Api.External/Endpoints/NotificationsEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/NotificationsEndpoints.cs @@ -1,4 +1,5 @@ -using CCE.Api.Common.Extensions; +using CCE.Api.Common.Extensions; +using CCE.Api.Common.Results; using CCE.Application.Common.Interfaces; using CCE.Application.Notifications.Public.Commands.MarkAllNotificationsRead; using CCE.Application.Notifications.Public.Commands.MarkNotificationRead; @@ -28,7 +29,7 @@ public static IEndpointRouteBuilder MapNotificationsEndpoints(this IEndpointRout IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var query = new ListMyNotificationsQuery(userId, page ?? 1, pageSize ?? 20, status); var result = await mediator.Send(query, ct).ConfigureAwait(false); return result.ToHttpResult(); @@ -39,7 +40,7 @@ public static IEndpointRouteBuilder MapNotificationsEndpoints(this IEndpointRout IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send(new GetMyUnreadCountQuery(userId), ct).ConfigureAwait(false); return result.ToHttpResult(); }).WithName("GetMyUnreadNotificationCount"); @@ -50,7 +51,7 @@ public static IEndpointRouteBuilder MapNotificationsEndpoints(this IEndpointRout IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send(new MarkNotificationReadCommand(id, userId), ct).ConfigureAwait(false); return result.ToHttpResult(); }).WithName("MarkNotificationRead"); @@ -60,7 +61,7 @@ public static IEndpointRouteBuilder MapNotificationsEndpoints(this IEndpointRout IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send(new MarkAllNotificationsReadCommand(userId), ct).ConfigureAwait(false); return result.ToHttpResult(); }).WithName("MarkAllNotificationsRead"); @@ -70,7 +71,7 @@ public static IEndpointRouteBuilder MapNotificationsEndpoints(this IEndpointRout IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send(new GetMyNotificationSettingsQuery(userId), ct).ConfigureAwait(false); return result.ToHttpResult(); }).WithName("GetMyNotificationSettings"); @@ -81,7 +82,7 @@ public static IEndpointRouteBuilder MapNotificationsEndpoints(this IEndpointRout IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var command = new UpdateMyNotificationSettingsCommand( userId, body.Channel, diff --git a/backend/src/CCE.Api.External/Endpoints/PagesPublicEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/PagesPublicEndpoints.cs index d4f809a7..b2e2b4df 100644 --- a/backend/src/CCE.Api.External/Endpoints/PagesPublicEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/PagesPublicEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Content.Public.Queries.GetPublicPageBySlug; using MediatR; using Microsoft.AspNetCore.Builder; @@ -14,8 +15,8 @@ public static IEndpointRouteBuilder MapPagesPublicEndpoints(this IEndpointRouteB pages.MapGet("/{slug}", async (string slug, IMediator mediator, CancellationToken ct) => { - var dto = await mediator.Send(new GetPublicPageBySlugQuery(slug), ct).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var result = await mediator.Send(new GetPublicPageBySlugQuery(slug), ct).ConfigureAwait(false); + return result.ToHttpResult(); }) .AllowAnonymous() .WithName("GetPublicPageBySlug"); diff --git a/backend/src/CCE.Api.External/Endpoints/ProfileEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/ProfileEndpoints.cs index 99814b5a..b10df689 100644 --- a/backend/src/CCE.Api.External/Endpoints/ProfileEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/ProfileEndpoints.cs @@ -1,5 +1,6 @@ -using CCE.Api.Common.Auth; +using CCE.Api.Common.Auth; using CCE.Api.Common.Extensions; +using CCE.Api.Common.Results; using CCE.Application.Common.Interfaces; using CCE.Application.Identity.Auth.Register; using CCE.Application.Identity.Public.Commands.ConfirmEmailChange; @@ -52,7 +53,7 @@ public static IEndpointRouteBuilder MapProfileEndpoints(this IEndpointRouteBuild IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var cmd = new SubmitExpertRequestCommand( userId, body.RequestedBioAr, body.RequestedBioEn, body.RequestedTags ?? System.Array.Empty(), @@ -69,7 +70,7 @@ public static IEndpointRouteBuilder MapProfileEndpoints(this IEndpointRouteBuild IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send(new GetMyProfileQuery(userId), ct).ConfigureAwait(false); return result.ToHttpResult(); }) @@ -81,7 +82,7 @@ public static IEndpointRouteBuilder MapProfileEndpoints(this IEndpointRouteBuild IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var cmd = new UpdateMyProfileCommand( userId, body.FirstName, body.LastName, body.JobTitle, body.OrganizationName, @@ -97,7 +98,7 @@ public static IEndpointRouteBuilder MapProfileEndpoints(this IEndpointRouteBuild IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send(new GetMyExpertStatusQuery(userId), ct).ConfigureAwait(false); return result.ToHttpResult(); }) @@ -109,7 +110,7 @@ public static IEndpointRouteBuilder MapProfileEndpoints(this IEndpointRouteBuild IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send( new RequestEmailChangeCommand(userId, body.NewEmail), ct).ConfigureAwait(false); return result.ToHttpResult(); @@ -122,7 +123,7 @@ public static IEndpointRouteBuilder MapProfileEndpoints(this IEndpointRouteBuild IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send( new ConfirmEmailChangeCommand(userId, body.VerificationId, body.Code), ct).ConfigureAwait(false); return result.ToHttpResult(); @@ -135,7 +136,7 @@ public static IEndpointRouteBuilder MapProfileEndpoints(this IEndpointRouteBuild IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send( new RequestPhoneChangeCommand(userId, body.NewPhone, body.CountryId), ct).ConfigureAwait(false); return result.ToHttpResult(); @@ -148,7 +149,7 @@ public static IEndpointRouteBuilder MapProfileEndpoints(this IEndpointRouteBuild IMediator mediator, CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send( new ConfirmPhoneChangeCommand(userId, body.VerificationId, body.Code), ct).ConfigureAwait(false); return result.ToHttpResult(); diff --git a/backend/src/CCE.Api.External/Endpoints/ResourcesPublicEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/ResourcesPublicEndpoints.cs index 61832699..34e1496a 100644 --- a/backend/src/CCE.Api.External/Endpoints/ResourcesPublicEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/ResourcesPublicEndpoints.cs @@ -1,5 +1,6 @@ -using System.IO; +using System.IO; using CCE.Api.Common.Extensions; +using CCE.Api.Common.Results; using CCE.Application.Common.Interfaces; using CCE.Application.Content; using CCE.Application.Content.Public; @@ -58,13 +59,13 @@ public static IEndpointRouteBuilder MapResourcesPublicEndpoints(this IEndpointRo { var resource = await db.Resources.FirstOrDefaultAsync(r => r.Id == id, cancellationToken).ConfigureAwait(false); if (resource is null || resource.PublishedOn is null) - return Results.NotFound(); + return EnvelopeResults.NotFound(); var asset = await db.AssetFiles.FirstOrDefaultAsync(a => a.Id == resource.AssetFileId, cancellationToken).ConfigureAwait(false); if (asset is null) - return Results.NotFound(); + return EnvelopeResults.NotFound(); if (asset.VirusScanStatus != VirusScanStatus.Clean) - return Results.StatusCode(StatusCodes.Status403Forbidden); + return EnvelopeResults.Forbidden(); httpContext.Response.ContentType = asset.MimeType; httpContext.Response.Headers.ContentDisposition = @@ -78,7 +79,7 @@ public static IEndpointRouteBuilder MapResourcesPublicEndpoints(this IEndpointRo } catch (FileNotFoundException) { - return Results.NotFound(); + return EnvelopeResults.NotFound(); } await using (fileStream) diff --git a/backend/src/CCE.Api.External/Endpoints/SearchEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/SearchEndpoints.cs index 4fbfee2d..ac8524e8 100644 --- a/backend/src/CCE.Api.External/Endpoints/SearchEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/SearchEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Search; using CCE.Application.Search.Queries; using MediatR; @@ -19,7 +20,7 @@ public static IEndpointRouteBuilder MapSearchEndpoints(this IEndpointRouteBuilde { var query = new SearchQuery(q ?? string.Empty, type, page ?? 1, pageSize ?? 20); var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .AllowAnonymous() .WithName("Search"); diff --git a/backend/src/CCE.Api.External/Endpoints/StateRepresentativeEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/StateRepresentativeEndpoints.cs index 8a0e9377..056270c3 100644 --- a/backend/src/CCE.Api.External/Endpoints/StateRepresentativeEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/StateRepresentativeEndpoints.cs @@ -75,7 +75,7 @@ public static IEndpointRouteBuilder MapStateRepresentativeEndpoints(this IEndpoi group.MapGet("/resource-categories", async (IMediator mediator, CancellationToken ct) => { var result = await mediator.Send(new ListPublicResourceCategoriesQuery(), ct).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Resource_Center_View) .WithName("ListStateRepResourceCategories"); diff --git a/backend/src/CCE.Api.External/Endpoints/SurveysEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/SurveysEndpoints.cs index 4b7752be..27b9ff11 100644 --- a/backend/src/CCE.Api.External/Endpoints/SurveysEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/SurveysEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Surveys.Commands.SubmitServiceRating; using MediatR; using Microsoft.AspNetCore.Builder; @@ -24,8 +25,8 @@ public static IEndpointRouteBuilder MapSurveysEndpoints(this IEndpointRouteBuild body.CommentEn, body.Page, body.Locale); - var id = await mediator.Send(cmd, ct).ConfigureAwait(false); - return Results.Created($"/api/surveys/service-rating/{id}", new { id }); + var result = await mediator.Send(cmd, ct).ConfigureAwait(false); + return result.ToCreatedHttpResult(); }) .AllowAnonymous() .WithName("SubmitServiceRating"); diff --git a/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs b/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs index 1ec98391..1b2c3939 100644 --- a/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs +++ b/backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs @@ -1,4 +1,5 @@ -using CCE.Api.Common.Extensions; +using CCE.Api.Common.Extensions; +using CCE.Api.Common.Results; using CCE.Application.Common.Interfaces; using CCE.Application.Identity.Public.Commands.UserInterest; using CCE.Application.Identity.Public.Dtos; @@ -22,7 +23,7 @@ public static IEndpointRouteBuilder MapUserInterestEndpoints(this IEndpointRoute CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send(new GetMyInterestsQuery(userId), ct).ConfigureAwait(false); return result.ToHttpResult(); @@ -36,7 +37,7 @@ public static IEndpointRouteBuilder MapUserInterestEndpoints(this IEndpointRoute CancellationToken ct) => { var userId = currentUser.GetUserId() ?? System.Guid.Empty; - if (userId == System.Guid.Empty) return Results.Unauthorized(); + if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized(); var result = await mediator.Send( new UpsertUserInterestCommand( diff --git a/backend/src/CCE.Api.External/Program.cs b/backend/src/CCE.Api.External/Program.cs index 57528522..fd921d17 100644 --- a/backend/src/CCE.Api.External/Program.cs +++ b/backend/src/CCE.Api.External/Program.cs @@ -60,7 +60,9 @@ var app = builder.Build(); -// Middleware order (spec §7.1): correlation → exception → security headers → rate → auth → output-cache → authz → locale +// Middleware order (spec §7.1): correlation → exception → security headers → rate → output-cache → auth → authz → locale +// Output-cache is before auth so public endpoints can be cached without authentication. +// Authorized endpoints opt out via vary-by-user or explicit Cache-Control headers. app.UseMiddleware(); app.UseSerilogRequestLogging(); app.UseMiddleware(); diff --git a/backend/src/CCE.Api.Internal/Endpoints/AssetEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/AssetEndpoints.cs index 33340fbb..d070a01b 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/AssetEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/AssetEndpoints.cs @@ -1,4 +1,5 @@ -using CCE.Api.Common.Extensions; +using CCE.Api.Common.Extensions; +using CCE.Api.Common.Results; using CCE.Application.Content; using CCE.Application.Content.Commands.UploadAsset; using CCE.Application.Content.Queries.DownloadFile; @@ -28,12 +29,12 @@ public static IEndpointRouteBuilder MapAssetEndpoints(this IEndpointRouteBuilder CancellationToken cancellationToken) => { if (!httpContext.Request.HasFormContentType) - return Results.BadRequest(new { error = "Multipart form-data with a single 'file' field is required." }); + return EnvelopeResults.BadRequest(); var form = await httpContext.Request.ReadFormAsync(cancellationToken).ConfigureAwait(false); var file = form.Files["file"] ?? (form.Files.Count > 0 ? form.Files[0] : null); if (file is null || file.Length == 0) - return Results.BadRequest(new { error = "Upload requires a non-empty 'file' field." }); + return EnvelopeResults.BadRequest(); var allowed = infraOpts.Value.AllowedAssetMimeTypes; if (!allowed.Contains(file.ContentType, System.StringComparer.OrdinalIgnoreCase)) diff --git a/backend/src/CCE.Api.Internal/Endpoints/AuditEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/AuditEndpoints.cs index fb53a457..1fb414d0 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/AuditEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/AuditEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Audit.Queries.ListAuditEvents; using CCE.Domain; using MediatR; @@ -25,7 +26,7 @@ public static IEndpointRouteBuilder MapAuditEndpoints(this IEndpointRouteBuilder actor, actionPrefix, resourceType, correlationId, from, to); var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Audit_Read) .WithName("ListAuditEvents"); diff --git a/backend/src/CCE.Api.Internal/Endpoints/CommunityModerationEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/CommunityModerationEndpoints.cs index a299959f..be6a1f44 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/CommunityModerationEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/CommunityModerationEndpoints.cs @@ -45,7 +45,7 @@ public static IEndpointRouteBuilder MapCommunityModerationEndpoints(this IEndpoi Search: search, Status: status, Locale: locale), cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Community_Post_Moderate) .WithName("ListAdminPosts"); @@ -53,8 +53,8 @@ public static IEndpointRouteBuilder MapCommunityModerationEndpoints(this IEndpoi moderation.MapDelete("/posts/{id:guid}", async ( System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - await mediator.Send(new SoftDeletePostCommand(id), cancellationToken).ConfigureAwait(false); - return Results.NoContent(); + var result = await mediator.Send(new SoftDeletePostCommand(id), cancellationToken).ConfigureAwait(false); + return result.ToNoContentHttpResult(); }) .RequireAuthorization(Permissions.Community_Post_Moderate) .WithName("SoftDeletePost"); @@ -62,8 +62,8 @@ public static IEndpointRouteBuilder MapCommunityModerationEndpoints(this IEndpoi moderation.MapDelete("/replies/{id:guid}", async ( System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - await mediator.Send(new SoftDeleteReplyCommand(id), cancellationToken).ConfigureAwait(false); - return Results.NoContent(); + var result = await mediator.Send(new SoftDeleteReplyCommand(id), cancellationToken).ConfigureAwait(false); + return result.ToNoContentHttpResult(); }) .RequireAuthorization(Permissions.Community_Post_Moderate) .WithName("SoftDeleteReply"); diff --git a/backend/src/CCE.Api.Internal/Endpoints/CountryCodeEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/CountryCodeEndpoints.cs index 27f0b81f..168b4589 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/CountryCodeEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/CountryCodeEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Lookups.Commands.UpsertCountryCode; using CCE.Application.Lookups.Queries.GetCountryCodeById; using CCE.Application.Lookups.Queries.ListCountryCodes; @@ -21,7 +22,7 @@ public static IEndpointRouteBuilder MapCountryCodeEndpoints(this IEndpointRouteB { var query = new ListCountryCodesQuery(Search: search, IsActive: isActive); var result = await mediator.Send(query, ct).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Lookup_Manage) .WithName("ListCountryCodes"); @@ -31,7 +32,7 @@ public static IEndpointRouteBuilder MapCountryCodeEndpoints(this IEndpointRouteB IMediator mediator, CancellationToken ct) => { var result = await mediator.Send(new GetCountryCodeByIdQuery(id), ct).ConfigureAwait(false); - return result.Success ? Results.Ok(result) : Results.NotFound(result); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Lookup_Manage) .WithName("GetCountryCodeById"); @@ -48,9 +49,7 @@ public static IEndpointRouteBuilder MapCountryCodeEndpoints(this IEndpointRouteB body.FlagUrl, body.IsActive); var result = await mediator.Send(cmd, ct).ConfigureAwait(false); - return result.Success - ? Results.Ok(result) - : Results.BadRequest(result); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Lookup_Manage) .WithName("UpsertCountryCode"); diff --git a/backend/src/CCE.Api.Internal/Endpoints/CountryEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/CountryEndpoints.cs index 649bbe7e..63e2ca4c 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/CountryEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/CountryEndpoints.cs @@ -41,8 +41,8 @@ public static IEndpointRouteBuilder MapCountryEndpoints(this IEndpointRouteBuild System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - var dto = await mediator.Send(new GetCountryByIdQuery(id), cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var result = await mediator.Send(new GetCountryByIdQuery(id), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Country_Profile_Update) .WithName("GetCountryById"); @@ -57,8 +57,8 @@ public static IEndpointRouteBuilder MapCountryEndpoints(this IEndpointRouteBuild body.NameAr, body.NameEn, body.RegionAr, body.RegionEn, body.IsActive); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var result = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Country_Profile_Update) .WithName("UpdateCountry"); diff --git a/backend/src/CCE.Api.Internal/Endpoints/CountryProfileEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/CountryProfileEndpoints.cs index 59006e22..75ec1c4c 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/CountryProfileEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/CountryProfileEndpoints.cs @@ -20,8 +20,8 @@ public static IEndpointRouteBuilder MapCountryProfileEndpoints(this IEndpointRou group.MapGet("", async ( System.Guid countryId, IMediator mediator, CancellationToken cancellationToken) => { - var dto = await mediator.Send(new GetCountryProfileQuery(countryId), cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var result = await mediator.Send(new GetCountryProfileQuery(countryId), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Country_Profile_Update) .WithName("GetCountryProfile"); diff --git a/backend/src/CCE.Api.Internal/Endpoints/ExpertEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/ExpertEndpoints.cs index 2d961856..8f8a48ca 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/ExpertEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/ExpertEndpoints.cs @@ -29,7 +29,7 @@ public static IEndpointRouteBuilder MapExpertEndpoints(this IEndpointRouteBuilde Status: status, RequestedById: requestedById); var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Community_Expert_ApproveRequest) .WithName("ListExpertRequests"); @@ -79,7 +79,7 @@ public static IEndpointRouteBuilder MapExpertEndpoints(this IEndpointRouteBuilde PageSize: pageSize ?? 20, Search: search); var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Community_Expert_ApproveRequest) .WithName("ListExpertProfiles"); diff --git a/backend/src/CCE.Api.Internal/Endpoints/HomepageSectionEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/HomepageSectionEndpoints.cs index 3ab928c5..52b2d95b 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/HomepageSectionEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/HomepageSectionEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Content.Commands.CreateHomepageSection; using CCE.Application.Content.Commands.DeleteHomepageSection; using CCE.Application.Content.Commands.ReorderHomepageSections; @@ -20,7 +21,7 @@ public static IEndpointRouteBuilder MapHomepageSectionEndpoints(this IEndpointRo sections.MapGet("", async (IMediator mediator, CancellationToken cancellationToken) => { var result = await mediator.Send(new ListHomepageSectionsQuery(), cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Page_Edit) .WithName("ListHomepageSections"); @@ -28,8 +29,8 @@ public static IEndpointRouteBuilder MapHomepageSectionEndpoints(this IEndpointRo sections.MapPost("", async (CreateHomepageSectionRequest body, IMediator mediator, CancellationToken cancellationToken) => { var cmd = new CreateHomepageSectionCommand(body.SectionType, body.OrderIndex, body.ContentAr, body.ContentEn); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return Results.Created($"/api/admin/homepage-sections/{dto.Id}", dto); + var result = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return result.ToCreatedHttpResult(); }) .RequireAuthorization(Permissions.Page_Edit) .WithName("CreateHomepageSection"); @@ -40,8 +41,8 @@ public static IEndpointRouteBuilder MapHomepageSectionEndpoints(this IEndpointRo IMediator mediator, CancellationToken cancellationToken) => { var cmd = new UpdateHomepageSectionCommand(id, body.ContentAr, body.ContentEn, body.IsActive); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var result = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Page_Edit) .WithName("UpdateHomepageSection"); @@ -50,8 +51,8 @@ public static IEndpointRouteBuilder MapHomepageSectionEndpoints(this IEndpointRo System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - await mediator.Send(new DeleteHomepageSectionCommand(id), cancellationToken).ConfigureAwait(false); - return Results.NoContent(); + var result = await mediator.Send(new DeleteHomepageSectionCommand(id), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Page_Edit) .WithName("DeleteHomepageSection"); @@ -63,8 +64,8 @@ public static IEndpointRouteBuilder MapHomepageSectionEndpoints(this IEndpointRo var assignments = body.Assignments .Select(a => new HomepageSectionOrderAssignment(a.Id, a.OrderIndex)) .ToList(); - await mediator.Send(new ReorderHomepageSectionsCommand(assignments), cancellationToken).ConfigureAwait(false); - return Results.NoContent(); + var result = await mediator.Send(new ReorderHomepageSectionsCommand(assignments), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Page_Edit) .WithName("ReorderHomepageSections"); diff --git a/backend/src/CCE.Api.Internal/Endpoints/IdentityEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/IdentityEndpoints.cs index f10126a0..04bb5f60 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/IdentityEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/IdentityEndpoints.cs @@ -224,7 +224,7 @@ Claims not held by the user are ignored. CountryId: countryId, Active: active ?? true); var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Role_Assign) .WithName("ListStateRepAssignments"); diff --git a/backend/src/CCE.Api.Internal/Endpoints/PageEndpoints.cs b/backend/src/CCE.Api.Internal/Endpoints/PageEndpoints.cs index b4853a70..ad435fe4 100644 --- a/backend/src/CCE.Api.Internal/Endpoints/PageEndpoints.cs +++ b/backend/src/CCE.Api.Internal/Endpoints/PageEndpoints.cs @@ -1,3 +1,4 @@ +using CCE.Api.Common.Extensions; using CCE.Application.Content.Commands.CreatePage; using CCE.Application.Content.Commands.DeletePage; using CCE.Application.Content.Commands.UpdatePage; @@ -24,15 +25,15 @@ public static IEndpointRouteBuilder MapPageEndpoints(this IEndpointRouteBuilder { var query = new ListPagesQuery(page ?? 1, pageSize ?? 20, search, pageType); var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false); - return Results.Ok(result); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Page_Edit) .WithName("ListPages"); pages.MapGet("/{id:guid}", async (System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - var dto = await mediator.Send(new GetPageByIdQuery(id), cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var result = await mediator.Send(new GetPageByIdQuery(id), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Page_Edit) .WithName("GetPageById"); @@ -40,8 +41,8 @@ public static IEndpointRouteBuilder MapPageEndpoints(this IEndpointRouteBuilder pages.MapPost("", async (CreatePageRequest body, IMediator mediator, CancellationToken cancellationToken) => { var cmd = new CreatePageCommand(body.Slug, body.PageType, body.TitleAr, body.TitleEn, body.ContentAr, body.ContentEn); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return Results.Created($"/api/admin/pages/{dto.Id}", dto); + var result = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return result.ToCreatedHttpResult(); }) .RequireAuthorization(Permissions.Page_Edit) .WithName("CreatePage"); @@ -53,8 +54,8 @@ public static IEndpointRouteBuilder MapPageEndpoints(this IEndpointRouteBuilder { var rowVersion = string.IsNullOrEmpty(body.RowVersion) ? System.Array.Empty() : System.Convert.FromBase64String(body.RowVersion); var cmd = new UpdatePageCommand(id, body.TitleAr, body.TitleEn, body.ContentAr, body.ContentEn, rowVersion); - var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); - return dto is null ? Results.NotFound() : Results.Ok(dto); + var result = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Page_Edit) .WithName("UpdatePage"); @@ -63,8 +64,8 @@ public static IEndpointRouteBuilder MapPageEndpoints(this IEndpointRouteBuilder System.Guid id, IMediator mediator, CancellationToken cancellationToken) => { - await mediator.Send(new DeletePageCommand(id), cancellationToken).ConfigureAwait(false); - return Results.NoContent(); + var result = await mediator.Send(new DeletePageCommand(id), cancellationToken).ConfigureAwait(false); + return result.ToHttpResult(); }) .RequireAuthorization(Permissions.Page_Edit) .WithName("DeletePage"); diff --git a/backend/src/CCE.Application/Audit/Queries/ListAuditEvents/ListAuditEventsQuery.cs b/backend/src/CCE.Application/Audit/Queries/ListAuditEvents/ListAuditEventsQuery.cs index 5521e214..0666d945 100644 --- a/backend/src/CCE.Application/Audit/Queries/ListAuditEvents/ListAuditEventsQuery.cs +++ b/backend/src/CCE.Application/Audit/Queries/ListAuditEvents/ListAuditEventsQuery.cs @@ -1,4 +1,5 @@ using CCE.Application.Audit.Dtos; +using CCE.Application.Common; using CCE.Application.Common.Pagination; using MediatR; @@ -12,4 +13,4 @@ public sealed record ListAuditEventsQuery( string? ResourceType = null, System.Guid? CorrelationId = null, System.DateTimeOffset? From = null, - System.DateTimeOffset? To = null) : IRequest>; + System.DateTimeOffset? To = null) : IRequest>>; diff --git a/backend/src/CCE.Application/Audit/Queries/ListAuditEvents/ListAuditEventsQueryHandler.cs b/backend/src/CCE.Application/Audit/Queries/ListAuditEvents/ListAuditEventsQueryHandler.cs index c5271741..644fe3bf 100644 --- a/backend/src/CCE.Application/Audit/Queries/ListAuditEvents/ListAuditEventsQueryHandler.cs +++ b/backend/src/CCE.Application/Audit/Queries/ListAuditEvents/ListAuditEventsQueryHandler.cs @@ -1,22 +1,26 @@ -using CCE.Application.Audit.Dtos; +using CCE.Application.Audit.Dtos; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; +using CCE.Application.Messages; using CCE.Domain.Audit; using MediatR; namespace CCE.Application.Audit.Queries.ListAuditEvents; public sealed class ListAuditEventsQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListAuditEventsQueryHandler(ICceDbContext db) + public ListAuditEventsQueryHandler(ICceDbContext db, MessageFactory msg) { _db = db; + _msg = msg; } - public async Task> Handle( + public async Task>> Handle( ListAuditEventsQuery request, CancellationToken cancellationToken) { @@ -48,7 +52,7 @@ public async Task> Handle( var items = page.Items.Select(MapToDto).ToList(); - return new PagedResult(items, page.Page, page.PageSize, page.Total); + return _msg.Ok(new PagedResult(items, page.Page, page.PageSize, page.Total), MessageKeys.General.ITEMS_LISTED); } private static AuditEventDto MapToDto(AuditEvent e) => diff --git a/backend/src/CCE.Application/Cache/EvictCacheKeyCommandHandler.cs b/backend/src/CCE.Application/Cache/EvictCacheKeyCommandHandler.cs index 11a712b9..0aa1ee9a 100644 --- a/backend/src/CCE.Application/Cache/EvictCacheKeyCommandHandler.cs +++ b/backend/src/CCE.Application/Cache/EvictCacheKeyCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Caching; using CCE.Application.Messages; using MediatR; @@ -21,6 +21,6 @@ public async Task> Handle( EvictCacheKeyCommand request, CancellationToken cancellationToken) { await _cache.EvictKeyAsync(request.Key, cancellationToken).ConfigureAwait(false); - return _messages.Ok("SUCCESS_DELETED"); + return _messages.Ok(MessageKeys.General.SUCCESS_DELETED); } } diff --git a/backend/src/CCE.Application/Cache/EvictCacheRegionCommandHandler.cs b/backend/src/CCE.Application/Cache/EvictCacheRegionCommandHandler.cs index 81aff179..58c6a98e 100644 --- a/backend/src/CCE.Application/Cache/EvictCacheRegionCommandHandler.cs +++ b/backend/src/CCE.Application/Cache/EvictCacheRegionCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Caching; using CCE.Application.Messages; using MediatR; @@ -21,6 +21,6 @@ public async Task> Handle( EvictCacheRegionCommand request, CancellationToken cancellationToken) { await _cache.EvictRegionsAsync([request.Region], cancellationToken).ConfigureAwait(false); - return _messages.Ok("SUCCESS_OPERATION"); + return _messages.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Cache/FlushCacheCommandHandler.cs b/backend/src/CCE.Application/Cache/FlushCacheCommandHandler.cs index 483c1ddc..0f6ee41c 100644 --- a/backend/src/CCE.Application/Cache/FlushCacheCommandHandler.cs +++ b/backend/src/CCE.Application/Cache/FlushCacheCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Caching; using CCE.Application.Messages; using MediatR; @@ -21,6 +21,6 @@ public async Task> Handle( FlushCacheCommand request, CancellationToken cancellationToken) { await _cache.FlushAllAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok("SUCCESS_OPERATION"); + return _messages.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Cache/GetCacheRegionsQueryHandler.cs b/backend/src/CCE.Application/Cache/GetCacheRegionsQueryHandler.cs index 4e62eea7..ec802a8c 100644 --- a/backend/src/CCE.Application/Cache/GetCacheRegionsQueryHandler.cs +++ b/backend/src/CCE.Application/Cache/GetCacheRegionsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Caching; using CCE.Application.Messages; using MediatR; @@ -21,6 +21,6 @@ public async Task>> Handle( GetCacheRegionsQuery request, CancellationToken cancellationToken) { var status = await _cache.GetStatusAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok(status, "ITEMS_LISTED"); + return _messages.Ok(status, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Cache/ListRedisKeysQuery.cs b/backend/src/CCE.Application/Cache/ListRedisKeysQuery.cs index 8ecd3818..b4994700 100644 --- a/backend/src/CCE.Application/Cache/ListRedisKeysQuery.cs +++ b/backend/src/CCE.Application/Cache/ListRedisKeysQuery.cs @@ -51,6 +51,6 @@ public async Task>> Handle( infos.Add(new RedisKeyInfo(key, type, value)); } - return _messages.Ok>(infos, "ITEMS_LISTED"); + return _messages.Ok>(infos, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Common/Behaviors/LoggingBehavior.cs b/backend/src/CCE.Application/Common/Behaviors/LoggingBehavior.cs deleted file mode 100644 index 4b8ee436..00000000 --- a/backend/src/CCE.Application/Common/Behaviors/LoggingBehavior.cs +++ /dev/null @@ -1,38 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using System.Diagnostics; - -namespace CCE.Application.Common.Behaviors; - -/// -/// MediatR pipeline behavior that logs handler entry, success, and elapsed time. -/// Logs at . Exceptions are not caught — they escape -/// to the next pipeline stage (typically the API middleware that converts to ProblemDetails). -/// -public sealed class LoggingBehavior : IPipelineBehavior - where TRequest : notnull -{ - private readonly ILogger> _logger; - - public LoggingBehavior(ILogger> logger) => _logger = logger; - - public async Task Handle( - TRequest request, - RequestHandlerDelegate next, - CancellationToken cancellationToken) - { - var requestName = typeof(TRequest).Name; - //_logger.LogInformation("Handling {RequestName}", requestName); - - var sw = Stopwatch.StartNew(); - var response = await next().ConfigureAwait(false); - sw.Stop(); - - //_logger.LogInformation( - // "Handled {RequestName} in {ElapsedMs}ms", - // requestName, - // sw.ElapsedMilliseconds); - - return response; - } -} diff --git a/backend/src/CCE.Application/Common/Behaviors/ResponseValidationBehavior.cs b/backend/src/CCE.Application/Common/Behaviors/ResponseValidationBehavior.cs index 769af6c3..f6469236 100644 --- a/backend/src/CCE.Application/Common/Behaviors/ResponseValidationBehavior.cs +++ b/backend/src/CCE.Application/Common/Behaviors/ResponseValidationBehavior.cs @@ -1,6 +1,3 @@ -using CCE.Application.Localization; -using CCE.Application.Messages; -using CCE.Domain.Common; using FluentValidation; using MediatR; @@ -11,14 +8,10 @@ public sealed class ResponseValidationBehavior where TRequest : notnull { private readonly IEnumerable> _validators; - private readonly ILocalizationService _l; - public ResponseValidationBehavior( - IEnumerable> validators, - ILocalizationService l) + public ResponseValidationBehavior(IEnumerable> validators) { _validators = validators; - _l = l; } public async Task Handle( @@ -41,44 +34,6 @@ public async Task Handle( if (failures.Count == 0) return await next().ConfigureAwait(false); - var responseType = typeof(TResponse); - if (responseType.IsGenericType && - responseType.GetGenericTypeDefinition() == typeof(Response<>)) - { - var fieldErrors = failures.Select(f => - { - var domainKey = f.ErrorCode; - var valCode = SystemCodeMap.ToSystemCode(domainKey); - var msg = _l.GetString(domainKey); - if (msg == domainKey) msg = f.ErrorMessage; - return new FieldError( - ToCamelCase(f.PropertyName), - valCode, - msg); - }).ToList(); - - var headerDomainKey = "VALIDATION_ERROR"; - var headerCode = SystemCodeMap.ToSystemCode(headerDomainKey); - var headerMsg = _l.GetString(headerDomainKey); - - var failMethod = responseType.GetMethod("Fail", - new[] { typeof(string), typeof(string), typeof(MessageType), typeof(IReadOnlyList) }); - - return (TResponse)failMethod!.Invoke(null, new object[] - { - headerCode, - headerMsg, - MessageType.Validation, - fieldErrors - })!; - } - throw new ValidationException(failures); } - - private static string ToCamelCase(string name) - { - if (string.IsNullOrEmpty(name)) return name; - return char.ToLowerInvariant(name[0]) + name[1..]; - } } diff --git a/backend/src/CCE.Application/Common/Behaviors/ResultValidationBehavior.cs b/backend/src/CCE.Application/Common/Behaviors/ResultValidationBehavior.cs deleted file mode 100644 index 6d20f79b..00000000 --- a/backend/src/CCE.Application/Common/Behaviors/ResultValidationBehavior.cs +++ /dev/null @@ -1,82 +0,0 @@ -using CCE.Application.Localization; -using CCE.Domain.Common; -using FluentValidation; -using MediatR; -using Microsoft.Extensions.DependencyInjection; - -namespace CCE.Application.Common.Behaviors; - -/// -/// MediatR pipeline behavior for requests returning . -/// Instead of throwing , it returns a failure Result -/// with localized messages and structured field-level details. -/// -public sealed class ResultValidationBehavior - : IPipelineBehavior - where TRequest : notnull - where TResponse : class -{ - private readonly IEnumerable> _validators; - private readonly IServiceProvider _serviceProvider; - - public ResultValidationBehavior( - IEnumerable> validators, - IServiceProvider serviceProvider) - { - _validators = validators; - _serviceProvider = serviceProvider; - } - - public async Task Handle( - TRequest request, - RequestHandlerDelegate next, - CancellationToken cancellationToken) - { - // Only intercept when TResponse is Result - if (!IsResultType(typeof(TResponse))) - { - return await next().ConfigureAwait(false); - } - - if (!_validators.Any()) - return await next().ConfigureAwait(false); - - var context = new ValidationContext(request); - var results = await Task.WhenAll( - _validators.Select(v => v.ValidateAsync(context, cancellationToken))) - .ConfigureAwait(false); - - var failures = results.SelectMany(r => r.Errors) - .Where(f => f is not null) - .ToList(); - - if (failures.Count == 0) - return await next().ConfigureAwait(false); - - var details = failures - .GroupBy(f => f.PropertyName) - .ToDictionary( - g => g.Key, - g => g.Select(f => f.ErrorMessage).ToArray()); - - var localization = _serviceProvider.GetRequiredService(); - var msg = localization.GetLocalizedMessage("GENERAL_VALIDATION_ERROR"); - var error = new Error( - "GENERAL_VALIDATION_ERROR", - msg?.Ar ?? "عذرًا، البيانات المدخلة غير صحيحة", - msg?.En ?? "Sorry, the entered data is invalid", - ErrorType.Validation, - details); - - // Use reflection to call Result.Failure(error) - var innerType = typeof(TResponse).GetGenericArguments()[0]; - var failureMethod = typeof(Result<>) - .MakeGenericType(innerType) - .GetMethod("Failure")!; - - return (TResponse)failureMethod.Invoke(null, [error])!; - } - - private static bool IsResultType(Type type) - => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Result<>); -} diff --git a/backend/src/CCE.Application/Common/Behaviors/ValidationBehavior.cs b/backend/src/CCE.Application/Common/Behaviors/ValidationBehavior.cs deleted file mode 100644 index a8e8d5cb..00000000 --- a/backend/src/CCE.Application/Common/Behaviors/ValidationBehavior.cs +++ /dev/null @@ -1,40 +0,0 @@ -using FluentValidation; -using MediatR; - -namespace CCE.Application.Common.Behaviors; - -/// -/// MediatR pipeline behavior that runs every registered for the request. -/// Aggregates all failures across validators into a single . -/// -public sealed class ValidationBehavior : IPipelineBehavior - where TRequest : notnull -{ - private readonly IEnumerable> _validators; - - public ValidationBehavior(IEnumerable> validators) => _validators = validators; - - public async Task Handle( - TRequest request, - RequestHandlerDelegate next, - CancellationToken cancellationToken) - { - if (!_validators.Any()) - { - return await next().ConfigureAwait(false); - } - - var context = new ValidationContext(request); - var results = await Task.WhenAll( - _validators.Select(v => v.ValidateAsync(context, cancellationToken))) - .ConfigureAwait(false); - - var failures = results.SelectMany(r => r.Errors).Where(f => f is not null).ToList(); - if (failures.Count > 0) - { - throw new ValidationException(failures); - } - - return await next().ConfigureAwait(false); - } -} diff --git a/backend/src/CCE.Application/Common/Response.cs b/backend/src/CCE.Application/Common/Response.cs index fbfdb3d9..7c304997 100644 --- a/backend/src/CCE.Application/Common/Response.cs +++ b/backend/src/CCE.Application/Common/Response.cs @@ -5,7 +5,6 @@ namespace CCE.Application.Common; /// /// Unified API response envelope. Every endpoint returns this shape. -/// Replaces with proper success messages and error arrays. /// Code field uses ERR0xx/CON0xx/VAL0xx numbering. /// Message is a single string in the language requested via Accept-Language header. /// @@ -24,6 +23,7 @@ public sealed record Response : IResponse [JsonInclude] public T? Data { get; private init; } [JsonInclude] public IReadOnlyList Errors { get; private init; } = []; [JsonInclude] public string TraceId { get; init; } = string.Empty; + [JsonInclude] public string CorrelationId { get; init; } = string.Empty; [JsonInclude] public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow; /// Not serialized — used internally to select HTTP status. diff --git a/backend/src/CCE.Application/Common/Result.cs b/backend/src/CCE.Application/Common/Result.cs deleted file mode 100644 index 3454e128..00000000 --- a/backend/src/CCE.Application/Common/Result.cs +++ /dev/null @@ -1,51 +0,0 @@ -using CCE.Domain.Common; -using System.Text.Json.Serialization; - -namespace CCE.Application.Common; - -/// -/// Discriminated result type for handler returns. Replaces returning null (not-found) -/// and throwing exceptions for expected business failures. -/// Designed to serialize cleanly with System.Text.Json. -/// -public sealed record Result -{ - [JsonInclude] - public bool IsSuccess { get; private init; } - - [JsonInclude] - public T? Data { get; private init; } - - [JsonInclude] - public Error? Error { get; private init; } - - // Public parameterless constructor so System.Text.Json can instantiate - // the record during serialization (records create temp instances). - public Result() { } - - public static Result Success(T data) => new() { IsSuccess = true, Data = data }; - public static Result Failure(Error error) => new() { IsSuccess = false, Error = error }; - - /// Allow implicit conversion from T for clean handler returns. - public static implicit operator Result(T data) => Success(data); - - /// Allow implicit conversion from Error for clean handler returns. - public static implicit operator Result(Error error) => Failure(error); -} - -/// -/// Non-generic companion for void commands that return no data on success. -/// -public static class Result -{ - private static readonly Result SuccessUnit = Result.Success(Unit.Value); - - public static Result Success() => SuccessUnit; - public static Result Failure(Error error) => Result.Failure(error); -} - -/// Unit type for commands that return no data. -public readonly record struct Unit -{ - public static readonly Unit Value = default; -} diff --git a/backend/src/CCE.Application/Community/Commands/ApproveJoinRequest/ApproveJoinRequestCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/ApproveJoinRequest/ApproveJoinRequestCommandHandler.cs index 197f0ce7..12a8966f 100644 --- a/backend/src/CCE.Application/Community/Commands/ApproveJoinRequest/ApproveJoinRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/ApproveJoinRequest/ApproveJoinRequestCommandHandler.cs @@ -1,7 +1,7 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -34,7 +34,7 @@ public async Task> Handle(ApproveJoinRequestCommand request, if (by is null || by == Guid.Empty) return _msg.NotAuthenticated(); var joinRequest = await _repo.GetRequestAsync(request.RequestId, cancellationToken).ConfigureAwait(false); - if (joinRequest is null) return _msg.NotFound(ApplicationErrors.Community.JOIN_REQUEST_NOT_FOUND); + if (joinRequest is null) return _msg.NotFound(MessageKeys.Community.JOIN_REQUEST_NOT_FOUND); joinRequest.Approve(by.Value, _clock); @@ -47,6 +47,6 @@ public async Task> Handle(ApproveJoinRequestCommand request, } await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(ApplicationErrors.General.SUCCESS_OPERATION); + return _msg.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/CastPollVote/CastPollVoteCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/CastPollVote/CastPollVoteCommandHandler.cs index 942d4316..fb4a90cf 100644 --- a/backend/src/CCE.Application/Community/Commands/CastPollVote/CastPollVoteCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/CastPollVote/CastPollVoteCommandHandler.cs @@ -1,9 +1,9 @@ -using System.Linq; +using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Realtime; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -43,13 +43,13 @@ public async Task> Handle(CastPollVoteCommand request, Cancel var optionIds = request.OptionIds.Distinct().ToList(); if (optionIds.Count == 0) - return _msg.BusinessRule(ApplicationErrors.Validation.REQUIRED_FIELD); + return _msg.BusinessRule(MessageKeys.Validation.REQUIRED_FIELD); var poll = await _repo.GetWithOptionsAsync(request.PollId, cancellationToken).ConfigureAwait(false); - if (poll is null) return _msg.NotFound(ApplicationErrors.Community.POLL_NOT_FOUND); - if (poll.IsClosed(_clock)) return _msg.BusinessRule(ApplicationErrors.Community.POLL_CLOSED); + if (poll is null) return _msg.NotFound(MessageKeys.Community.POLL_NOT_FOUND); + if (poll.IsClosed(_clock)) return _msg.BusinessRule(MessageKeys.Community.POLL_CLOSED); if (!poll.AllowMultiple && optionIds.Count > 1) - return _msg.BusinessRule(ApplicationErrors.Validation.INVALID_FORMAT); + return _msg.BusinessRule(MessageKeys.Validation.INVALID_FORMAT); var existingVotes = await _repo.RemoveVotesAsync(poll.Id, userId.Value, cancellationToken).ConfigureAwait(false); foreach (var oldVote in existingVotes) @@ -61,7 +61,7 @@ public async Task> Handle(CastPollVoteCommand request, Cancel foreach (var optionId in optionIds) { var option = poll.FindOption(optionId); - if (option is null) return _msg.NotFound(ApplicationErrors.Community.POLL_NOT_FOUND); + if (option is null) return _msg.NotFound(MessageKeys.Community.POLL_NOT_FOUND); _repo.AddVote(PollVote.Cast(poll.Id, option.Id, userId.Value, _clock)); option.IncrementVotes(); } @@ -85,6 +85,6 @@ await _realtime.PublishToPostAsync(poll.PostId, RealtimeEvents.PollResultsChange }), }, cancellationToken).ConfigureAwait(false); - return _msg.Ok(ApplicationErrors.General.SUCCESS_OPERATION); + return _msg.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/ChangeCommunityVisibility/ChangeCommunityVisibilityCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/ChangeCommunityVisibility/ChangeCommunityVisibilityCommandHandler.cs index 3dd760c8..ee745066 100644 --- a/backend/src/CCE.Application/Community/Commands/ChangeCommunityVisibility/ChangeCommunityVisibilityCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/ChangeCommunityVisibility/ChangeCommunityVisibilityCommandHandler.cs @@ -1,7 +1,7 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Errors; using CCE.Application.Messages; + using MediatR; namespace CCE.Application.Community.Commands.ChangeCommunityVisibility; @@ -23,10 +23,10 @@ public ChangeCommunityVisibilityCommandHandler(ICommunityRepository repo, ICceDb public async Task> Handle(ChangeCommunityVisibilityCommand request, CancellationToken cancellationToken) { var community = await _repo.GetAsync(request.CommunityId, cancellationToken).ConfigureAwait(false); - if (community is null) return _msg.NotFound(ApplicationErrors.Community.COMMUNITY_NOT_FOUND); + if (community is null) return _msg.NotFound(MessageKeys.Community.COMMUNITY_NOT_FOUND); community.ChangeVisibility(request.Visibility); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(ApplicationErrors.General.SUCCESS_UPDATED); + return _msg.Ok(MessageKeys.General.SUCCESS_UPDATED); } } diff --git a/backend/src/CCE.Application/Community/Commands/CreateCommunity/CreateCommunityCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/CreateCommunity/CreateCommunityCommandHandler.cs index 7577f9c9..7ee233b3 100644 --- a/backend/src/CCE.Application/Community/Commands/CreateCommunity/CreateCommunityCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/CreateCommunity/CreateCommunityCommandHandler.cs @@ -1,7 +1,7 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -34,7 +34,7 @@ public async Task> Handle(CreateCommunityCommand request, Cancell if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); if (await _repo.SlugExistsAsync(request.Slug, cancellationToken).ConfigureAwait(false)) - return _msg.Conflict(ApplicationErrors.General.DUPLICATE_VALUE); + return _msg.Conflict(MessageKeys.General.DUPLICATE_VALUE); var community = Domain.Community.Community.Create( request.NameAr, request.NameEn, request.DescriptionAr, request.DescriptionEn, @@ -45,6 +45,6 @@ public async Task> Handle(CreateCommunityCommand request, Cancell community.IncrementMembers(); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(community.Id, ApplicationErrors.General.SUCCESS_CREATED); + return _msg.Ok(community.Id, MessageKeys.General.SUCCESS_CREATED); } } diff --git a/backend/src/CCE.Application/Community/Commands/CreateCommunity/CreateCommunityCommandValidator.cs b/backend/src/CCE.Application/Community/Commands/CreateCommunity/CreateCommunityCommandValidator.cs index b82eb36a..de994eb8 100644 --- a/backend/src/CCE.Application/Community/Commands/CreateCommunity/CreateCommunityCommandValidator.cs +++ b/backend/src/CCE.Application/Community/Commands/CreateCommunity/CreateCommunityCommandValidator.cs @@ -1,4 +1,4 @@ -using CCE.Application.Errors; +using CCE.Application.Messages; using CCE.Domain.Community; using FluentValidation; @@ -8,11 +8,11 @@ public sealed class CreateCommunityCommandValidator : AbstractValidator x.NameAr).NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(CCE.Domain.Community.Community.MaxNameLength).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); - RuleFor(x => x.NameEn).NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(CCE.Domain.Community.Community.MaxNameLength).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); - RuleFor(x => x.Slug).NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD); - RuleFor(x => x.Visibility).IsInEnum().WithErrorCode(ApplicationErrors.Validation.INVALID_ENUM); + RuleFor(x => x.NameAr).NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(CCE.Domain.Community.Community.MaxNameLength).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + RuleFor(x => x.NameEn).NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(CCE.Domain.Community.Community.MaxNameLength).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + RuleFor(x => x.Slug).NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); + RuleFor(x => x.Visibility).IsInEnum().WithErrorCode(MessageKeys.Validation.INVALID_ENUM); } } diff --git a/backend/src/CCE.Application/Community/Commands/CreatePost/CreatePostCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/CreatePost/CreatePostCommandHandler.cs index 5347099b..c1c78022 100644 --- a/backend/src/CCE.Application/Community/Commands/CreatePost/CreatePostCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/CreatePost/CreatePostCommandHandler.cs @@ -1,10 +1,10 @@ -using System.Linq; +using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Sanitization; -using CCE.Application.Errors; -using CCE.Application.Identity; using CCE.Application.Messages; +using CCE.Application.Identity; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -73,7 +73,7 @@ public async Task> Handle(CreatePostCommand request, Cancellation } else { - return _msg.Forbidden(ApplicationErrors.General.FORBIDDEN); + return _msg.Forbidden(MessageKeys.General.FORBIDDEN); } } @@ -93,7 +93,7 @@ public async Task> Handle(CreatePostCommand request, Cancellation if (request.Attachments.Count > 0) { if (request.Attachments.Count > Post.MaxAttachments) - return _msg.BusinessRule(ApplicationErrors.Media.FILE_TOO_LARGE); + return _msg.BusinessRule(MessageKeys.Media.FILE_TOO_LARGE); var assetIds = request.Attachments.Select(a => a.AssetFileId).Distinct().ToList(); var assets = (await _repo.GetAssetsAsync(assetIds, cancellationToken).ConfigureAwait(false)) @@ -122,7 +122,7 @@ public async Task> Handle(CreatePostCommand request, Cancellation if (request.Type == PostType.Poll) { if (request.Poll is null) - return _msg.BusinessRule(ApplicationErrors.Validation.REQUIRED_FIELD); + return _msg.BusinessRule(MessageKeys.Validation.REQUIRED_FIELD); var poll = Poll.Create(post.Id, request.Poll.Deadline, request.Poll.AllowMultiple, request.Poll.IsAnonymous, request.Poll.ShowResultsBeforeClose, request.Poll.OptionLabels, _clock); _pollRepo.AddPoll(poll); @@ -144,7 +144,7 @@ public async Task> Handle(CreatePostCommand request, Cancellation // Worker: Post.Publish raises PostCreatedEvent → PostCreatedBusPublisher → SignalRConsumer. The // API stays publish-only here (no direct SignalR push). return _msg.Ok(post.Id, request.SaveAsDraft - ? ApplicationErrors.Community.POST_DRAFT_SAVED - : ApplicationErrors.Community.POST_CREATED); + ? MessageKeys.Community.POST_DRAFT_SAVED + : MessageKeys.Community.POST_CREATED); } } diff --git a/backend/src/CCE.Application/Community/Commands/CreatePost/CreatePostCommandValidator.cs b/backend/src/CCE.Application/Community/Commands/CreatePost/CreatePostCommandValidator.cs index 420ff1b1..4b561d49 100644 --- a/backend/src/CCE.Application/Community/Commands/CreatePost/CreatePostCommandValidator.cs +++ b/backend/src/CCE.Application/Community/Commands/CreatePost/CreatePostCommandValidator.cs @@ -1,4 +1,4 @@ -using CCE.Application.Errors; +using CCE.Application.Messages; using CCE.Domain.Community; using FluentValidation; @@ -8,15 +8,15 @@ public sealed class CreatePostCommandValidator : AbstractValidator x.CommunityId).NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD); - RuleFor(x => x.TopicId).NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD); - RuleFor(x => x.Type).IsInEnum().WithErrorCode(ApplicationErrors.Validation.INVALID_ENUM); + RuleFor(x => x.CommunityId).NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); + RuleFor(x => x.TopicId).NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); + RuleFor(x => x.Type).IsInEnum().WithErrorCode(MessageKeys.Validation.INVALID_ENUM); RuleFor(x => x.Title) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(Post.MaxTitleLength).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(Post.MaxTitleLength).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.Content) - .MaximumLength(Post.MaxContentLength).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .MaximumLength(Post.MaxContentLength).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.Locale) - .Must(l => l is "ar" or "en").WithErrorCode(ApplicationErrors.Validation.INVALID_ENUM); + .Must(l => l is "ar" or "en").WithErrorCode(MessageKeys.Validation.INVALID_ENUM); } } diff --git a/backend/src/CCE.Application/Community/Commands/CreateReply/CreateReplyCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/CreateReply/CreateReplyCommandHandler.cs index 800a9f40..122a46ba 100644 --- a/backend/src/CCE.Application/Community/Commands/CreateReply/CreateReplyCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/CreateReply/CreateReplyCommandHandler.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Realtime; using CCE.Application.Common.Sanitization; -using CCE.Application.Errors; -using CCE.Application.Identity; using CCE.Application.Messages; +using CCE.Application.Identity; + using CCE.Application.Notifications.Messages; using CCE.Domain.Common; using CCE.Domain.Community; @@ -55,7 +55,7 @@ public async Task> Handle(CreateReplyCommand request, Cancellatio if (authorId is null || authorId == Guid.Empty) return _msg.NotAuthenticated(); var post = await _repo.GetPostAsync(request.PostId, cancellationToken).ConfigureAwait(false); - if (post is null) return _msg.NotFound(ApplicationErrors.Community.POST_NOT_FOUND); + if (post is null) return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); var content = _sanitizer.Sanitize(request.Content); @@ -64,7 +64,7 @@ public async Task> Handle(CreateReplyCommand request, Cancellatio { var parent = await _repo.GetParentAsync(parentId, cancellationToken).ConfigureAwait(false); if (parent is null || parent.PostId != post.Id) - return _msg.NotFound(ApplicationErrors.Community.REPLY_NOT_FOUND); + return _msg.NotFound(MessageKeys.Community.REPLY_NOT_FOUND); reply = PostReply.CreateChild(parent, authorId.Value, content, request.Locale, isByExpert: false, _clock); } else @@ -121,7 +121,7 @@ await _dispatcher.DispatchAsync(new NotificationMessage( Locale: request.Locale), cancellationToken).ConfigureAwait(false); } - return _msg.Ok(reply.Id, ApplicationErrors.General.SUCCESS_CREATED); + return _msg.Ok(reply.Id, MessageKeys.General.SUCCESS_CREATED); } private async Task> PersistMentionsAsync( diff --git a/backend/src/CCE.Application/Community/Commands/CreateTopic/CreateTopicCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/CreateTopic/CreateTopicCommandHandler.cs index c78bef4c..6b5e344d 100644 --- a/backend/src/CCE.Application/Community/Commands/CreateTopic/CreateTopicCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/CreateTopic/CreateTopicCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Community.Dtos; using CCE.Application.Community.Queries.ListTopics; @@ -39,6 +39,6 @@ public async Task> Handle(CreateTopicCommand request, Cancell await _repo.AddAsync(topic, cancellationToken).ConfigureAwait(false); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok(ListTopicsQueryHandler.MapToDto(topic), "CONTENT_CREATED"); + return _messages.Ok(ListTopicsQueryHandler.MapToDto(topic), MessageKeys.Content.CONTENT_CREATED); } } diff --git a/backend/src/CCE.Application/Community/Commands/DeleteDraft/DeleteDraftCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/DeleteDraft/DeleteDraftCommandHandler.cs index f052eb1f..bf7d88a8 100644 --- a/backend/src/CCE.Application/Community/Commands/DeleteDraft/DeleteDraftCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/DeleteDraft/DeleteDraftCommandHandler.cs @@ -1,7 +1,7 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Community; using MediatR; @@ -30,13 +30,13 @@ public async Task> Handle(DeleteDraftCommand request, Cancell if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); var post = await _repo.GetAsync(request.PostId, cancellationToken).ConfigureAwait(false); - if (post is null) return _msg.NotFound(ApplicationErrors.Community.POST_NOT_FOUND); - if (post.AuthorId != userId.Value) return _msg.Forbidden(ApplicationErrors.General.FORBIDDEN); + if (post is null) return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); + if (post.AuthorId != userId.Value) return _msg.Forbidden(MessageKeys.General.FORBIDDEN); if (post.Status != PostStatus.Draft) - return _msg.BusinessRule(ApplicationErrors.Community.POST_ALREADY_PUBLISHED); + return _msg.BusinessRule(MessageKeys.Community.POST_ALREADY_PUBLISHED); _repo.Remove(post); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(ApplicationErrors.Community.DRAFT_DELETED); + return _msg.Ok(MessageKeys.Community.DRAFT_DELETED); } } diff --git a/backend/src/CCE.Application/Community/Commands/DeleteTopic/DeleteTopicCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/DeleteTopic/DeleteTopicCommandHandler.cs index 90e862dd..4f893e78 100644 --- a/backend/src/CCE.Application/Community/Commands/DeleteTopic/DeleteTopicCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/DeleteTopic/DeleteTopicCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -42,6 +42,6 @@ public async Task> Handle(DeleteTopicCommand request, Cancell topic.SoftDelete(deletedById.Value, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok("CONTENT_DELETED"); + return _messages.Ok(MessageKeys.Content.CONTENT_DELETED); } } diff --git a/backend/src/CCE.Application/Community/Commands/EditReply/EditReplyCommand.cs b/backend/src/CCE.Application/Community/Commands/EditReply/EditReplyCommand.cs index 2679e650..3f33a7ba 100644 --- a/backend/src/CCE.Application/Community/Commands/EditReply/EditReplyCommand.cs +++ b/backend/src/CCE.Application/Community/Commands/EditReply/EditReplyCommand.cs @@ -1,7 +1,8 @@ +using CCE.Application.Common; using MediatR; namespace CCE.Application.Community.Commands.EditReply; public sealed record EditReplyCommand( Guid ReplyId, - string Content) : IRequest; + string Content) : IRequest>; diff --git a/backend/src/CCE.Application/Community/Commands/EditReply/EditReplyCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/EditReply/EditReplyCommandHandler.cs index d04b35b3..f538912a 100644 --- a/backend/src/CCE.Application/Community/Commands/EditReply/EditReplyCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/EditReply/EditReplyCommandHandler.cs @@ -1,11 +1,13 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Sanitization; +using CCE.Application.Messages; using CCE.Domain.Common; using MediatR; namespace CCE.Application.Community.Commands.EditReply; -public sealed class EditReplyCommandHandler : IRequestHandler +public sealed class EditReplyCommandHandler : IRequestHandler> { private static readonly TimeSpan EditWindow = TimeSpan.FromMinutes(15); @@ -13,20 +15,23 @@ public sealed class EditReplyCommandHandler : IRequestHandler Handle(EditReplyCommand request, CancellationToken cancellationToken) + public async Task> Handle(EditReplyCommand request, CancellationToken cancellationToken) { var userId = _currentUser.GetUserId() ?? throw new DomainException("Cannot edit a reply without a user identity."); @@ -51,6 +56,6 @@ public async Task Handle(EditReplyCommand request, CancellationToken cance var sanitized = _sanitizer.Sanitize(request.Content); reply.EditContent(sanitized, userId, _clock); await _service.UpdateReplyAsync(reply, cancellationToken).ConfigureAwait(false); - return Unit.Value; + return _msg.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/JoinCommunity/JoinCommunityCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/JoinCommunity/JoinCommunityCommandHandler.cs index 695f3f7d..dc4c1c99 100644 --- a/backend/src/CCE.Application/Community/Commands/JoinCommunity/JoinCommunityCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/JoinCommunity/JoinCommunityCommandHandler.cs @@ -1,7 +1,7 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -35,10 +35,10 @@ public async Task> Handle(JoinCommunityCommand request, Cance var community = await _repo.GetAsync(request.CommunityId, cancellationToken).ConfigureAwait(false); if (community is null || !community.IsActive) - return _msg.NotFound(ApplicationErrors.Community.COMMUNITY_NOT_FOUND); + return _msg.NotFound(MessageKeys.Community.COMMUNITY_NOT_FOUND); if (await _repo.HasMembershipAsync(request.CommunityId, userId.Value, cancellationToken).ConfigureAwait(false)) - return _msg.Conflict(ApplicationErrors.General.DUPLICATE_VALUE); + return _msg.Conflict(MessageKeys.General.DUPLICATE_VALUE); if (community.IsPublic) { @@ -48,7 +48,7 @@ public async Task> Handle(JoinCommunityCommand request, Cance else { if (await _repo.HasPendingRequestAsync(request.CommunityId, userId.Value, cancellationToken).ConfigureAwait(false)) - return _msg.Conflict(ApplicationErrors.General.DUPLICATE_VALUE); + return _msg.Conflict(MessageKeys.General.DUPLICATE_VALUE); var joinRequest = CommunityJoinRequest.Submit(community.Id, userId.Value, _clock); _repo.AddJoinRequest(joinRequest); @@ -60,6 +60,6 @@ public async Task> Handle(JoinCommunityCommand request, Cance await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(ApplicationErrors.General.SUCCESS_OPERATION); + return _msg.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/LeaveCommunity/LeaveCommunityCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/LeaveCommunity/LeaveCommunityCommandHandler.cs index bfe0d9c8..b89de8d1 100644 --- a/backend/src/CCE.Application/Community/Commands/LeaveCommunity/LeaveCommunityCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/LeaveCommunity/LeaveCommunityCommandHandler.cs @@ -1,7 +1,7 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Errors; using CCE.Application.Messages; + using MediatR; namespace CCE.Application.Community.Commands.LeaveCommunity; @@ -37,6 +37,6 @@ public async Task> Handle(LeaveCommunityCommand request, Canc await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } - return _msg.Ok(ApplicationErrors.General.SUCCESS_OPERATION); + return _msg.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/MarkPostAnswered/MarkPostAnsweredCommand.cs b/backend/src/CCE.Application/Community/Commands/MarkPostAnswered/MarkPostAnsweredCommand.cs index ff954f8a..db5f1255 100644 --- a/backend/src/CCE.Application/Community/Commands/MarkPostAnswered/MarkPostAnsweredCommand.cs +++ b/backend/src/CCE.Application/Community/Commands/MarkPostAnswered/MarkPostAnsweredCommand.cs @@ -1,7 +1,8 @@ +using CCE.Application.Common; using MediatR; namespace CCE.Application.Community.Commands.MarkPostAnswered; public sealed record MarkPostAnsweredCommand( Guid PostId, - Guid ReplyId) : IRequest; + Guid ReplyId) : IRequest>; diff --git a/backend/src/CCE.Application/Community/Commands/MarkPostAnswered/MarkPostAnsweredCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/MarkPostAnswered/MarkPostAnsweredCommandHandler.cs index 0f1bd6d6..d732e155 100644 --- a/backend/src/CCE.Application/Community/Commands/MarkPostAnswered/MarkPostAnsweredCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/MarkPostAnswered/MarkPostAnsweredCommandHandler.cs @@ -1,23 +1,28 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; +using CCE.Application.Messages; using CCE.Domain.Common; using MediatR; namespace CCE.Application.Community.Commands.MarkPostAnswered; -public sealed class MarkPostAnsweredCommandHandler : IRequestHandler +public sealed class MarkPostAnsweredCommandHandler : IRequestHandler> { private readonly ICommunityWriteService _service; private readonly ICurrentUserAccessor _currentUser; + private readonly MessageFactory _msg; public MarkPostAnsweredCommandHandler( ICommunityWriteService service, - ICurrentUserAccessor currentUser) + ICurrentUserAccessor currentUser, + MessageFactory msg) { _service = service; _currentUser = currentUser; + _msg = msg; } - public async Task Handle(MarkPostAnsweredCommand request, CancellationToken cancellationToken) + public async Task> Handle(MarkPostAnsweredCommand request, CancellationToken cancellationToken) { var userId = _currentUser.GetUserId() ?? throw new DomainException("Cannot mark answer without a user identity."); @@ -42,6 +47,6 @@ public async Task Handle(MarkPostAnsweredCommand request, CancellationToke post.MarkAnswered(request.ReplyId); await _service.UpdatePostAsync(post, cancellationToken).ConfigureAwait(false); - return Unit.Value; + return _msg.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/PublishPost/PublishPostCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/PublishPost/PublishPostCommandHandler.cs index c172ffe7..d0e958dc 100644 --- a/backend/src/CCE.Application/Community/Commands/PublishPost/PublishPostCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/PublishPost/PublishPostCommandHandler.cs @@ -1,8 +1,8 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Errors; -using CCE.Application.Identity; using CCE.Application.Messages; +using CCE.Application.Identity; + using CCE.Domain.Common; using MediatR; @@ -39,8 +39,8 @@ public async Task> Handle(PublishPostCommand request, Cancell if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); var post = await _repo.GetAsync(request.PostId, cancellationToken).ConfigureAwait(false); - if (post is null) return _msg.NotFound(ApplicationErrors.Community.POST_NOT_FOUND); - if (post.AuthorId != userId.Value) return _msg.Forbidden(ApplicationErrors.General.FORBIDDEN); + if (post is null) return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); + if (post.AuthorId != userId.Value) return _msg.Forbidden(MessageKeys.General.FORBIDDEN); post.Publish(_clock); @@ -51,6 +51,6 @@ public async Task> Handle(PublishPostCommand request, Cancell community?.IncrementPosts(); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(ApplicationErrors.Community.POST_PUBLISHED); + return _msg.Ok(MessageKeys.Community.POST_PUBLISHED); } } diff --git a/backend/src/CCE.Application/Community/Commands/RebuildHotLeaderboard/RebuildHotLeaderboardCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/RebuildHotLeaderboard/RebuildHotLeaderboardCommandHandler.cs index 84e96cf6..d0cb7df8 100644 --- a/backend/src/CCE.Application/Community/Commands/RebuildHotLeaderboard/RebuildHotLeaderboardCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/RebuildHotLeaderboard/RebuildHotLeaderboardCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -54,6 +54,6 @@ await _feedStore.AddToHotLeaderboardAsync(communityId, post.Id, post.Score, canc } } - return _msg.Ok("SUCCESS_OPERATION"); + return _msg.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/RejectJoinRequest/RejectJoinRequestCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/RejectJoinRequest/RejectJoinRequestCommandHandler.cs index 33aa9800..ff7a2501 100644 --- a/backend/src/CCE.Application/Community/Commands/RejectJoinRequest/RejectJoinRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/RejectJoinRequest/RejectJoinRequestCommandHandler.cs @@ -1,7 +1,7 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using MediatR; @@ -33,10 +33,10 @@ public async Task> Handle(RejectJoinRequestCommand request, C if (by is null || by == Guid.Empty) return _msg.NotAuthenticated(); var joinRequest = await _repo.GetRequestAsync(request.RequestId, cancellationToken).ConfigureAwait(false); - if (joinRequest is null) return _msg.NotFound(ApplicationErrors.Community.JOIN_REQUEST_NOT_FOUND); + if (joinRequest is null) return _msg.NotFound(MessageKeys.Community.JOIN_REQUEST_NOT_FOUND); joinRequest.Reject(by.Value, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(ApplicationErrors.General.SUCCESS_OPERATION); + return _msg.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/SetCommunityFollow/SetCommunityFollowCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/SetCommunityFollow/SetCommunityFollowCommandHandler.cs index ab817715..b432f5b3 100644 --- a/backend/src/CCE.Application/Community/Commands/SetCommunityFollow/SetCommunityFollowCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/SetCommunityFollow/SetCommunityFollowCommandHandler.cs @@ -1,7 +1,7 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -37,7 +37,7 @@ public async Task> Handle(SetCommunityFollowCommand request, { var community = await _repo.GetAsync(request.CommunityId, cancellationToken).ConfigureAwait(false); if (community is null || !community.IsActive) - return _msg.NotFound(ApplicationErrors.Community.COMMUNITY_NOT_FOUND); + return _msg.NotFound(MessageKeys.Community.COMMUNITY_NOT_FOUND); var existing = await _repo.FindFollowAsync(request.CommunityId, userId.Value, cancellationToken).ConfigureAwait(false); if (existing is null) @@ -59,6 +59,6 @@ public async Task> Handle(SetCommunityFollowCommand request, } } - return _msg.Ok(ApplicationErrors.General.SUCCESS_OPERATION); + return _msg.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/SetPostFollow/SetPostFollowCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/SetPostFollow/SetPostFollowCommandHandler.cs index 30032d83..c38e5a2a 100644 --- a/backend/src/CCE.Application/Community/Commands/SetPostFollow/SetPostFollowCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/SetPostFollow/SetPostFollowCommandHandler.cs @@ -1,8 +1,8 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -38,7 +38,7 @@ public async Task> Handle(SetPostFollowCommand request, Cance { var exists = await _db.Posts .AnyAsyncEither(p => p.Id == request.PostId, cancellationToken).ConfigureAwait(false); - if (!exists) return _msg.NotFound(ApplicationErrors.Community.POST_NOT_FOUND); + if (!exists) return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); // Idempotent: only create when not already following var existing = await _service.FindPostFollowAsync(request.PostId, userId.Value, cancellationToken).ConfigureAwait(false); @@ -51,6 +51,6 @@ public async Task> Handle(SetPostFollowCommand request, Cance await _service.RemovePostFollowAsync(request.PostId, userId.Value, cancellationToken).ConfigureAwait(false); } - return _msg.Ok(ApplicationErrors.General.SUCCESS_OPERATION); + return _msg.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/SetTopicFollow/SetTopicFollowCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/SetTopicFollow/SetTopicFollowCommandHandler.cs index 162722cc..41145c4c 100644 --- a/backend/src/CCE.Application/Community/Commands/SetTopicFollow/SetTopicFollowCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/SetTopicFollow/SetTopicFollowCommandHandler.cs @@ -1,8 +1,8 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -38,7 +38,7 @@ public async Task> Handle(SetTopicFollowCommand request, Canc { var exists = await _db.Topics .AnyAsyncEither(t => t.Id == request.TopicId, cancellationToken).ConfigureAwait(false); - if (!exists) return _msg.NotFound(ApplicationErrors.Community.TOPIC_NOT_FOUND); + if (!exists) return _msg.NotFound(MessageKeys.Community.TOPIC_NOT_FOUND); // Idempotent: only create when not already following var existing = await _service.FindTopicFollowAsync(request.TopicId, userId.Value, cancellationToken).ConfigureAwait(false); @@ -51,6 +51,6 @@ public async Task> Handle(SetTopicFollowCommand request, Canc await _service.RemoveTopicFollowAsync(request.TopicId, userId.Value, cancellationToken).ConfigureAwait(false); } - return _msg.Ok(ApplicationErrors.General.SUCCESS_OPERATION); + return _msg.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/SetUserFollow/SetUserFollowCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/SetUserFollow/SetUserFollowCommandHandler.cs index 320240dc..f5998451 100644 --- a/backend/src/CCE.Application/Community/Commands/SetUserFollow/SetUserFollowCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/SetUserFollow/SetUserFollowCommandHandler.cs @@ -1,8 +1,8 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Errors; -using CCE.Application.Identity; using CCE.Application.Messages; +using CCE.Application.Identity; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -68,6 +68,6 @@ public async Task> Handle(SetUserFollowCommand request, Cance } } - return _msg.Ok(ApplicationErrors.General.SUCCESS_OPERATION); + return _msg.Ok(MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/SoftDeletePost/SoftDeletePostCommand.cs b/backend/src/CCE.Application/Community/Commands/SoftDeletePost/SoftDeletePostCommand.cs index d6dbfc0a..029085c8 100644 --- a/backend/src/CCE.Application/Community/Commands/SoftDeletePost/SoftDeletePostCommand.cs +++ b/backend/src/CCE.Application/Community/Commands/SoftDeletePost/SoftDeletePostCommand.cs @@ -1,5 +1,6 @@ +using CCE.Application.Common; using MediatR; namespace CCE.Application.Community.Commands.SoftDeletePost; -public sealed record SoftDeletePostCommand(System.Guid Id) : IRequest; +public sealed record SoftDeletePostCommand(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Community/Commands/SoftDeletePost/SoftDeletePostCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/SoftDeletePost/SoftDeletePostCommandHandler.cs index 0bd68279..c5168906 100644 --- a/backend/src/CCE.Application/Community/Commands/SoftDeletePost/SoftDeletePostCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/SoftDeletePost/SoftDeletePostCommandHandler.cs @@ -1,14 +1,16 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Realtime; using CCE.Application.Community; using CCE.Application.Identity; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; namespace CCE.Application.Community.Commands.SoftDeletePost; -public sealed class SoftDeletePostCommandHandler : IRequestHandler +public sealed class SoftDeletePostCommandHandler : IRequestHandler> { private readonly ICommunityModerationService _service; private readonly ICommunityRepository _communityRepo; @@ -18,6 +20,7 @@ public sealed class SoftDeletePostCommandHandler : IRequestHandler Handle(SoftDeletePostCommand request, CancellationToken cancellationToken) + public async Task> Handle(SoftDeletePostCommand request, CancellationToken cancellationToken) { var post = await _service.FindPostAsync(request.Id, cancellationToken).ConfigureAwait(false); if (post is null) @@ -82,6 +87,6 @@ await _feedStore.RemovePostFromAllFeedsAsync(post.CommunityId, post.Id, cancella var contentModerated = RealtimeEnvelope.Wrap(new ContentModeratedRealtime("Post", post.Id, post.Id, moderatorId, "SoftDeleted")); await _realtime.PublishToModeratorsAsync(RealtimeEvents.ContentModerated, contentModerated, cancellationToken).ConfigureAwait(false); - return Unit.Value; + return _msg.Ok(MessageKeys.Content.CONTENT_DELETED); } } diff --git a/backend/src/CCE.Application/Community/Commands/SoftDeleteReply/SoftDeleteReplyCommand.cs b/backend/src/CCE.Application/Community/Commands/SoftDeleteReply/SoftDeleteReplyCommand.cs index a02bb4fc..7a0572b1 100644 --- a/backend/src/CCE.Application/Community/Commands/SoftDeleteReply/SoftDeleteReplyCommand.cs +++ b/backend/src/CCE.Application/Community/Commands/SoftDeleteReply/SoftDeleteReplyCommand.cs @@ -1,5 +1,6 @@ +using CCE.Application.Common; using MediatR; namespace CCE.Application.Community.Commands.SoftDeleteReply; -public sealed record SoftDeleteReplyCommand(System.Guid Id) : IRequest; +public sealed record SoftDeleteReplyCommand(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Community/Commands/SoftDeleteReply/SoftDeleteReplyCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/SoftDeleteReply/SoftDeleteReplyCommandHandler.cs index 8c0e3b0f..99339254 100644 --- a/backend/src/CCE.Application/Community/Commands/SoftDeleteReply/SoftDeleteReplyCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/SoftDeleteReply/SoftDeleteReplyCommandHandler.cs @@ -1,13 +1,15 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Realtime; using CCE.Application.Community; using CCE.Application.Identity; +using CCE.Application.Messages; using CCE.Domain.Common; using MediatR; namespace CCE.Application.Community.Commands.SoftDeleteReply; -public sealed class SoftDeleteReplyCommandHandler : IRequestHandler +public sealed class SoftDeleteReplyCommandHandler : IRequestHandler> { private readonly ICommunityModerationService _service; private readonly ICceDbContext _db; @@ -15,6 +17,7 @@ public sealed class SoftDeleteReplyCommandHandler : IRequestHandler Handle(SoftDeleteReplyCommand request, CancellationToken cancellationToken) + public async Task> Handle(SoftDeleteReplyCommand request, CancellationToken cancellationToken) { var reply = await _service.FindReplyAsync(request.Id, cancellationToken).ConfigureAwait(false); if (reply is null) @@ -65,6 +70,6 @@ public async Task Handle(SoftDeleteReplyCommand request, CancellationToken var contentModerated = RealtimeEnvelope.Wrap(new ContentModeratedRealtime("Reply", reply.Id, reply.PostId, moderatorId, "SoftDeleted")); await _realtime.PublishToModeratorsAsync(RealtimeEvents.ContentModerated, contentModerated, cancellationToken).ConfigureAwait(false); - return Unit.Value; + return _msg.Ok(MessageKeys.Content.CONTENT_DELETED); } } diff --git a/backend/src/CCE.Application/Community/Commands/UpdateCommunity/UpdateCommunityCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/UpdateCommunity/UpdateCommunityCommandHandler.cs index 4f2a2922..0fb30bc8 100644 --- a/backend/src/CCE.Application/Community/Commands/UpdateCommunity/UpdateCommunityCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/UpdateCommunity/UpdateCommunityCommandHandler.cs @@ -1,7 +1,7 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Errors; using CCE.Application.Messages; + using MediatR; namespace CCE.Application.Community.Commands.UpdateCommunity; @@ -23,11 +23,11 @@ public UpdateCommunityCommandHandler(ICommunityRepository repo, ICceDbContext db public async Task> Handle(UpdateCommunityCommand request, CancellationToken cancellationToken) { var community = await _repo.GetAsync(request.CommunityId, cancellationToken).ConfigureAwait(false); - if (community is null) return _msg.NotFound(ApplicationErrors.Community.COMMUNITY_NOT_FOUND); + if (community is null) return _msg.NotFound(MessageKeys.Community.COMMUNITY_NOT_FOUND); community.UpdateContent(request.NameAr, request.NameEn, request.DescriptionAr, request.DescriptionEn, request.PresentationJson); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(ApplicationErrors.General.SUCCESS_UPDATED); + return _msg.Ok(MessageKeys.General.SUCCESS_UPDATED); } } diff --git a/backend/src/CCE.Application/Community/Commands/UpdateDraft/UpdateDraftCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/UpdateDraft/UpdateDraftCommandHandler.cs index bfcc2b5d..a0f19f96 100644 --- a/backend/src/CCE.Application/Community/Commands/UpdateDraft/UpdateDraftCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/UpdateDraft/UpdateDraftCommandHandler.cs @@ -1,8 +1,8 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Sanitization; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -37,10 +37,10 @@ public async Task> Handle(UpdateDraftCommand request, Cancell if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); var post = await _repo.GetAsync(request.PostId, cancellationToken).ConfigureAwait(false); - if (post is null) return _msg.NotFound(ApplicationErrors.Community.POST_NOT_FOUND); - if (post.AuthorId != userId.Value) return _msg.Forbidden(ApplicationErrors.General.FORBIDDEN); + if (post is null) return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); + if (post.AuthorId != userId.Value) return _msg.Forbidden(MessageKeys.General.FORBIDDEN); if (post.Status != PostStatus.Draft) - return _msg.BusinessRule(ApplicationErrors.Community.POST_ALREADY_PUBLISHED); + return _msg.BusinessRule(MessageKeys.Community.POST_ALREADY_PUBLISHED); var sanitized = request.Content is null ? null : _sanitizer.Sanitize(request.Content); post.UpdateDraft(request.Title, sanitized, userId.Value, _clock); @@ -49,6 +49,6 @@ public async Task> Handle(UpdateDraftCommand request, Cancell post.SetTags(tags); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(ApplicationErrors.Community.POST_DRAFT_SAVED); + return _msg.Ok(MessageKeys.Community.POST_DRAFT_SAVED); } } diff --git a/backend/src/CCE.Application/Community/Commands/UpdateDraft/UpdateDraftCommandValidator.cs b/backend/src/CCE.Application/Community/Commands/UpdateDraft/UpdateDraftCommandValidator.cs index 5a45b1ae..01410a64 100644 --- a/backend/src/CCE.Application/Community/Commands/UpdateDraft/UpdateDraftCommandValidator.cs +++ b/backend/src/CCE.Application/Community/Commands/UpdateDraft/UpdateDraftCommandValidator.cs @@ -1,4 +1,4 @@ -using CCE.Application.Errors; +using CCE.Application.Messages; using CCE.Domain.Community; using FluentValidation; @@ -8,11 +8,11 @@ public sealed class UpdateDraftCommandValidator : AbstractValidator x.PostId).NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD); + RuleFor(x => x.PostId).NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); RuleFor(x => x.Title) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(Post.MaxTitleLength).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(Post.MaxTitleLength).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.Content) - .MaximumLength(Post.MaxContentLength).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .MaximumLength(Post.MaxContentLength).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); } } diff --git a/backend/src/CCE.Application/Community/Commands/UpdateTopic/UpdateTopicCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/UpdateTopic/UpdateTopicCommandHandler.cs index cd18a38f..ef0c1e39 100644 --- a/backend/src/CCE.Application/Community/Commands/UpdateTopic/UpdateTopicCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/UpdateTopic/UpdateTopicCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Community.Dtos; using CCE.Application.Community.Queries.ListTopics; @@ -40,6 +40,6 @@ public async Task> Handle(UpdateTopicCommand request, Cancell await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok(ListTopicsQueryHandler.MapToDto(topic), "SUCCESS_OPERATION"); + return _messages.Ok(ListTopicsQueryHandler.MapToDto(topic), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Commands/VotePost/VotePostCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/VotePost/VotePostCommandHandler.cs index bd68a58f..cf712fd2 100644 --- a/backend/src/CCE.Application/Community/Commands/VotePost/VotePostCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/VotePost/VotePostCommandHandler.cs @@ -1,8 +1,8 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Realtime; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -48,7 +48,7 @@ public async Task> Handle(VotePostCommand request, Cancellati var post = await _repo.GetPostAsync(request.PostId, cancellationToken).ConfigureAwait(false); if (post is null) - return _msg.NotFound(ApplicationErrors.Community.POST_NOT_FOUND); + return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); var existing = await _repo.FindPostVoteAsync(request.PostId, userId.Value, cancellationToken).ConfigureAwait(false); var oldValue = existing?.Value ?? 0; @@ -79,6 +79,6 @@ public async Task> Handle(VotePostCommand request, Cancellati await _realtime.PublishToPostAsync(request.PostId, RealtimeEvents.VoteChanged, new { postId = request.PostId, post.UpvoteCount, post.DownvoteCount, post.Score }, cancellationToken).ConfigureAwait(false); - return _msg.Ok(ApplicationErrors.Community.POST_VOTED); + return _msg.Ok(MessageKeys.Community.POST_VOTED); } } diff --git a/backend/src/CCE.Application/Community/Commands/VotePost/VotePostCommandValidator.cs b/backend/src/CCE.Application/Community/Commands/VotePost/VotePostCommandValidator.cs index 2c48f471..b73577c6 100644 --- a/backend/src/CCE.Application/Community/Commands/VotePost/VotePostCommandValidator.cs +++ b/backend/src/CCE.Application/Community/Commands/VotePost/VotePostCommandValidator.cs @@ -1,4 +1,4 @@ -using CCE.Application.Errors; +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.Community.Commands.VotePost; @@ -7,7 +7,7 @@ public sealed class VotePostCommandValidator : AbstractValidator x.PostId).NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD); - RuleFor(x => x.Direction).IsInEnum().WithErrorCode(ApplicationErrors.Validation.INVALID_ENUM); + RuleFor(x => x.PostId).NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); + RuleFor(x => x.Direction).IsInEnum().WithErrorCode(MessageKeys.Validation.INVALID_ENUM); } } diff --git a/backend/src/CCE.Application/Community/Commands/VoteReply/VoteReplyCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/VoteReply/VoteReplyCommandHandler.cs index eae8794d..d712072d 100644 --- a/backend/src/CCE.Application/Community/Commands/VoteReply/VoteReplyCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/VoteReply/VoteReplyCommandHandler.cs @@ -1,8 +1,8 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Realtime; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -44,7 +44,7 @@ public async Task> Handle(VoteReplyCommand request, Cancellat var reply = await _repo.GetReplyAsync(request.ReplyId, cancellationToken).ConfigureAwait(false); if (reply is null) - return _msg.NotFound(ApplicationErrors.Community.REPLY_NOT_FOUND); + return _msg.NotFound(MessageKeys.Community.REPLY_NOT_FOUND); var existing = await _repo.FindReplyVoteAsync(request.ReplyId, userId.Value, cancellationToken).ConfigureAwait(false); var oldValue = existing?.Value ?? 0; @@ -69,6 +69,6 @@ public async Task> Handle(VoteReplyCommand request, Cancellat await _realtime.PublishToPostAsync(reply.PostId, RealtimeEvents.VoteChanged, new { replyId = reply.Id, reply.UpvoteCount, reply.DownvoteCount, reply.Score }, cancellationToken).ConfigureAwait(false); - return _msg.Ok(ApplicationErrors.Community.POST_VOTED); + return _msg.Ok(MessageKeys.Community.POST_VOTED); } } diff --git a/backend/src/CCE.Application/Community/Commands/VoteReply/VoteReplyCommandValidator.cs b/backend/src/CCE.Application/Community/Commands/VoteReply/VoteReplyCommandValidator.cs index f5db84d6..0c08cfc6 100644 --- a/backend/src/CCE.Application/Community/Commands/VoteReply/VoteReplyCommandValidator.cs +++ b/backend/src/CCE.Application/Community/Commands/VoteReply/VoteReplyCommandValidator.cs @@ -1,4 +1,4 @@ -using CCE.Application.Errors; +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.Community.Commands.VoteReply; @@ -7,7 +7,7 @@ public sealed class VoteReplyCommandValidator : AbstractValidator x.ReplyId).NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD); - RuleFor(x => x.Direction).IsInEnum().WithErrorCode(ApplicationErrors.Validation.INVALID_ENUM); + RuleFor(x => x.ReplyId).NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); + RuleFor(x => x.Direction).IsInEnum().WithErrorCode(MessageKeys.Validation.INVALID_ENUM); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetCommunityBySlug/GetCommunityBySlugQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetCommunityBySlug/GetCommunityBySlugQueryHandler.cs index bb0fa9aa..3aed8cbb 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetCommunityBySlug/GetCommunityBySlugQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetCommunityBySlug/GetCommunityBySlugQueryHandler.cs @@ -1,8 +1,8 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Community.Public.Dtos; -using CCE.Application.Errors; using CCE.Application.Messages; + using MediatR; using Microsoft.EntityFrameworkCore; @@ -32,7 +32,7 @@ public async Task> Handle( .ConfigureAwait(false); return dto is null - ? _msg.NotFound(ApplicationErrors.Community.COMMUNITY_NOT_FOUND) - : _msg.Ok(dto, "ITEMS_LISTED"); + ? _msg.NotFound(MessageKeys.Community.COMMUNITY_NOT_FOUND) + : _msg.Ok(dto, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetCommunityRoles/GetCommunityRolesQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetCommunityRoles/GetCommunityRolesQueryHandler.cs index bc10068f..c3b5d644 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetCommunityRoles/GetCommunityRolesQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetCommunityRoles/GetCommunityRolesQueryHandler.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using CCE.Application.Common; using CCE.Application.Community.Public.Dtos; using CCE.Application.Messages; @@ -48,6 +48,6 @@ public Task>> Handle( ModeratorCapabilities), }; - return Task.FromResult(_msg.Ok(roles, "ITEMS_LISTED")); + return Task.FromResult(_msg.Ok(roles, MessageKeys.General.ITEMS_LISTED)); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetCommunityUserProfile/GetCommunityUserProfileQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetCommunityUserProfile/GetCommunityUserProfileQueryHandler.cs index 12680d0e..2f5c5e5b 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetCommunityUserProfile/GetCommunityUserProfileQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetCommunityUserProfile/GetCommunityUserProfileQueryHandler.cs @@ -1,8 +1,8 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Community.Public.Dtos; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Community; using MediatR; using Microsoft.EntityFrameworkCore; @@ -77,6 +77,6 @@ public async Task> Handle( user.Id, user.FirstName, user.LastName, user.JobTitle, user.OrganizationName, user.AvatarUrl, isExpert, user.PostsCount, user.CommentsCount, user.FollowerCount, user.FollowingCount, isFollowed, expertBioAr, expertBioEn, countryNameAr, countryNameEn, user.CreatedOn); - return _msg.Ok(dto, "ITEMS_LISTED"); + return _msg.Ok(dto, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetMyFollows/GetMyFollowsQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetMyFollows/GetMyFollowsQueryHandler.cs index aa0331e7..d54e2983 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetMyFollows/GetMyFollowsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetMyFollows/GetMyFollowsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Public.Dtos; @@ -44,6 +44,6 @@ public async Task> Handle( .Select(f => f.PostId) .ToList(); - return _msg.Ok(new MyFollowsDto(topicIds, userIds, postIds), "ITEMS_LISTED"); + return _msg.Ok(new MyFollowsDto(topicIds, userIds, postIds), MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetMyTopics/GetMyTopicsQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetMyTopics/GetMyTopicsQueryHandler.cs index a7b7f114..1e913691 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetMyTopics/GetMyTopicsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetMyTopics/GetMyTopicsQueryHandler.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; @@ -56,7 +56,7 @@ join t in _db.Topics on f.TopicId equals t.Id { return _msg.Ok( new PagedResult(System.Array.Empty(), pagedIds.Page, pagedIds.PageSize, pagedIds.Total), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } // Step 2: batch-load topic names @@ -91,6 +91,6 @@ join t in _db.Topics on f.TopicId equals t.Id return _msg.Ok( new PagedResult(items, pagedIds.Page, pagedIds.PageSize, pagedIds.Total), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetPollResults/GetPollResultsQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetPollResults/GetPollResultsQueryHandler.cs index f9831fea..58ceff37 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetPollResults/GetPollResultsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetPollResults/GetPollResultsQueryHandler.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Community.Public.Dtos; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using MediatR; using Microsoft.EntityFrameworkCore; @@ -45,7 +45,7 @@ public async Task> Handle(GetPollResultsQuery request, .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); - if (poll is null) return _msg.NotFound(ApplicationErrors.Community.POLL_NOT_FOUND); + if (poll is null) return _msg.NotFound(MessageKeys.Community.POLL_NOT_FOUND); var isClosed = _clock.UtcNow >= poll.Deadline; var resultsVisible = isClosed || poll.ShowResultsBeforeClose; @@ -60,6 +60,6 @@ public async Task> Handle(GetPollResultsQuery request, var dto = new PollResultsDto(poll.Id, poll.Deadline, isClosed, poll.AllowMultiple, resultsVisible, resultsVisible ? total : 0, options); - return _msg.Ok(dto, "ITEMS_LISTED"); + return _msg.Ok(dto, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetPostActivity/GetPostActivityQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetPostActivity/GetPostActivityQueryHandler.cs index f5d76a88..571a5c8d 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetPostActivity/GetPostActivityQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetPostActivity/GetPostActivityQueryHandler.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community; using CCE.Application.Community.Public.Dtos; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -45,7 +45,7 @@ public async Task> Handle( .ConfigureAwait(false); if (post is null) - return _msg.NotFound(ApplicationErrors.Community.POST_NOT_FOUND); + return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); // Replies created since the cursor — fetch the full nodes so mobile can render them // without a follow-up GET. Performance note: posts have hot index on (PostId, AuthorId) @@ -87,6 +87,6 @@ join u in _db.Users.AsNoTracking() on r.AuthorId equals u.Id post.Score, post.CommentsCount, newReplies, - poll), ApplicationErrors.General.SUCCESS_OPERATION); + poll), MessageKeys.General.SUCCESS_OPERATION); } } \ No newline at end of file diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetPostShareLink/GetPostShareLinkQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetPostShareLink/GetPostShareLinkQueryHandler.cs index be5ee14c..3569ee4b 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetPostShareLink/GetPostShareLinkQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetPostShareLink/GetPostShareLinkQueryHandler.cs @@ -1,8 +1,8 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Community.Public.Dtos; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Community; using MediatR; using Microsoft.EntityFrameworkCore; @@ -28,9 +28,9 @@ public async Task> Handle( var exists = await _db.Posts .AnyAsync(p => p.Id == request.PostId && p.Status == PostStatus.Published, cancellationToken) .ConfigureAwait(false); - if (!exists) return _msg.NotFound(ApplicationErrors.Community.POST_NOT_FOUND); + if (!exists) return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); var dto = new PostShareLinkDto(request.PostId, $"/community/posts/{request.PostId}"); - return _msg.Ok(dto, "ITEMS_LISTED"); + return _msg.Ok(dto, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetPublicPostById/GetPublicPostByIdQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetPublicPostById/GetPublicPostByIdQueryHandler.cs index dab32671..6e86192e 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetPublicPostById/GetPublicPostByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetPublicPostById/GetPublicPostByIdQueryHandler.cs @@ -1,10 +1,10 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Community; using CCE.Application.Community.Public.Dtos; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Community; using MediatR; @@ -64,7 +64,7 @@ from ep in epGroup.DefaultIfEmpty() .ConfigureAwait(false); if (raw is null) - return _msg.NotFound(ApplicationErrors.Community.POST_NOT_FOUND); + return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); // Attachments — separate query to avoid cartesian explosion with the JOIN above. var attachmentIds = await _db.PostAttachments.AsNoTracking() @@ -122,6 +122,6 @@ from ep in epGroup.DefaultIfEmpty() vote, pollSummary); - return _msg.Ok(dto, ApplicationErrors.General.SUCCESS_OPERATION); + return _msg.Ok(dto, MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetPublicTopicBySlug/GetPublicTopicBySlugQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetPublicTopicBySlug/GetPublicTopicBySlugQueryHandler.cs index 312bb318..23aaf099 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetPublicTopicBySlug/GetPublicTopicBySlugQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetPublicTopicBySlug/GetPublicTopicBySlugQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Public.Dtos; @@ -33,6 +33,6 @@ public async Task> Handle( if (topic is null) return _messages.TopicNotFound(); - return _messages.Ok(ListPublicTopicsQueryHandler.MapToDto(topic), "SUCCESS_OPERATION"); + return _messages.Ok(ListPublicTopicsQueryHandler.MapToDto(topic), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetReplyThread/GetReplyThreadQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetReplyThread/GetReplyThreadQueryHandler.cs index 7610790b..4a5ba79c 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetReplyThread/GetReplyThreadQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetReplyThread/GetReplyThreadQueryHandler.cs @@ -1,10 +1,10 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Public.Dtos; using CCE.Application.Community.Public.Queries.ListPublicPostReplies; -using CCE.Application.Errors; using CCE.Application.Messages; + using MediatR; using Microsoft.EntityFrameworkCore; @@ -32,7 +32,7 @@ public async Task>> Handle( .ConfigureAwait(false); if (prefix is null) - return _msg.NotFound>(ApplicationErrors.Community.REPLY_NOT_FOUND); + return _msg.NotFound>(MessageKeys.Community.REPLY_NOT_FOUND); var paged = await _db.PostReplies .Where(r => r.ThreadPath.StartsWith(prefix) && r.Id != request.ReplyId) @@ -47,6 +47,6 @@ public async Task>> Handle( return _msg.Ok( new PagedResult(dtos, paged.Page, paged.PageSize, paged.Total), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListCommunityFeed/ListCommunityFeedQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListCommunityFeed/ListCommunityFeedQueryHandler.cs index 97bd4157..9a8b7659 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListCommunityFeed/ListCommunityFeedQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListCommunityFeed/ListCommunityFeedQueryHandler.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; @@ -106,7 +106,7 @@ join c in _db.Communities on p.CommunityId equals c.Id { return _msg.Ok( new PagedResult(hydrated, page, pageSize, total), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } // topicId active: page the in-memory filtered result. @@ -116,7 +116,7 @@ join c in _db.Communities on p.CommunityId equals c.Id return _msg.Ok( new PagedResult( hydrated.Skip(skip).Take(pageSize).ToList(), page, pageSize, total), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } // Window exhausted for this page — fall through to SQL. } @@ -177,6 +177,6 @@ join c in _db.Communities on p.CommunityId equals c.Id .ConfigureAwait(false); return _msg.Ok( new PagedResult(items, page, pageSize, pagedIds.Total), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListExpertLeaderboard/ListExpertLeaderboardQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListExpertLeaderboard/ListExpertLeaderboardQueryHandler.cs index 9bcaed4c..294601a7 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListExpertLeaderboard/ListExpertLeaderboardQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListExpertLeaderboard/ListExpertLeaderboardQueryHandler.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; @@ -42,7 +42,7 @@ public async Task>> Handle( return _msg.Ok( new PagedResult( System.Array.Empty(), page, pageSize, 0), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } var userIds = profiles.Select(p => p.UserId).Distinct().ToList(); @@ -114,6 +114,6 @@ public async Task>> Handle( return _msg.Ok( new PagedResult(items, page, pageSize, ranked.Count), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListFeaturedPosts/ListFeaturedPostsQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListFeaturedPosts/ListFeaturedPostsQueryHandler.cs index dca1efaf..38fc0cdd 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListFeaturedPosts/ListFeaturedPostsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListFeaturedPosts/ListFeaturedPostsQueryHandler.cs @@ -1,8 +1,8 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Pagination; using CCE.Application.Community.Public.Dtos; -using CCE.Application.Errors; using CCE.Application.Messages; + using MediatR; namespace CCE.Application.Community.Public.Queries.ListFeaturedPosts; @@ -39,7 +39,7 @@ public Task>> Handle( .ToList(); var result = new PagedResult(items, page, pageSize, all.Count); - return Task.FromResult(_messages.Ok(result, ApplicationErrors.General.SUCCESS_OPERATION)); + return Task.FromResult(_messages.Ok(result, MessageKeys.General.SUCCESS_OPERATION)); } // ─── Mock data (deterministic ids + fixed timestamps) ───────────────────── diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListMyDrafts/ListMyDraftsQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListMyDrafts/ListMyDraftsQueryHandler.cs index 822b11f6..a7ca8906 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListMyDrafts/ListMyDraftsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListMyDrafts/ListMyDraftsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Public.Dtos; @@ -38,6 +38,6 @@ public async Task>> Handle( .ToPagedResultAsync(request.Page, request.PageSize, cancellationToken) .ConfigureAwait(false); - return _msg.Ok(paged, "ITEMS_LISTED"); + return _msg.Ok(paged, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListMyMentions/ListMyMentionsQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListMyMentions/ListMyMentionsQueryHandler.cs index d0947096..40839f66 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListMyMentions/ListMyMentionsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListMyMentions/ListMyMentionsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Public.Dtos; @@ -35,6 +35,6 @@ public async Task>> Handle( .ToPagedResultAsync(request.Page, request.PageSize, cancellationToken) .ConfigureAwait(false); - return _msg.Ok(paged, "ITEMS_LISTED"); + return _msg.Ok(paged, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListPublicCommunities/ListPublicCommunitiesQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListPublicCommunities/ListPublicCommunitiesQueryHandler.cs index 75dcb8c4..24945b13 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListPublicCommunities/ListPublicCommunitiesQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListPublicCommunities/ListPublicCommunitiesQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Public.Dtos; @@ -32,6 +32,6 @@ public async Task>> Handle( .ToPagedResultAsync(request.Page, request.PageSize, cancellationToken) .ConfigureAwait(false); - return _msg.Ok(paged, "ITEMS_LISTED"); + return _msg.Ok(paged, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListPublicPostReplies/ListPublicPostRepliesQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListPublicPostReplies/ListPublicPostRepliesQueryHandler.cs index 5aaa9c36..4365abc3 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListPublicPostReplies/ListPublicPostRepliesQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListPublicPostReplies/ListPublicPostRepliesQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Public.Dtos; @@ -38,7 +38,7 @@ public async Task>> Handle( return _msg.Ok( new PagedResult(dtos, paged.Page, paged.PageSize, paged.Total), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } internal static async Task> LoadAuthorMapAsync( diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListPublicPostsInTopic/ListPublicPostsInTopicQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListPublicPostsInTopic/ListPublicPostsInTopicQueryHandler.cs index 76685bd2..8f8bff6a 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListPublicPostsInTopic/ListPublicPostsInTopicQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListPublicPostsInTopic/ListPublicPostsInTopicQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Public.Dtos; @@ -41,7 +41,7 @@ public async Task>> Handle( return _msg.Ok( new PagedResult( System.Array.Empty(), paged.Page, paged.PageSize, paged.Total), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } var authorIds = items.Select(p => p.AuthorId).Distinct().ToList(); @@ -79,7 +79,7 @@ public async Task>> Handle( return _msg.Ok( new PagedResult(dtos, paged.Page, paged.PageSize, paged.Total), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } internal static PublicPostDto MapToDto( diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListPublicTopics/ListPublicTopicsQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListPublicTopics/ListPublicTopicsQueryHandler.cs index 89cab163..e32d0022 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListPublicTopics/ListPublicTopicsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListPublicTopics/ListPublicTopicsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Public.Dtos; @@ -30,7 +30,7 @@ public ListPublicTopicsQueryHandler(ICceDbContext db, MessageFactory messages) .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); - return _messages.Ok((System.Collections.Generic.IReadOnlyList)list.Select(MapToDto).ToList(), "ITEMS_LISTED"); + return _messages.Ok((System.Collections.Generic.IReadOnlyList)list.Select(MapToDto).ToList(), MessageKeys.General.ITEMS_LISTED); } internal static PublicTopicDto MapToDto(Topic t) => new( diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListPublicTopicsPaginated/ListPublicTopicsPaginatedQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListPublicTopicsPaginated/ListPublicTopicsPaginatedQueryHandler.cs index 646ee6e6..ce494783 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListPublicTopicsPaginated/ListPublicTopicsPaginatedQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListPublicTopicsPaginated/ListPublicTopicsPaginatedQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Public.Dtos; @@ -53,7 +53,7 @@ public async Task>> Handle( if (pagedIds.Count == 0) return _messages.Ok( new PagedResult([], request.Page, request.PageSize, total), - "TOPICS_LISTED"); + MessageKeys.Community.TOPICS_LISTED); var topics = await _db.Topics .Where(t => pagedIds.Contains(t.Id)) @@ -74,7 +74,7 @@ public async Task>> Handle( return _messages.Ok( new PagedResult(sortedItems, request.Page, request.PageSize, total), - "TOPICS_LISTED"); + MessageKeys.Community.TOPICS_LISTED); } IQueryable sortedQuery; @@ -92,7 +92,7 @@ public async Task>> Handle( if (topicIds.Count == 0) return _messages.Ok( new PagedResult([], pagedIdsResult.Page, pagedIdsResult.PageSize, pagedIdsResult.Total), - "TOPICS_LISTED"); + MessageKeys.Community.TOPICS_LISTED); var pagedTopics = await _db.Topics .Where(t => topicIds.Contains(t.Id)) @@ -122,6 +122,6 @@ public async Task>> Handle( return _messages.Ok( new PagedResult(items, pagedIdsResult.Page, pagedIdsResult.PageSize, pagedIdsResult.Total), - "TOPICS_LISTED"); + MessageKeys.Community.TOPICS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListUserFeed/ListUserFeedQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListUserFeed/ListUserFeedQueryHandler.cs index da3b72a4..6bd454a5 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListUserFeed/ListUserFeedQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListUserFeed/ListUserFeedQueryHandler.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; @@ -126,7 +126,7 @@ join c in _db.Communities on p.CommunityId equals c.Id { return _msg.Ok( new PagedResult(hydrated, page, pageSize, total), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } // topicId active: page the in-memory filtered result. @@ -136,7 +136,7 @@ join c in _db.Communities on p.CommunityId equals c.Id return _msg.Ok( new PagedResult( hydrated.Skip(skip).Take(pageSize).ToList(), page, pageSize, total), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } // Window exhausted for this page — fall through to SQL. } @@ -232,7 +232,7 @@ join p in _db.Posts on ep.UserId equals p.AuthorId .ConfigureAwait(false); return _msg.Ok( new PagedResult(hydrated, page, pageSize, redisTotal + expertTotal), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } } } @@ -267,7 +267,7 @@ private async Task>> FallbackSqlAsync return _msg.Ok( new PagedResult( System.Array.Empty(), page, pageSize, 0), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } // The WHERE clause is (followedCommunity OR followedTopic OR followedUser) AND community = X. @@ -280,7 +280,7 @@ private async Task>> FallbackSqlAsync return _msg.Ok( new PagedResult( System.Array.Empty(), page, pageSize, 0), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } // Fix E: use JOIN instead of correlated Communities.Any() per post row, matching the @@ -324,6 +324,6 @@ join c in _db.Communities on p.CommunityId equals c.Id .ConfigureAwait(false); return _msg.Ok( new PagedResult(items, page, pageSize, pagedIds.Total), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Queries/GetTopicById/GetTopicByIdQueryHandler.cs b/backend/src/CCE.Application/Community/Queries/GetTopicById/GetTopicByIdQueryHandler.cs index d7a7aa1a..fa79ad0e 100644 --- a/backend/src/CCE.Application/Community/Queries/GetTopicById/GetTopicByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Queries/GetTopicById/GetTopicByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Dtos; @@ -29,6 +29,6 @@ public async Task> Handle(GetTopicByIdQuery request, Cancella if (topic is null) return _messages.TopicNotFound(); - return _messages.Ok(ListTopicsQueryHandler.MapToDto(topic), "SUCCESS_OPERATION"); + return _messages.Ok(ListTopicsQueryHandler.MapToDto(topic), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Community/Queries/ListAdminPosts/ListAdminPostsQuery.cs b/backend/src/CCE.Application/Community/Queries/ListAdminPosts/ListAdminPostsQuery.cs index 8afb8770..2d8e1df6 100644 --- a/backend/src/CCE.Application/Community/Queries/ListAdminPosts/ListAdminPostsQuery.cs +++ b/backend/src/CCE.Application/Community/Queries/ListAdminPosts/ListAdminPostsQuery.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Common.Pagination; using MediatR; @@ -18,4 +19,4 @@ public sealed record ListAdminPostsQuery( System.Guid? TopicId = null, string? Search = null, string? Status = null, - string? Locale = null) : IRequest>; + string? Locale = null) : IRequest>>; diff --git a/backend/src/CCE.Application/Community/Queries/ListAdminPosts/ListAdminPostsQueryHandler.cs b/backend/src/CCE.Application/Community/Queries/ListAdminPosts/ListAdminPostsQueryHandler.cs index 134de380..83c9cec8 100644 --- a/backend/src/CCE.Application/Community/Queries/ListAdminPosts/ListAdminPostsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Queries/ListAdminPosts/ListAdminPostsQueryHandler.cs @@ -1,5 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; +using CCE.Application.Messages; using CCE.Domain.Community; using MediatR; using Microsoft.EntityFrameworkCore; @@ -14,16 +16,18 @@ namespace CCE.Application.Community.Queries.ListAdminPosts; /// against PostReplies (excluding soft-deleted replies). /// public sealed class ListAdminPostsQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListAdminPostsQueryHandler(ICceDbContext db) + public ListAdminPostsQueryHandler(ICceDbContext db, MessageFactory msg) { _db = db; + _msg = msg; } - public async Task> Handle( + public async Task>> Handle( ListAdminPostsQuery request, CancellationToken cancellationToken) { @@ -82,9 +86,10 @@ public async Task> Handle( if (pagePostsResult.Items.Count == 0) { - return new PagedResult( + var empty = new PagedResult( System.Array.Empty(), pagePostsResult.Page, pagePostsResult.PageSize, pagePostsResult.Total); + return _msg.Ok(empty, MessageKeys.General.ITEMS_LISTED); } // ─── Lookups for the page slice only ──────────────── @@ -127,7 +132,8 @@ public async Task> Handle( ReplyCount: replyCount); }).ToList(); - return new PagedResult( + var result = new PagedResult( items, pagePostsResult.Page, pagePostsResult.PageSize, pagePostsResult.Total); + return _msg.Ok(result, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Queries/ListJoinRequests/ListJoinRequestsQueryHandler.cs b/backend/src/CCE.Application/Community/Queries/ListJoinRequests/ListJoinRequestsQueryHandler.cs index 82fc1466..c34e2fe3 100644 --- a/backend/src/CCE.Application/Community/Queries/ListJoinRequests/ListJoinRequestsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Queries/ListJoinRequests/ListJoinRequestsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Public.Dtos; @@ -31,6 +31,6 @@ public async Task>> Handle( .ToPagedResultAsync(request.Page, request.PageSize, cancellationToken) .ConfigureAwait(false); - return _msg.Ok(paged, "ITEMS_LISTED"); + return _msg.Ok(paged, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Community/Queries/ListTopics/ListTopicsQueryHandler.cs b/backend/src/CCE.Application/Community/Queries/ListTopics/ListTopicsQueryHandler.cs index 8c3c2cf2..1670bd29 100644 --- a/backend/src/CCE.Application/Community/Queries/ListTopics/ListTopicsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Queries/ListTopics/ListTopicsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Community.Dtos; @@ -50,7 +50,7 @@ public async Task>> Handle( var page = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken) .ConfigureAwait(false); - return _messages.Ok(page.Map(MapToDto), "ITEMS_LISTED"); + return _messages.Ok(page.Map(MapToDto), MessageKeys.General.ITEMS_LISTED); } internal static TopicDto MapToDto(Topic t) => new( diff --git a/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommandHandler.cs index 9a8bb195..dd8bb72a 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateEvent/CreateEventCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -33,7 +33,7 @@ public async Task> Handle(CreateEventCommand request, Cancell { var topicExists = await _db.Topics.Where(t => t.Id == request.TopicId).CountAsyncEither(cancellationToken) > 0; if (!topicExists) - return _messages.NotFound("TOPIC_NOT_FOUND"); + return _messages.NotFound(MessageKeys.Community.TOPIC_NOT_FOUND); var ev = Event.Schedule( request.TitleAr, @@ -66,6 +66,6 @@ public async Task> Handle(CreateEventCommand request, Cancell var topicNameAr = topic.FirstOrDefault()?.NameAr ?? string.Empty; var topicNameEn = topic.FirstOrDefault()?.NameEn ?? string.Empty; - return _messages.Ok(ListEventsQueryHandler.MapToDto(ev, topicNameAr, topicNameEn, ev.Tags.Select(t => new TagDto(t.Id, t.NameAr, t.NameEn, t.Color)).ToList()), "CONTENT_CREATED"); + return _messages.Ok(ListEventsQueryHandler.MapToDto(ev, topicNameAr, topicNameEn, ev.Tags.Select(t => new TagDto(t.Id, t.NameAr, t.NameEn, t.Color)).ToList()), MessageKeys.Content.CONTENT_CREATED); } } diff --git a/backend/src/CCE.Application/Content/Commands/CreateHomepageSection/CreateHomepageSectionCommand.cs b/backend/src/CCE.Application/Content/Commands/CreateHomepageSection/CreateHomepageSectionCommand.cs index e6a02f99..74fef6f1 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateHomepageSection/CreateHomepageSectionCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateHomepageSection/CreateHomepageSectionCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using CCE.Domain.Content; using MediatR; @@ -8,4 +9,4 @@ public sealed record CreateHomepageSectionCommand( HomepageSectionType SectionType, int OrderIndex, string ContentAr, - string ContentEn) : IRequest; + string ContentEn) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/CreateHomepageSection/CreateHomepageSectionCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/CreateHomepageSection/CreateHomepageSectionCommandHandler.cs index 4b6729f2..e458e78f 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateHomepageSection/CreateHomepageSectionCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateHomepageSection/CreateHomepageSectionCommandHandler.cs @@ -1,20 +1,24 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using CCE.Application.Content.Queries.ListHomepageSections; +using CCE.Application.Messages; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Commands.CreateHomepageSection; -public sealed class CreateHomepageSectionCommandHandler : IRequestHandler +public sealed class CreateHomepageSectionCommandHandler : IRequestHandler> { private readonly IHomepageSectionRepository _service; + private readonly MessageFactory _msg; - public CreateHomepageSectionCommandHandler(IHomepageSectionRepository service) + public CreateHomepageSectionCommandHandler(IHomepageSectionRepository service, MessageFactory msg) { _service = service; + _msg = msg; } - public async Task Handle(CreateHomepageSectionCommand request, CancellationToken cancellationToken) + public async Task> Handle(CreateHomepageSectionCommand request, CancellationToken cancellationToken) { var section = HomepageSection.Create( request.SectionType, @@ -22,6 +26,6 @@ public async Task Handle(CreateHomepageSectionCommand reques request.ContentAr, request.ContentEn); await _service.SaveAsync(section, cancellationToken).ConfigureAwait(false); - return ListHomepageSectionsQueryHandler.MapToDto(section); + return _msg.Ok(ListHomepageSectionsQueryHandler.MapToDto(section), MessageKeys.Content.CONTENT_CREATED); } } diff --git a/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandHandler.cs index a01bc1d0..71b99c99 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -40,7 +40,7 @@ public async Task> Handle(CreateNewsCommand request, Cancellat var topicExists = await _db.Topics.Where(t => t.Id == request.TopicId).CountAsyncEither(cancellationToken) > 0; if (!topicExists) - return _messages.NotFound("TOPIC_NOT_FOUND"); + return _messages.NotFound(MessageKeys.Community.TOPIC_NOT_FOUND); var news = News.Draft( request.TitleAr, @@ -69,6 +69,6 @@ public async Task> Handle(CreateNewsCommand request, Cancellat var topicNameAr = topic.FirstOrDefault()?.NameAr ?? string.Empty; var topicNameEn = topic.FirstOrDefault()?.NameEn ?? string.Empty; - return _messages.Ok(ListNewsQueryHandler.MapToDto(news, topicNameAr, topicNameEn, news.Tags.Select(t => new TagDto(t.Id, t.NameAr, t.NameEn, t.Color)).ToList()), "CONTENT_CREATED"); + return _messages.Ok(ListNewsQueryHandler.MapToDto(news, topicNameAr, topicNameEn, news.Tags.Select(t => new TagDto(t.Id, t.NameAr, t.NameEn, t.Color)).ToList()), MessageKeys.Content.CONTENT_CREATED); } } diff --git a/backend/src/CCE.Application/Content/Commands/CreatePage/CreatePageCommand.cs b/backend/src/CCE.Application/Content/Commands/CreatePage/CreatePageCommand.cs index 7274d5cc..f286e30b 100644 --- a/backend/src/CCE.Application/Content/Commands/CreatePage/CreatePageCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/CreatePage/CreatePageCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using CCE.Domain.Content; using MediatR; @@ -10,4 +11,4 @@ public sealed record CreatePageCommand( string TitleAr, string TitleEn, string ContentAr, - string ContentEn) : IRequest; + string ContentEn) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/CreatePage/CreatePageCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/CreatePage/CreatePageCommandHandler.cs index 2e970275..f41b1f9a 100644 --- a/backend/src/CCE.Application/Content/Commands/CreatePage/CreatePageCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/CreatePage/CreatePageCommandHandler.cs @@ -1,27 +1,31 @@ +using CCE.Application.Common; using CCE.Application.Content; using CCE.Application.Content.Dtos; using CCE.Application.Content.Queries.ListPages; +using CCE.Application.Messages; using CCE.Domain.Content; using MediatR; namespace CCE.Application.Content.Commands.CreatePage; -public sealed class CreatePageCommandHandler : IRequestHandler +public sealed class CreatePageCommandHandler : IRequestHandler> { private readonly IPageRepository _service; + private readonly MessageFactory _msg; - public CreatePageCommandHandler(IPageRepository service) + public CreatePageCommandHandler(IPageRepository service, MessageFactory msg) { _service = service; + _msg = msg; } - public async Task Handle(CreatePageCommand request, CancellationToken cancellationToken) + public async Task> Handle(CreatePageCommand request, CancellationToken cancellationToken) { var page = Page.Create( request.Slug, request.PageType, request.TitleAr, request.TitleEn, request.ContentAr, request.ContentEn); await _service.SaveAsync(page, cancellationToken).ConfigureAwait(false); - return ListPagesQueryHandler.MapToDto(page); + return _msg.Ok(ListPagesQueryHandler.MapToDto(page), MessageKeys.Content.CONTENT_CREATED); } } diff --git a/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandHandler.cs index 8a71d264..2ae72174 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -56,7 +56,7 @@ public CreateResourceCommandHandler( .CountAsyncEither(cancellationToken) .ConfigureAwait(false); if (existingCountryCount != countryIds.Count) - return _messages.NotFound("COUNTRY_NOT_FOUND"); + return _messages.NotFound(MessageKeys.Country.COUNTRY_NOT_FOUND); } var uploadedById = _currentUser.GetUserId(); @@ -81,7 +81,7 @@ public CreateResourceCommandHandler( await _repo.AddAsync(resource, cancellationToken).ConfigureAwait(false); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok(resource.Id, "RESOURCE_CREATED"); + return _messages.Ok(resource.Id, MessageKeys.Content.RESOURCE_CREATED); } private static async Task ExistsAsync(IQueryable query, CancellationToken ct) diff --git a/backend/src/CCE.Application/Content/Commands/CreateResourceCategory/CreateResourceCategoryCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/CreateResourceCategory/CreateResourceCategoryCommandHandler.cs index bf70d285..15f5232d 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateResourceCategory/CreateResourceCategoryCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateResourceCategory/CreateResourceCategoryCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content.Dtos; using CCE.Application.Content.Queries.ListResourceCategories; @@ -36,6 +36,6 @@ public async Task> Handle(CreateResourceCategoryCo await _repo.AddAsync(category, cancellationToken).ConfigureAwait(false); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok(ListResourceCategoriesQueryHandler.MapToDto(category), "CONTENT_CREATED"); + return _messages.Ok(ListResourceCategoriesQueryHandler.MapToDto(category), MessageKeys.Content.CONTENT_CREATED); } } diff --git a/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommandHandler.cs index 7275628f..99267b5c 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -42,6 +42,6 @@ public async Task> Handle(DeleteEventCommand request, Cancell ev.SoftDelete(userId.Value, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok("CONTENT_DELETED"); + return _messages.Ok(MessageKeys.Content.CONTENT_DELETED); } } diff --git a/backend/src/CCE.Application/Content/Commands/DeleteHomepageSection/DeleteHomepageSectionCommand.cs b/backend/src/CCE.Application/Content/Commands/DeleteHomepageSection/DeleteHomepageSectionCommand.cs index d6413a80..9fefd115 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteHomepageSection/DeleteHomepageSectionCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteHomepageSection/DeleteHomepageSectionCommand.cs @@ -1,5 +1,6 @@ +using CCE.Application.Common; using MediatR; namespace CCE.Application.Content.Commands.DeleteHomepageSection; -public sealed record DeleteHomepageSectionCommand(System.Guid Id) : IRequest; +public sealed record DeleteHomepageSectionCommand(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/DeleteHomepageSection/DeleteHomepageSectionCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteHomepageSection/DeleteHomepageSectionCommandHandler.cs index 051c2ceb..1bc1c732 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteHomepageSection/DeleteHomepageSectionCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteHomepageSection/DeleteHomepageSectionCommandHandler.cs @@ -1,35 +1,42 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; +using CCE.Application.Messages; using CCE.Domain.Common; using MediatR; namespace CCE.Application.Content.Commands.DeleteHomepageSection; -public sealed class DeleteHomepageSectionCommandHandler : IRequestHandler +public sealed class DeleteHomepageSectionCommandHandler : IRequestHandler> { private readonly IHomepageSectionRepository _service; private readonly ICurrentUserAccessor _currentUser; private readonly ISystemClock _clock; + private readonly MessageFactory _msg; - public DeleteHomepageSectionCommandHandler(IHomepageSectionRepository service, ICurrentUserAccessor currentUser, ISystemClock clock) + public DeleteHomepageSectionCommandHandler(IHomepageSectionRepository service, ICurrentUserAccessor currentUser, ISystemClock clock, MessageFactory msg) { _service = service; _currentUser = currentUser; _clock = clock; + _msg = msg; } - public async Task Handle(DeleteHomepageSectionCommand request, CancellationToken cancellationToken) + public async Task> Handle(DeleteHomepageSectionCommand request, CancellationToken cancellationToken) { var section = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); if (section is null) { - throw new System.Collections.Generic.KeyNotFoundException($"HomepageSection {request.Id} not found."); + return _msg.NotFound(MessageKeys.PlatformSettings.HOMEPAGE_SECTION_NOT_FOUND); } - var deletedById = _currentUser.GetUserId() - ?? throw new DomainException("Cannot delete homepage section from a request without a user identity."); + var deletedById = _currentUser.GetUserId(); + if (deletedById is null) + { + return _msg.NotAuthenticated(); + } - section.SoftDelete(deletedById, _clock); + section.SoftDelete(deletedById.Value, _clock); await _service.UpdateAsync(section, cancellationToken).ConfigureAwait(false); - return Unit.Value; + return _msg.Ok(MessageKeys.Content.CONTENT_DELETED); } } diff --git a/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommandHandler.cs index 50b547d2..9b70f3f8 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -42,6 +42,6 @@ public async Task> Handle(DeleteNewsCommand request, Cancella news.SoftDelete(userId.Value, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok("CONTENT_DELETED"); + return _messages.Ok(MessageKeys.Content.CONTENT_DELETED); } } diff --git a/backend/src/CCE.Application/Content/Commands/DeletePage/DeletePageCommand.cs b/backend/src/CCE.Application/Content/Commands/DeletePage/DeletePageCommand.cs index 5b203195..b70c6aa3 100644 --- a/backend/src/CCE.Application/Content/Commands/DeletePage/DeletePageCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/DeletePage/DeletePageCommand.cs @@ -1,5 +1,6 @@ +using CCE.Application.Common; using MediatR; namespace CCE.Application.Content.Commands.DeletePage; -public sealed record DeletePageCommand(System.Guid Id) : IRequest; +public sealed record DeletePageCommand(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/DeletePage/DeletePageCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeletePage/DeletePageCommandHandler.cs index 6b5ee194..1c2fa663 100644 --- a/backend/src/CCE.Application/Content/Commands/DeletePage/DeletePageCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeletePage/DeletePageCommandHandler.cs @@ -1,36 +1,43 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content; +using CCE.Application.Messages; using CCE.Domain.Common; using MediatR; namespace CCE.Application.Content.Commands.DeletePage; -public sealed class DeletePageCommandHandler : IRequestHandler +public sealed class DeletePageCommandHandler : IRequestHandler> { private readonly IPageRepository _service; private readonly ICurrentUserAccessor _currentUser; private readonly ISystemClock _clock; + private readonly MessageFactory _msg; - public DeletePageCommandHandler(IPageRepository service, ICurrentUserAccessor currentUser, ISystemClock clock) + public DeletePageCommandHandler(IPageRepository service, ICurrentUserAccessor currentUser, ISystemClock clock, MessageFactory msg) { _service = service; _currentUser = currentUser; _clock = clock; + _msg = msg; } - public async Task Handle(DeletePageCommand request, CancellationToken cancellationToken) + public async Task> Handle(DeletePageCommand request, CancellationToken cancellationToken) { var page = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); if (page is null) { - throw new System.Collections.Generic.KeyNotFoundException($"Page {request.Id} not found."); + return _msg.PageNotFound(); } - var deletedById = _currentUser.GetUserId() - ?? throw new DomainException("Cannot delete page from a request without a user identity."); + var deletedById = _currentUser.GetUserId(); + if (deletedById is null) + { + return _msg.NotAuthenticated(); + } - page.SoftDelete(deletedById, _clock); + page.SoftDelete(deletedById.Value, _clock); await _service.UpdateAsync(page, page.RowVersion, cancellationToken).ConfigureAwait(false); - return Unit.Value; + return _msg.Ok(MessageKeys.Content.CONTENT_DELETED); } } diff --git a/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommandHandler.cs index 5d48354e..f66b1258 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -42,6 +42,6 @@ public async Task> Handle(DeleteResourceCommand request, Canc resource.SoftDelete(userId.Value, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok("RESOURCE_DELETED"); + return _messages.Ok(MessageKeys.Content.RESOURCE_DELETED); } } diff --git a/backend/src/CCE.Application/Content/Commands/DeleteResourceCategory/DeleteResourceCategoryCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteResourceCategory/DeleteResourceCategoryCommandHandler.cs index 458f8235..bc531900 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteResourceCategory/DeleteResourceCategoryCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteResourceCategory/DeleteResourceCategoryCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Content; @@ -31,6 +31,6 @@ public async Task> Handle(DeleteResourceCategoryCommand reque category.Deactivate(); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok("CONTENT_DELETED"); + return _messages.Ok(MessageKeys.Content.CONTENT_DELETED); } } diff --git a/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommandHandler.cs index 408d7e23..ee14eb26 100644 --- a/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content.Dtos; using CCE.Application.Content.Queries.GetNewsById; @@ -40,6 +40,6 @@ public async Task> Handle(PublishNewsCommand request, Cancella _db.SetExpectedRowVersion(news, expectedRowVersion); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok(GetNewsByIdQueryHandler.MapToDto(news), "SUCCESS_OPERATION"); + return _messages.Ok(GetNewsByIdQueryHandler.MapToDto(news), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommandHandler.cs index 4ae8fc8e..69254361 100644 --- a/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -49,6 +49,6 @@ public PublishResourceCommandHandler( _db.SetExpectedRowVersion(resource, expectedRowVersion); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok(resource.Id, "SUCCESS_OPERATION"); + return _messages.Ok(resource.Id, MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Commands/ReorderHomepageSections/ReorderHomepageSectionsCommand.cs b/backend/src/CCE.Application/Content/Commands/ReorderHomepageSections/ReorderHomepageSectionsCommand.cs index 56bd4d45..14d8f700 100644 --- a/backend/src/CCE.Application/Content/Commands/ReorderHomepageSections/ReorderHomepageSectionsCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/ReorderHomepageSections/ReorderHomepageSectionsCommand.cs @@ -1,9 +1,10 @@ +using CCE.Application.Common; using MediatR; namespace CCE.Application.Content.Commands.ReorderHomepageSections; public sealed record ReorderHomepageSectionsCommand( System.Collections.Generic.IReadOnlyList Assignments) - : IRequest; + : IRequest>; public sealed record HomepageSectionOrderAssignment(System.Guid Id, int OrderIndex); diff --git a/backend/src/CCE.Application/Content/Commands/ReorderHomepageSections/ReorderHomepageSectionsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/ReorderHomepageSections/ReorderHomepageSectionsCommandHandler.cs index 748df251..fd7a9de8 100644 --- a/backend/src/CCE.Application/Content/Commands/ReorderHomepageSections/ReorderHomepageSectionsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/ReorderHomepageSections/ReorderHomepageSectionsCommandHandler.cs @@ -1,22 +1,26 @@ +using CCE.Application.Common; using CCE.Application.Content; +using CCE.Application.Messages; using MediatR; namespace CCE.Application.Content.Commands.ReorderHomepageSections; public sealed class ReorderHomepageSectionsCommandHandler - : IRequestHandler + : IRequestHandler> { private readonly IHomepageSectionRepository _service; + private readonly MessageFactory _msg; - public ReorderHomepageSectionsCommandHandler(IHomepageSectionRepository service) + public ReorderHomepageSectionsCommandHandler(IHomepageSectionRepository service, MessageFactory msg) { _service = service; + _msg = msg; } - public async Task Handle(ReorderHomepageSectionsCommand request, CancellationToken cancellationToken) + public async Task> Handle(ReorderHomepageSectionsCommand request, CancellationToken cancellationToken) { var pairs = request.Assignments.Select(a => (a.Id, a.OrderIndex)).ToList(); await _service.ReorderAsync(pairs, cancellationToken).ConfigureAwait(false); - return Unit.Value; + return _msg.Ok(MessageKeys.General.SUCCESS_UPDATED); } } diff --git a/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandHandler.cs index 6d33c647..bc7daff0 100644 --- a/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content.Dtos; using CCE.Application.Content.Queries.GetEventById; @@ -37,6 +37,6 @@ public async Task> Handle(RescheduleEventCommand request, Can _db.SetExpectedRowVersion(ev, expectedRowVersion); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok(GetEventByIdQueryHandler.MapToDto(ev), "SUCCESS_OPERATION"); + return _messages.Ok(GetEventByIdQueryHandler.MapToDto(ev), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Commands/SubmitCountryContentRequest/SubmitCountryContentRequestCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/SubmitCountryContentRequest/SubmitCountryContentRequestCommandHandler.cs index ed9925d9..25401d95 100644 --- a/backend/src/CCE.Application/Content/Commands/SubmitCountryContentRequest/SubmitCountryContentRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/SubmitCountryContentRequest/SubmitCountryContentRequestCommandHandler.cs @@ -1,9 +1,9 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.CountryScope; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Application.Notifications.Messages; using CCE.Domain.Common; using CCE.Domain.Content; @@ -73,7 +73,7 @@ await _dispatcher.DispatchAsync(new NotificationMessage( }), cancellationToken).ConfigureAwait(false); - return _messages.Ok(contentRequest.Id, ApplicationErrors.Content.COUNTRY_CONTENT_REQUEST_SUBMITTED); + return _messages.Ok(contentRequest.Id, MessageKeys.Content.COUNTRY_CONTENT_REQUEST_SUBMITTED); } private async Task ResolveCountryIdAsync( diff --git a/backend/src/CCE.Application/Content/Commands/SubscribeNewsletter/SubscribeNewsletterCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/SubscribeNewsletter/SubscribeNewsletterCommandHandler.cs index a371027c..9fc99d36 100644 --- a/backend/src/CCE.Application/Content/Commands/SubscribeNewsletter/SubscribeNewsletterCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/SubscribeNewsletter/SubscribeNewsletterCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -36,7 +36,7 @@ public async Task> Handle( if (existing is not null) { if (existing.UnsubscribedOn is null) - return _messages.Ok("NEWSLETTER_SUBSCRIBED"); + return _messages.Ok(MessageKeys.Content.NEWSLETTER_SUBSCRIBED); existing.Resubscribe(request.Locale, _clock); } @@ -47,6 +47,6 @@ public async Task> Handle( } await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok("NEWSLETTER_SUBSCRIBED"); + return _messages.Ok(MessageKeys.Content.NEWSLETTER_SUBSCRIBED); } } diff --git a/backend/src/CCE.Application/Content/Commands/Tags/CreateTag/CreateTagCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/Tags/CreateTag/CreateTagCommandHandler.cs index 9c2dd3bb..12700f51 100644 --- a/backend/src/CCE.Application/Content/Commands/Tags/CreateTag/CreateTagCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/Tags/CreateTag/CreateTagCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content.Dtos; using CCE.Application.Messages; @@ -22,6 +22,6 @@ public async Task> Handle(CreateTagCommand request, Cancellatio { var tag = Tag.Create(request.NameAr, request.NameEn, request.Color); await _repo.AddAsync(tag, cancellationToken).ConfigureAwait(false); - return _messages.Ok(new TagDto(tag.Id, tag.NameAr, tag.NameEn, tag.Color), "CONTENT_CREATED"); + return _messages.Ok(new TagDto(tag.Id, tag.NameAr, tag.NameEn, tag.Color), MessageKeys.Content.CONTENT_CREATED); } } diff --git a/backend/src/CCE.Application/Content/Commands/Tags/DeleteTag/DeleteTagCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/Tags/DeleteTag/DeleteTagCommandHandler.cs index 18e96d95..2c316c63 100644 --- a/backend/src/CCE.Application/Content/Commands/Tags/DeleteTag/DeleteTagCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/Tags/DeleteTag/DeleteTagCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Content; @@ -21,9 +21,9 @@ public async Task> Handle(DeleteTagCommand request, Cancellat { var tag = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (tag is null) - return _messages.NotFound("TAG_NOT_FOUND"); + return _messages.NotFound(MessageKeys.Content.TAG_NOT_FOUND); _repo.Delete(tag); - return _messages.Ok(VoidData.Instance, "SUCCESS_OPERATION"); + return _messages.Ok(VoidData.Instance, MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Commands/Tags/UpdateTag/UpdateTagCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/Tags/UpdateTag/UpdateTagCommandHandler.cs index f7c50a69..c15e098e 100644 --- a/backend/src/CCE.Application/Content/Commands/Tags/UpdateTag/UpdateTagCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/Tags/UpdateTag/UpdateTagCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content.Dtos; using CCE.Application.Messages; @@ -22,9 +22,9 @@ public async Task> Handle(UpdateTagCommand request, Cancellatio { var tag = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (tag is null) - return _messages.NotFound("TAG_NOT_FOUND"); + return _messages.NotFound(MessageKeys.Content.TAG_NOT_FOUND); tag.Update(request.NameAr, request.NameEn, request.Color); - return _messages.Ok(new TagDto(tag.Id, tag.NameAr, tag.NameEn, tag.Color), "SUCCESS_OPERATION"); + return _messages.Ok(new TagDto(tag.Id, tag.NameAr, tag.NameEn, tag.Color), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandHandler.cs index 350db354..be5cd520 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -38,7 +38,7 @@ public async Task> Handle(UpdateEventCommand request, Cancell var topicExists = await _db.Topics.Where(t => t.Id == request.TopicId).CountAsyncEither(cancellationToken) > 0; if (!topicExists) - return _messages.NotFound("TOPIC_NOT_FOUND"); + return _messages.NotFound(MessageKeys.Community.TOPIC_NOT_FOUND); var expectedRowVersion = ev.RowVersion; ev.UpdateContent( @@ -70,6 +70,6 @@ public async Task> Handle(UpdateEventCommand request, Cancell var topicNameEn = topic.FirstOrDefault()?.NameEn ?? string.Empty; var tagDtos = ev.Tags.Select(t => new TagDto(t.Id, t.NameAr, t.NameEn, t.Color)).ToList(); - return _messages.Ok(GetEventByIdQueryHandler.MapToDto(ev, topicNameAr, topicNameEn, tagDtos), "SUCCESS_OPERATION"); + return _messages.Ok(GetEventByIdQueryHandler.MapToDto(ev, topicNameAr, topicNameEn, tagDtos), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Commands/UpdateHomepageSection/UpdateHomepageSectionCommand.cs b/backend/src/CCE.Application/Content/Commands/UpdateHomepageSection/UpdateHomepageSectionCommand.cs index 133af06c..c042b51b 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateHomepageSection/UpdateHomepageSectionCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateHomepageSection/UpdateHomepageSectionCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; @@ -7,4 +8,4 @@ public sealed record UpdateHomepageSectionCommand( System.Guid Id, string ContentAr, string ContentEn, - bool IsActive) : IRequest; + bool IsActive) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/UpdateHomepageSection/UpdateHomepageSectionCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdateHomepageSection/UpdateHomepageSectionCommandHandler.cs index 64c0e587..2e91da6a 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateHomepageSection/UpdateHomepageSectionCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateHomepageSection/UpdateHomepageSectionCommandHandler.cs @@ -1,24 +1,28 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using CCE.Application.Content.Queries.ListHomepageSections; +using CCE.Application.Messages; using MediatR; namespace CCE.Application.Content.Commands.UpdateHomepageSection; -public sealed class UpdateHomepageSectionCommandHandler : IRequestHandler +public sealed class UpdateHomepageSectionCommandHandler : IRequestHandler> { private readonly IHomepageSectionRepository _service; + private readonly MessageFactory _msg; - public UpdateHomepageSectionCommandHandler(IHomepageSectionRepository service) + public UpdateHomepageSectionCommandHandler(IHomepageSectionRepository service, MessageFactory msg) { _service = service; + _msg = msg; } - public async Task Handle(UpdateHomepageSectionCommand request, CancellationToken cancellationToken) + public async Task> Handle(UpdateHomepageSectionCommand request, CancellationToken cancellationToken) { var section = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); if (section is null) { - return null; + return _msg.NotFound(MessageKeys.PlatformSettings.HOMEPAGE_SECTION_NOT_FOUND); } section.UpdateContent(request.ContentAr, request.ContentEn); @@ -30,6 +34,6 @@ public UpdateHomepageSectionCommandHandler(IHomepageSectionRepository service) await _service.UpdateAsync(section, cancellationToken).ConfigureAwait(false); - return ListHomepageSectionsQueryHandler.MapToDto(section); + return _msg.Ok(ListHomepageSectionsQueryHandler.MapToDto(section), MessageKeys.Content.CONTENT_UPDATED); } } diff --git a/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandHandler.cs index 741c1e28..04bad0a4 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -38,7 +38,7 @@ public async Task> Handle(UpdateNewsCommand request, Cancellat var topicExists = await _db.Topics.Where(t => t.Id == request.TopicId).CountAsyncEither(cancellationToken) > 0; if (!topicExists) - return _messages.NotFound("TOPIC_NOT_FOUND"); + return _messages.NotFound(MessageKeys.Community.TOPIC_NOT_FOUND); var expectedRowVersion = news.RowVersion; news.UpdateContent( @@ -67,6 +67,6 @@ public async Task> Handle(UpdateNewsCommand request, Cancellat var topicNameEn = topic.FirstOrDefault()?.NameEn ?? string.Empty; var tagDtos = news.Tags.Select(t => new TagDto(t.Id, t.NameAr, t.NameEn, t.Color)).ToList(); - return _messages.Ok(GetNewsByIdQueryHandler.MapToDto(news, topicNameAr, topicNameEn, tagDtos), "SUCCESS_OPERATION"); + return _messages.Ok(GetNewsByIdQueryHandler.MapToDto(news, topicNameAr, topicNameEn, tagDtos), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Commands/UpdatePage/UpdatePageCommand.cs b/backend/src/CCE.Application/Content/Commands/UpdatePage/UpdatePageCommand.cs index 5d139ddd..a8068b5c 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdatePage/UpdatePageCommand.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdatePage/UpdatePageCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; @@ -9,4 +10,4 @@ public sealed record UpdatePageCommand( string TitleEn, string ContentAr, string ContentEn, - byte[] RowVersion) : IRequest; + byte[] RowVersion) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Commands/UpdatePage/UpdatePageCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdatePage/UpdatePageCommandHandler.cs index d1e0377f..462208a7 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdatePage/UpdatePageCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdatePage/UpdatePageCommandHandler.cs @@ -1,24 +1,28 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using CCE.Application.Content.Queries.ListPages; +using CCE.Application.Messages; using MediatR; namespace CCE.Application.Content.Commands.UpdatePage; -public sealed class UpdatePageCommandHandler : IRequestHandler +public sealed class UpdatePageCommandHandler : IRequestHandler> { private readonly IPageRepository _service; + private readonly MessageFactory _msg; - public UpdatePageCommandHandler(IPageRepository service) + public UpdatePageCommandHandler(IPageRepository service, MessageFactory msg) { _service = service; + _msg = msg; } - public async Task Handle(UpdatePageCommand request, CancellationToken cancellationToken) + public async Task> Handle(UpdatePageCommand request, CancellationToken cancellationToken) { var page = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); if (page is null) { - return null; + return _msg.NotFound(MessageKeys.Content.PAGE_NOT_FOUND); } page.UpdateContent( @@ -29,6 +33,6 @@ public UpdatePageCommandHandler(IPageRepository service) await _service.UpdateAsync(page, request.RowVersion, cancellationToken).ConfigureAwait(false); - return ListPagesQueryHandler.MapToDto(page); + return _msg.Ok(ListPagesQueryHandler.MapToDto(page), MessageKeys.Content.CONTENT_UPDATED); } } diff --git a/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandHandler.cs index abbfcc93..a8db42fd 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -46,7 +46,7 @@ public UpdateResourceCommandHandler( .CountAsyncEither(cancellationToken) .ConfigureAwait(false); if (existingCountryCount != countryIds.Count) - return _messages.NotFound("COUNTRY_NOT_FOUND"); + return _messages.NotFound(MessageKeys.Country.COUNTRY_NOT_FOUND); } var expectedRowVersion = resource.RowVersion; @@ -64,7 +64,7 @@ public UpdateResourceCommandHandler( _db.SetExpectedRowVersion(resource, expectedRowVersion); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok(resource.Id, "SUCCESS_OPERATION"); + return _messages.Ok(resource.Id, MessageKeys.General.SUCCESS_OPERATION); } private static async Task ExistsAsync(IQueryable query, CancellationToken ct) diff --git a/backend/src/CCE.Application/Content/Commands/UpdateResourceCategory/UpdateResourceCategoryCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdateResourceCategory/UpdateResourceCategoryCommandHandler.cs index 939f102c..056021c7 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateResourceCategory/UpdateResourceCategoryCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateResourceCategory/UpdateResourceCategoryCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content.Dtos; using CCE.Application.Content.Queries.ListResourceCategories; @@ -40,6 +40,6 @@ public async Task> Handle(UpdateResourceCategoryCo await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.Ok(ListResourceCategoriesQueryHandler.MapToDto(category), "SUCCESS_OPERATION"); + return _messages.Ok(ListResourceCategoriesQueryHandler.MapToDto(category), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Commands/UploadAsset/UploadAssetCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UploadAsset/UploadAssetCommandHandler.cs index c9322b8e..3909a958 100644 --- a/backend/src/CCE.Application/Content/Commands/UploadAsset/UploadAssetCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UploadAsset/UploadAssetCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content.Dtos; using CCE.Application.Messages; @@ -93,6 +93,6 @@ public async Task> Handle(UploadAssetCommand request, Can asset.UploadedById, asset.UploadedOn, asset.VirusScanStatus, - asset.ScannedOn), "ASSET_UPLOADED"); + asset.ScannedOn), MessageKeys.Content.ASSET_UPLOADED); } } 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 c0f6e489..db6f031a 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicEventById/GetPublicEventByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicEventById/GetPublicEventByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -36,7 +36,7 @@ public async Task> Handle(GetPublicEventByIdQuery reque var tagDtos = ev.Tags.Select(t => new TagDto(t.Id, t.NameAr, t.NameEn, t.Color)).ToList(); - return _messages.Ok(MapToDto(ev, topic?.NameAr ?? string.Empty, topic?.NameEn ?? string.Empty, tagDtos), "SUCCESS_OPERATION"); + return _messages.Ok(MapToDto(ev, topic?.NameAr ?? string.Empty, topic?.NameEn ?? string.Empty, tagDtos), MessageKeys.General.SUCCESS_OPERATION); } internal static PublicEventDto MapToDto(Event e, string topicNameAr, string topicNameEn, System.Collections.Generic.IReadOnlyList? tags = null) => new( diff --git a/backend/src/CCE.Application/Content/Public/Queries/GetPublicNewsById/GetPublicNewsByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/GetPublicNewsById/GetPublicNewsByIdQueryHandler.cs index 4d0f38fa..47910484 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicNewsById/GetPublicNewsByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicNewsById/GetPublicNewsByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -36,7 +36,7 @@ public async Task> Handle(GetPublicNewsByIdQuery request var tagDtos = news.Tags.Select(t => new TagDto(t.Id, t.NameAr, t.NameEn, t.Color)).ToList(); - return _messages.Ok(MapToDto(news, topic?.NameAr ?? string.Empty, topic?.NameEn ?? string.Empty, tagDtos), "SUCCESS_OPERATION"); + return _messages.Ok(MapToDto(news, topic?.NameAr ?? string.Empty, topic?.NameEn ?? string.Empty, tagDtos), MessageKeys.General.SUCCESS_OPERATION); } internal static PublicNewsDto MapToDto(News n, string topicNameAr, string topicNameEn, System.Collections.Generic.IReadOnlyList? tags = null) => new( diff --git a/backend/src/CCE.Application/Content/Public/Queries/GetPublicPageBySlug/GetPublicPageBySlugQuery.cs b/backend/src/CCE.Application/Content/Public/Queries/GetPublicPageBySlug/GetPublicPageBySlugQuery.cs index 366003d8..e1eb8b38 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicPageBySlug/GetPublicPageBySlugQuery.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicPageBySlug/GetPublicPageBySlugQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Content.Public.Dtos; using MediatR; namespace CCE.Application.Content.Public.Queries.GetPublicPageBySlug; -public sealed record GetPublicPageBySlugQuery(string Slug) : IRequest; +public sealed record GetPublicPageBySlugQuery(string Slug) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Public/Queries/GetPublicPageBySlug/GetPublicPageBySlugQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/GetPublicPageBySlug/GetPublicPageBySlugQueryHandler.cs index 7fa87b6d..83f26409 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicPageBySlug/GetPublicPageBySlugQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicPageBySlug/GetPublicPageBySlugQueryHandler.cs @@ -1,25 +1,34 @@ +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.GetPublicPageBySlug; -public sealed class GetPublicPageBySlugQueryHandler : IRequestHandler +public sealed class GetPublicPageBySlugQueryHandler : IRequestHandler> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public GetPublicPageBySlugQueryHandler(ICceDbContext db) => _db = db; + public GetPublicPageBySlugQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task Handle(GetPublicPageBySlugQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetPublicPageBySlugQuery request, CancellationToken cancellationToken) { var list = await _db.Pages .Where(p => p.Slug == request.Slug) .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); var pageEntity = list.SingleOrDefault(); - return pageEntity is null ? null : MapToDto(pageEntity); + return pageEntity is null + ? _msg.NotFound(MessageKeys.Content.PAGE_NOT_FOUND) + : _msg.Ok(MapToDto(pageEntity), MessageKeys.General.SUCCESS_OPERATION); } internal static PublicPageDto MapToDto(Page p) => new( 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 661d9a6e..bdabb005 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicResourceById/GetPublicResourceByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicResourceById/GetPublicResourceByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; @@ -31,7 +31,7 @@ public async Task> Handle(GetPublicResourceByIdQuery var resource = list.SingleOrDefault(); if (resource is null || resource.PublishedOn is null) return _messages.ResourceNotFound(); - return _messages.Ok(await MapToDtoAsync(resource, cancellationToken).ConfigureAwait(false), "SUCCESS_OPERATION"); + return _messages.Ok(await MapToDtoAsync(resource, cancellationToken).ConfigureAwait(false), MessageKeys.General.SUCCESS_OPERATION); } private async Task MapToDtoAsync(Resource r, CancellationToken ct) diff --git a/backend/src/CCE.Application/Content/Public/Queries/GetShareLink/GetShareLinkQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/GetShareLink/GetShareLinkQueryHandler.cs index 8a68b988..afc914da 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetShareLink/GetShareLinkQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetShareLink/GetShareLinkQueryHandler.cs @@ -1,9 +1,9 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Content; using MediatR; @@ -38,9 +38,9 @@ public async Task> Handle( }; if (dto is null) - return _messages.NotFound(ApplicationErrors.General.NOT_FOUND); + return _messages.NotFound(MessageKeys.General.RESOURCE_NOT_FOUND_GENERIC); - return _messages.Ok(dto, ApplicationErrors.General.SUCCESS_OPERATION); + return _messages.Ok(dto, MessageKeys.General.SUCCESS_OPERATION); } private async Task GetNewsAsync( diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListHomepageFeed/ListHomepageFeedQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/ListHomepageFeed/ListHomepageFeedQueryHandler.cs index fc4756ae..d7e828f7 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListHomepageFeed/ListHomepageFeedQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListHomepageFeed/ListHomepageFeedQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; @@ -91,7 +91,7 @@ public async Task>> Handle( return string.IsNullOrEmpty(fullName) ? u.UserName ?? string.Empty : fullName; }); - return _messages.Ok(result.Map(r => MapToDto(r, topicById, authorById)), "ITEMS_LISTED"); + return _messages.Ok(result.Map(r => MapToDto(r, topicById, authorById)), MessageKeys.General.ITEMS_LISTED); } private static IQueryable ApplySort( 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 4d998854..24de7e7f 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicEvents/ListPublicEventsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicEvents/ListPublicEventsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -71,7 +71,7 @@ public async Task>> Handle(ListPublicEvents var eventIds = result.Items.Select(e => e.Id).ToList(); var tagByEventId = await GetTagDtosByEventIdsAsync(eventIds, cancellationToken).ConfigureAwait(false); - return _messages.Ok(result.Map(e => MapToDto(e, topicById, tagByEventId)), "ITEMS_LISTED"); + return _messages.Ok(result.Map(e => MapToDto(e, topicById, tagByEventId)), MessageKeys.General.ITEMS_LISTED); } private async Task>> GetTagDtosByEventIdsAsync( diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListPublicHomepageSections/ListPublicHomepageSectionsQuery.cs b/backend/src/CCE.Application/Content/Public/Queries/ListPublicHomepageSections/ListPublicHomepageSectionsQuery.cs index 9357836a..9c5256d1 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicHomepageSections/ListPublicHomepageSectionsQuery.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicHomepageSections/ListPublicHomepageSectionsQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Content.Public.Dtos; using MediatR; namespace CCE.Application.Content.Public.Queries.ListPublicHomepageSections; -public sealed record ListPublicHomepageSectionsQuery() : IRequest>; +public sealed record ListPublicHomepageSectionsQuery() : IRequest>>; diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListPublicHomepageSections/ListPublicHomepageSectionsQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/ListPublicHomepageSections/ListPublicHomepageSectionsQueryHandler.cs index 87874520..7328962d 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicHomepageSections/ListPublicHomepageSectionsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicHomepageSections/ListPublicHomepageSectionsQueryHandler.cs @@ -1,19 +1,26 @@ +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.ListPublicHomepageSections; public sealed class ListPublicHomepageSectionsQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListPublicHomepageSectionsQueryHandler(ICceDbContext db) => _db = db; + public ListPublicHomepageSectionsQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task> Handle( + public async Task>> Handle( ListPublicHomepageSectionsQuery request, CancellationToken cancellationToken) { @@ -22,7 +29,7 @@ public sealed class ListPublicHomepageSectionsQueryHandler .OrderBy(s => s.OrderIndex) .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); - return list.Select(MapToDto).ToList(); + return _msg.Ok((System.Collections.Generic.IReadOnlyList)list.Select(MapToDto).ToList(), MessageKeys.General.ITEMS_LISTED); } internal static PublicHomepageSectionDto MapToDto(HomepageSection s) => new( 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 cb37d1cf..2dff5f48 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicNews/ListPublicNewsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicNews/ListPublicNewsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -62,7 +62,7 @@ public async Task>> Handle(ListPublicNewsQue var newsIds = result.Items.Select(n => n.Id).ToList(); var tagByNewsId = await GetTagDtosByNewsIdsAsync(newsIds, cancellationToken).ConfigureAwait(false); - return _messages.Ok(result.Map(n => MapToDto(n, topicById, tagByNewsId)), "ITEMS_LISTED"); + return _messages.Ok(result.Map(n => MapToDto(n, topicById, tagByNewsId)), MessageKeys.General.ITEMS_LISTED); } private async Task>> GetTagDtosByNewsIdsAsync( diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListPublicResourceCategories/ListPublicResourceCategoriesQuery.cs b/backend/src/CCE.Application/Content/Public/Queries/ListPublicResourceCategories/ListPublicResourceCategoriesQuery.cs index e800a696..c94d22c6 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicResourceCategories/ListPublicResourceCategoriesQuery.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicResourceCategories/ListPublicResourceCategoriesQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Content.Public.Dtos; using MediatR; namespace CCE.Application.Content.Public.Queries.ListPublicResourceCategories; -public sealed record ListPublicResourceCategoriesQuery() : IRequest>; +public sealed record ListPublicResourceCategoriesQuery() : IRequest>>; diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListPublicResourceCategories/ListPublicResourceCategoriesQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/ListPublicResourceCategories/ListPublicResourceCategoriesQueryHandler.cs index ea72e924..b264906b 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicResourceCategories/ListPublicResourceCategoriesQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicResourceCategories/ListPublicResourceCategoriesQueryHandler.cs @@ -1,19 +1,26 @@ +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.ListPublicResourceCategories; public sealed class ListPublicResourceCategoriesQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListPublicResourceCategoriesQueryHandler(ICceDbContext db) => _db = db; + public ListPublicResourceCategoriesQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task> Handle( + public async Task>> Handle( ListPublicResourceCategoriesQuery request, CancellationToken cancellationToken) { @@ -22,7 +29,7 @@ public sealed class ListPublicResourceCategoriesQueryHandler .OrderBy(c => c.OrderIndex) .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); - return list.Select(MapToDto).ToList(); + return _msg.Ok((System.Collections.Generic.IReadOnlyList)list.Select(MapToDto).ToList(), MessageKeys.General.ITEMS_LISTED); } internal static PublicResourceCategoryDto MapToDto(ResourceCategory c) => new( 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 40d692c6..f1d4b2f0 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicResources/ListPublicResourcesQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicResources/ListPublicResourcesQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; @@ -128,7 +128,7 @@ public async Task>> Handle(ListPublicRes }).ToList(); var result = new PagedResult(dtos, paged.Page, paged.PageSize, paged.Total); - return _messages.Ok(result, "ITEMS_LISTED"); + return _messages.Ok(result, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListPublicTags/ListPublicTagsQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/ListPublicTags/ListPublicTagsQueryHandler.cs index 9173f07c..e09649e3 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListPublicTags/ListPublicTagsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListPublicTags/ListPublicTagsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -28,6 +28,6 @@ public ListPublicTagsQueryHandler(ICceDbContext db, MessageFactory messages) .ConfigureAwait(false); var dtos = tags.Select(t => new TagDto(t.Id, t.NameAr, t.NameEn, t.Color)).ToList(); - return _messages.Ok(dtos, "ITEMS_LISTED"); + return _messages.Ok(dtos, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Content/Public/Queries/ListResourceTypes/ListResourceTypesQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/ListResourceTypes/ListResourceTypesQueryHandler.cs index d0a3531d..72fc3017 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/ListResourceTypes/ListResourceTypesQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/ListResourceTypes/ListResourceTypesQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Content.Public.Dtos; using CCE.Application.Messages; using CCE.Domain.Content; @@ -18,7 +18,7 @@ public Task>> Handle(ListResourceTypesQuery reque NameEn: e.ToString())) .ToList(); - return Task.FromResult(_messages.Ok(types, "ITEMS_LISTED")); + return Task.FromResult(_messages.Ok(types, MessageKeys.General.ITEMS_LISTED)); } private static string GetArabicName(ResourceType type) => type switch diff --git a/backend/src/CCE.Application/Content/Queries/DownloadFile/DownloadFileQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/DownloadFile/DownloadFileQueryHandler.cs index 979f0194..0ed5d42f 100644 --- a/backend/src/CCE.Application/Content/Queries/DownloadFile/DownloadFileQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/DownloadFile/DownloadFileQueryHandler.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content.Dtos; @@ -51,7 +51,7 @@ public async Task> Handle(DownloadFileQuery reques } var payload = new DownloadFilePayload(stream, media.MimeType, media.OriginalFileName); - return _msg.Ok(payload, "SUCCESS_OPERATION"); + return _msg.Ok(payload, MessageKeys.General.SUCCESS_OPERATION); } var asset = await _db.AssetFiles @@ -78,6 +78,6 @@ public async Task> Handle(DownloadFileQuery reques } var assetPayload = new DownloadFilePayload(assetStream, asset.MimeType, asset.OriginalFileName); - return _msg.Ok(assetPayload, "SUCCESS_OPERATION"); + return _msg.Ok(assetPayload, MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Queries/GetAssetById/GetAssetByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetAssetById/GetAssetByIdQueryHandler.cs index adcb67df..9a662f1d 100644 --- a/backend/src/CCE.Application/Content/Queries/GetAssetById/GetAssetByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetAssetById/GetAssetByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -44,6 +44,6 @@ public async Task> Handle(GetAssetByIdQuery request, Canc asset.UploadedById, asset.UploadedOn, asset.VirusScanStatus, - asset.ScannedOn), "SUCCESS_OPERATION"); + asset.ScannedOn), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Queries/GetCountryContentRequest/GetCountryContentRequestQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetCountryContentRequest/GetCountryContentRequestQueryHandler.cs index 83b59f1c..39c37b1c 100644 --- a/backend/src/CCE.Application/Content/Queries/GetCountryContentRequest/GetCountryContentRequestQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetCountryContentRequest/GetCountryContentRequestQueryHandler.cs @@ -1,10 +1,10 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.CountryScope; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; -using CCE.Application.Errors; using CCE.Application.Messages; + using MediatR; namespace CCE.Application.Content.Queries.GetCountryContentRequest; @@ -53,6 +53,6 @@ public async Task> Handle( entity.ProcessedById, entity.ProcessedOn, entity.ProposedKnowledgeLevelId, entity.ProposedJobSectorId); - return _messages.Ok(dto, ApplicationErrors.General.SUCCESS_OPERATION); + return _messages.Ok(dto, MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQueryHandler.cs index dbfbb800..1ef2ccba 100644 --- a/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -33,7 +33,7 @@ public async Task> Handle(GetEventByIdQuery request, Cancella var tagDtos = ev.Tags.Select(t => new TagDto(t.Id, t.NameAr, t.NameEn, t.Color)).ToList(); - return _messages.Ok(MapToDto(ev, topic?.NameAr ?? string.Empty, topic?.NameEn ?? string.Empty, tagDtos), "SUCCESS_OPERATION"); + return _messages.Ok(MapToDto(ev, topic?.NameAr ?? string.Empty, topic?.NameEn ?? string.Empty, tagDtos), MessageKeys.General.SUCCESS_OPERATION); } internal static EventDto MapToDto(Event e, string topicNameAr = "", string topicNameEn = "", System.Collections.Generic.IReadOnlyList? tags = null) => new( diff --git a/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQueryHandler.cs index 0c0098b8..d7e47cba 100644 --- a/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -33,7 +33,7 @@ public async Task> Handle(GetNewsByIdQuery request, Cancellati var tagDtos = news.Tags.Select(t => new TagDto(t.Id, t.NameAr, t.NameEn, t.Color)).ToList(); - return _messages.Ok(MapToDto(news, topic?.NameAr ?? string.Empty, topic?.NameEn ?? string.Empty, tagDtos), "SUCCESS_OPERATION"); + return _messages.Ok(MapToDto(news, topic?.NameAr ?? string.Empty, topic?.NameEn ?? string.Empty, tagDtos), MessageKeys.General.SUCCESS_OPERATION); } internal static NewsDto MapToDto(News n, string topicNameAr = "", string topicNameEn = "", System.Collections.Generic.IReadOnlyList? tags = null) => new( diff --git a/backend/src/CCE.Application/Content/Queries/GetPageById/GetPageByIdQuery.cs b/backend/src/CCE.Application/Content/Queries/GetPageById/GetPageByIdQuery.cs index 4d56e2f1..10e522cf 100644 --- a/backend/src/CCE.Application/Content/Queries/GetPageById/GetPageByIdQuery.cs +++ b/backend/src/CCE.Application/Content/Queries/GetPageById/GetPageByIdQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; namespace CCE.Application.Content.Queries.GetPageById; -public sealed record GetPageByIdQuery(System.Guid Id) : IRequest; +public sealed record GetPageByIdQuery(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Content/Queries/GetPageById/GetPageByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetPageById/GetPageByIdQueryHandler.cs index 39d429a0..b622d627 100644 --- a/backend/src/CCE.Application/Content/Queries/GetPageById/GetPageByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetPageById/GetPageByIdQueryHandler.cs @@ -1,22 +1,31 @@ +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; namespace CCE.Application.Content.Queries.GetPageById; -public sealed class GetPageByIdQueryHandler : IRequestHandler +public sealed class GetPageByIdQueryHandler : IRequestHandler> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public GetPageByIdQueryHandler(ICceDbContext db) => _db = db; + public GetPageByIdQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task Handle(GetPageByIdQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetPageByIdQuery request, CancellationToken cancellationToken) { var list = await _db.Pages.Where(p => p.Id == request.Id).ToListAsyncEither(cancellationToken).ConfigureAwait(false); var pageEntity = list.SingleOrDefault(); - return pageEntity is null ? null : MapToDto(pageEntity); + return pageEntity is null + ? _msg.NotFound(MessageKeys.Content.PAGE_NOT_FOUND) + : _msg.Ok(MapToDto(pageEntity), MessageKeys.General.SUCCESS_OPERATION); } internal static PageDto MapToDto(Page p) => new( diff --git a/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQueryHandler.cs index 9a94eb41..17503356 100644 --- a/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -31,7 +31,7 @@ public async Task> Handle(GetResourceByIdQuery request, Ca var resource = list.SingleOrDefault(); return resource is null ? _messages.ResourceNotFound() - : _messages.Ok(await MapToDtoAsync(resource, cancellationToken).ConfigureAwait(false), "SUCCESS_OPERATION"); + : _messages.Ok(await MapToDtoAsync(resource, cancellationToken).ConfigureAwait(false), MessageKeys.General.SUCCESS_OPERATION); } private async Task MapToDtoAsync(Resource r, CancellationToken ct) diff --git a/backend/src/CCE.Application/Content/Queries/GetResourceCategoryById/GetResourceCategoryByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetResourceCategoryById/GetResourceCategoryByIdQueryHandler.cs index 51578b5f..088ac1f4 100644 --- a/backend/src/CCE.Application/Content/Queries/GetResourceCategoryById/GetResourceCategoryByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetResourceCategoryById/GetResourceCategoryByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -29,7 +29,7 @@ public async Task> Handle(GetResourceCategoryByIdQ if (category is null) return _messages.CategoryNotFound(); - return _messages.Ok(MapToDto(category), "SUCCESS_OPERATION"); + return _messages.Ok(MapToDto(category), MessageKeys.General.SUCCESS_OPERATION); } internal static ResourceCategoryDto MapToDto(ResourceCategory c) => new( diff --git a/backend/src/CCE.Application/Content/Queries/ListCountryContentRequests/ListCountryContentRequestsQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/ListCountryContentRequests/ListCountryContentRequestsQueryHandler.cs index 2b810b98..a79c2b63 100644 --- a/backend/src/CCE.Application/Content/Queries/ListCountryContentRequests/ListCountryContentRequestsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/ListCountryContentRequests/ListCountryContentRequestsQueryHandler.cs @@ -1,10 +1,10 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.CountryScope; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; -using CCE.Application.Errors; using CCE.Application.Messages; + using MediatR; namespace CCE.Application.Content.Queries.ListCountryContentRequests; @@ -36,7 +36,7 @@ public async Task>> Handle( if (authorizedIds is not null && authorizedIds.Count == 0) return _messages.Ok( new PagedResult([], request.Page, request.PageSize, 0), - ApplicationErrors.General.SUCCESS_OPERATION); + MessageKeys.General.SUCCESS_OPERATION); var query = _db.CountryContentRequests // Scope filter: null = admin bypass, list = state-rep restricted to own countries @@ -63,6 +63,6 @@ public async Task>> Handle( request.Page, request.PageSize, cancellationToken) .ConfigureAwait(false); - return _messages.Ok(page, ApplicationErrors.General.SUCCESS_OPERATION); + return _messages.Ok(page, MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Queries/ListEvents/ListEventsQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/ListEvents/ListEventsQueryHandler.cs index 82da5cd6..dca3f839 100644 --- a/backend/src/CCE.Application/Content/Queries/ListEvents/ListEventsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/ListEvents/ListEventsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -42,7 +42,7 @@ public async Task>> Handle(ListEventsQuery reques var eventIds = result.Items.Select(e => e.Id).ToList(); var tagByEventId = await GetTagDtosByEventIdsAsync(eventIds, cancellationToken).ConfigureAwait(false); - return _messages.Ok(result.Map(e => MapToDto(e, topicById, tagByEventId)), "ITEMS_LISTED"); + return _messages.Ok(result.Map(e => MapToDto(e, topicById, tagByEventId)), MessageKeys.General.ITEMS_LISTED); } private async Task>> GetTagDtosByEventIdsAsync( diff --git a/backend/src/CCE.Application/Content/Queries/ListHomepageSections/ListHomepageSectionsQuery.cs b/backend/src/CCE.Application/Content/Queries/ListHomepageSections/ListHomepageSectionsQuery.cs index 0dd5b8fc..cb17d6a5 100644 --- a/backend/src/CCE.Application/Content/Queries/ListHomepageSections/ListHomepageSectionsQuery.cs +++ b/backend/src/CCE.Application/Content/Queries/ListHomepageSections/ListHomepageSectionsQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Content.Dtos; using MediatR; namespace CCE.Application.Content.Queries.ListHomepageSections; -public sealed record ListHomepageSectionsQuery() : IRequest>; +public sealed record ListHomepageSectionsQuery() : IRequest>>; diff --git a/backend/src/CCE.Application/Content/Queries/ListHomepageSections/ListHomepageSectionsQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/ListHomepageSections/ListHomepageSectionsQueryHandler.cs index fb62b99b..b2d71a93 100644 --- a/backend/src/CCE.Application/Content/Queries/ListHomepageSections/ListHomepageSectionsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/ListHomepageSections/ListHomepageSectionsQueryHandler.cs @@ -1,19 +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.Content; using MediatR; namespace CCE.Application.Content.Queries.ListHomepageSections; public sealed class ListHomepageSectionsQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListHomepageSectionsQueryHandler(ICceDbContext db) => _db = db; + public ListHomepageSectionsQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task> Handle( + public async Task>> Handle( ListHomepageSectionsQuery request, CancellationToken cancellationToken) { @@ -21,7 +28,7 @@ public sealed class ListHomepageSectionsQueryHandler .OrderBy(s => s.OrderIndex) .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); - return list.Select(MapToDto).ToList(); + return _msg.Ok((System.Collections.Generic.IReadOnlyList)list.Select(MapToDto).ToList(), MessageKeys.General.ITEMS_LISTED); } internal static HomepageSectionDto MapToDto(HomepageSection s) => new( diff --git a/backend/src/CCE.Application/Content/Queries/ListNews/ListNewsQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/ListNews/ListNewsQueryHandler.cs index 1d45bfe2..b0b1b432 100644 --- a/backend/src/CCE.Application/Content/Queries/ListNews/ListNewsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/ListNews/ListNewsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -44,7 +44,7 @@ public async Task>> Handle(ListNewsQuery request, var newsIds = result.Items.Select(n => n.Id).ToList(); var tagByNewsId = await GetTagDtosByNewsIdsAsync(newsIds, cancellationToken).ConfigureAwait(false); - return _messages.Ok(result.Map(n => MapToDto(n, topicById, tagByNewsId)), "ITEMS_LISTED"); + return _messages.Ok(result.Map(n => MapToDto(n, topicById, tagByNewsId)), MessageKeys.General.ITEMS_LISTED); } private async Task>> GetTagDtosByNewsIdsAsync( diff --git a/backend/src/CCE.Application/Content/Queries/ListPages/ListPagesQuery.cs b/backend/src/CCE.Application/Content/Queries/ListPages/ListPagesQuery.cs index a94aba85..a611dc01 100644 --- a/backend/src/CCE.Application/Content/Queries/ListPages/ListPagesQuery.cs +++ b/backend/src/CCE.Application/Content/Queries/ListPages/ListPagesQuery.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; using CCE.Domain.Content; @@ -9,4 +10,4 @@ public sealed record ListPagesQuery( int Page = 1, int PageSize = 20, string? Search = null, - PageType? PageType = null) : IRequest>; + PageType? PageType = null) : IRequest>>; diff --git a/backend/src/CCE.Application/Content/Queries/ListPages/ListPagesQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/ListPages/ListPagesQueryHandler.cs index ac354522..4b1d9704 100644 --- a/backend/src/CCE.Application/Content/Queries/ListPages/ListPagesQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/ListPages/ListPagesQueryHandler.cs @@ -1,18 +1,25 @@ +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; namespace CCE.Application.Content.Queries.ListPages; -public sealed class ListPagesQueryHandler : IRequestHandler> +public sealed class ListPagesQueryHandler : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListPagesQueryHandler(ICceDbContext db) => _db = db; + public ListPagesQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task> Handle(ListPagesQuery request, CancellationToken cancellationToken) + public async Task>> Handle(ListPagesQuery request, CancellationToken cancellationToken) { var query = _db.Pages .WhereIf(!string.IsNullOrWhiteSpace(request.Search), @@ -23,7 +30,7 @@ public async Task> Handle(ListPagesQuery request, Cancellat .OrderBy(p => p.Slug); var result = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); - return result.Map(MapToDto); + return _msg.Ok(result.Map(MapToDto), MessageKeys.General.ITEMS_LISTED); } internal static PageDto MapToDto(Page p) => new( diff --git a/backend/src/CCE.Application/Content/Queries/ListResourceCategories/ListResourceCategoriesQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/ListResourceCategories/ListResourceCategoriesQueryHandler.cs index 2b5607e7..c503c187 100644 --- a/backend/src/CCE.Application/Content/Queries/ListResourceCategories/ListResourceCategoriesQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/ListResourceCategories/ListResourceCategoriesQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -30,7 +30,7 @@ public async Task>> Handle( .OrderBy(c => c.OrderIndex); var result = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); - return _messages.Ok(result.Map(MapToDto), "ITEMS_LISTED"); + return _messages.Ok(result.Map(MapToDto), MessageKeys.General.ITEMS_LISTED); } internal static ResourceCategoryDto MapToDto(ResourceCategory c) => new( diff --git a/backend/src/CCE.Application/Content/Queries/ListResources/ListResourcesQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/ListResources/ListResourcesQueryHandler.cs index 296a7027..1a06221f 100644 --- a/backend/src/CCE.Application/Content/Queries/ListResources/ListResourcesQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/ListResources/ListResourcesQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -111,6 +111,6 @@ public async Task>> Handle( }).ToList(); var result = new PagedResult(dtos, paged.Page, paged.PageSize, paged.Total); - return _messages.Ok(result, "ITEMS_LISTED"); + return _messages.Ok(result, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Content/Queries/Tags/GetTagById/GetTagByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/Tags/GetTagById/GetTagByIdQueryHandler.cs index 46b0ca3c..01b13350 100644 --- a/backend/src/CCE.Application/Content/Queries/Tags/GetTagById/GetTagByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/Tags/GetTagById/GetTagByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -24,8 +24,8 @@ public async Task> Handle(GetTagByIdQuery request, Cancellation .ToListAsyncEither(cancellationToken).ConfigureAwait(false); var tag = tags.FirstOrDefault(); if (tag is null) - return _messages.NotFound("TAG_NOT_FOUND"); + return _messages.NotFound(MessageKeys.Content.TAG_NOT_FOUND); - return _messages.Ok(new TagDto(tag.Id, tag.NameAr, tag.NameEn, tag.Color), "SUCCESS_OPERATION"); + return _messages.Ok(new TagDto(tag.Id, tag.NameAr, tag.NameEn, tag.Color), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Content/Queries/Tags/ListTags/ListTagsQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/Tags/ListTags/ListTagsQueryHandler.cs index 74e52bbd..513030c8 100644 --- a/backend/src/CCE.Application/Content/Queries/Tags/ListTags/ListTagsQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/Tags/ListTags/ListTagsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Dtos; @@ -26,6 +26,6 @@ public ListTagsQueryHandler(ICceDbContext db, MessageFactory messages) .ConfigureAwait(false); var dtos = tags.Select(t => new TagDto(t.Id, t.NameAr, t.NameEn, t.Color)).ToList(); - return _messages.Ok(dtos, "ITEMS_LISTED"); + return _messages.Ok(dtos, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Country/Commands/UpdateCountry/UpdateCountryCommand.cs b/backend/src/CCE.Application/Country/Commands/UpdateCountry/UpdateCountryCommand.cs index 0c369a02..2fd374c1 100644 --- a/backend/src/CCE.Application/Country/Commands/UpdateCountry/UpdateCountryCommand.cs +++ b/backend/src/CCE.Application/Country/Commands/UpdateCountry/UpdateCountryCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Country.Dtos; using MediatR; @@ -9,4 +10,4 @@ public sealed record UpdateCountryCommand( string NameEn, string RegionAr, string RegionEn, - bool IsActive) : IRequest; + bool IsActive) : IRequest>; diff --git a/backend/src/CCE.Application/Country/Commands/UpdateCountry/UpdateCountryCommandHandler.cs b/backend/src/CCE.Application/Country/Commands/UpdateCountry/UpdateCountryCommandHandler.cs index 7e368aaa..781a5172 100644 --- a/backend/src/CCE.Application/Country/Commands/UpdateCountry/UpdateCountryCommandHandler.cs +++ b/backend/src/CCE.Application/Country/Commands/UpdateCountry/UpdateCountryCommandHandler.cs @@ -1,24 +1,29 @@ +using CCE.Application.Common; using CCE.Application.Country.Dtos; using CCE.Application.Country.Queries.ListCountries; +using CCE.Application.Messages; + using MediatR; namespace CCE.Application.Country.Commands.UpdateCountry; -public sealed class UpdateCountryCommandHandler : IRequestHandler +public sealed class UpdateCountryCommandHandler : IRequestHandler> { private readonly ICountryAdminService _service; + private readonly MessageFactory _msg; - public UpdateCountryCommandHandler(ICountryAdminService service) + public UpdateCountryCommandHandler(ICountryAdminService service, MessageFactory msg) { _service = service; + _msg = msg; } - public async Task Handle(UpdateCountryCommand request, CancellationToken cancellationToken) + public async Task> Handle(UpdateCountryCommand request, CancellationToken cancellationToken) { var country = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); if (country is null) { - return null; + return _msg.NotFound(MessageKeys.Country.COUNTRY_NOT_FOUND); } country.UpdateNames(request.NameAr, request.NameEn, request.RegionAr, request.RegionEn); @@ -30,6 +35,6 @@ public UpdateCountryCommandHandler(ICountryAdminService service) await _service.UpdateAsync(country, cancellationToken).ConfigureAwait(false); - return ListCountriesQueryHandler.MapToDto(country); + return _msg.Ok(ListCountriesQueryHandler.MapToDto(country), MessageKeys.General.SUCCESS_UPDATED); } } diff --git a/backend/src/CCE.Application/Country/Commands/UpsertCountryProfile/UpsertCountryProfileCommandHandler.cs b/backend/src/CCE.Application/Country/Commands/UpsertCountryProfile/UpsertCountryProfileCommandHandler.cs index f578b077..1805fe72 100644 --- a/backend/src/CCE.Application/Country/Commands/UpsertCountryProfile/UpsertCountryProfileCommandHandler.cs +++ b/backend/src/CCE.Application/Country/Commands/UpsertCountryProfile/UpsertCountryProfileCommandHandler.cs @@ -1,10 +1,10 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.CountryScope; using CCE.Application.Common.Interfaces; using CCE.Application.Country.Dtos; using CCE.Application.Country.Queries.GetCountryProfile; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Country; using MediatR; @@ -75,6 +75,6 @@ public async Task> Handle(UpsertCountryProfileComman result = existing; } - return _messages.Ok(GetCountryProfileQueryHandler.MapToDto(result), ApplicationErrors.Country.COUNTRY_PROFILE_UPDATED); + return _messages.Ok(GetCountryProfileQueryHandler.MapToDto(result), MessageKeys.Country.COUNTRY_PROFILE_UPDATED); } } diff --git a/backend/src/CCE.Application/Country/Queries/GetCountryById/GetCountryByIdQuery.cs b/backend/src/CCE.Application/Country/Queries/GetCountryById/GetCountryByIdQuery.cs index a81a29b3..41cc1151 100644 --- a/backend/src/CCE.Application/Country/Queries/GetCountryById/GetCountryByIdQuery.cs +++ b/backend/src/CCE.Application/Country/Queries/GetCountryById/GetCountryByIdQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Country.Dtos; using MediatR; namespace CCE.Application.Country.Queries.GetCountryById; -public sealed record GetCountryByIdQuery(System.Guid Id) : IRequest; +public sealed record GetCountryByIdQuery(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/Country/Queries/GetCountryById/GetCountryByIdQueryHandler.cs b/backend/src/CCE.Application/Country/Queries/GetCountryById/GetCountryByIdQueryHandler.cs index 62eabc31..1d2a2441 100644 --- a/backend/src/CCE.Application/Country/Queries/GetCountryById/GetCountryByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Country/Queries/GetCountryById/GetCountryByIdQueryHandler.cs @@ -1,27 +1,34 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Country.Dtos; using CCE.Application.Country.Queries.ListCountries; +using CCE.Application.Messages; + using MediatR; namespace CCE.Application.Country.Queries.GetCountryById; -public sealed class GetCountryByIdQueryHandler : IRequestHandler +public sealed class GetCountryByIdQueryHandler : IRequestHandler> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public GetCountryByIdQueryHandler(ICceDbContext db) + public GetCountryByIdQueryHandler(ICceDbContext db, MessageFactory msg) { _db = db; + _msg = msg; } - public async Task Handle(GetCountryByIdQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetCountryByIdQuery request, CancellationToken cancellationToken) { var list = await _db.Countries .Where(c => c.Id == request.Id) .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); var country = list.SingleOrDefault(); - return country is null ? null : ListCountriesQueryHandler.MapToDto(country); + return country is null + ? _msg.NotFound(MessageKeys.Country.COUNTRY_NOT_FOUND) + : _msg.Ok(ListCountriesQueryHandler.MapToDto(country), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Country/Queries/GetCountryProfile/GetCountryProfileQuery.cs b/backend/src/CCE.Application/Country/Queries/GetCountryProfile/GetCountryProfileQuery.cs index b2b42015..868955b5 100644 --- a/backend/src/CCE.Application/Country/Queries/GetCountryProfile/GetCountryProfileQuery.cs +++ b/backend/src/CCE.Application/Country/Queries/GetCountryProfile/GetCountryProfileQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.Country.Dtos; using MediatR; namespace CCE.Application.Country.Queries.GetCountryProfile; -public sealed record GetCountryProfileQuery(System.Guid CountryId) : IRequest; +public sealed record GetCountryProfileQuery(System.Guid CountryId) : IRequest>; diff --git a/backend/src/CCE.Application/Country/Queries/GetCountryProfile/GetCountryProfileQueryHandler.cs b/backend/src/CCE.Application/Country/Queries/GetCountryProfile/GetCountryProfileQueryHandler.cs index b5e009f9..ed6d4cee 100644 --- a/backend/src/CCE.Application/Country/Queries/GetCountryProfile/GetCountryProfileQueryHandler.cs +++ b/backend/src/CCE.Application/Country/Queries/GetCountryProfile/GetCountryProfileQueryHandler.cs @@ -1,27 +1,32 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Country.Dtos; +using CCE.Application.Messages; + using CCE.Domain.Country; using MediatR; namespace CCE.Application.Country.Queries.GetCountryProfile; -public sealed class GetCountryProfileQueryHandler : IRequestHandler +public sealed class GetCountryProfileQueryHandler : IRequestHandler> { private readonly ICountryProfileService _service; private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public GetCountryProfileQueryHandler(ICountryProfileService service, ICceDbContext db) + public GetCountryProfileQueryHandler(ICountryProfileService service, ICceDbContext db, MessageFactory msg) { _service = service; _db = db; + _msg = msg; } - public async Task Handle(GetCountryProfileQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetCountryProfileQuery request, CancellationToken cancellationToken) { var profile = await _service.FindByCountryIdAsync(request.CountryId, cancellationToken).ConfigureAwait(false); if (profile is null) - return null; + return _msg.NotFound(MessageKeys.Country.COUNTRY_PROFILE_NOT_FOUND); CountryKapsarcSnapshot? snapshot = null; var countries = await _db.Countries @@ -36,7 +41,7 @@ public GetCountryProfileQueryHandler(ICountryProfileService service, ICceDbConte snapshot = snapshots.FirstOrDefault(); } - return MapToDto(profile, snapshot); + return _msg.Ok(MapToDto(profile, snapshot), MessageKeys.General.SUCCESS_OPERATION); } internal static CountryProfileDto MapToDto(CountryProfile profile, CountryKapsarcSnapshot? snapshot = null) => diff --git a/backend/src/CCE.Application/Country/Queries/GetMyCountryProfile/GetMyCountryProfileQueryHandler.cs b/backend/src/CCE.Application/Country/Queries/GetMyCountryProfile/GetMyCountryProfileQueryHandler.cs index 891ad02d..35ca7f91 100644 --- a/backend/src/CCE.Application/Country/Queries/GetMyCountryProfile/GetMyCountryProfileQueryHandler.cs +++ b/backend/src/CCE.Application/Country/Queries/GetMyCountryProfile/GetMyCountryProfileQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.CountryScope; using CCE.Application.Common.Pagination; using CCE.Application.Country.Dtos; @@ -62,6 +62,6 @@ public async Task> Handle( return _messages.Ok( GetCountryProfileQueryHandler.MapToDto(profile, snapshot), - CCE.Application.Errors.ApplicationErrors.General.SUCCESS_OPERATION); + MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Country/Queries/ListCountries/ListCountriesQueryHandler.cs b/backend/src/CCE.Application/Country/Queries/ListCountries/ListCountriesQueryHandler.cs index 38af4a0e..f50ba909 100644 --- a/backend/src/CCE.Application/Country/Queries/ListCountries/ListCountriesQueryHandler.cs +++ b/backend/src/CCE.Application/Country/Queries/ListCountries/ListCountriesQueryHandler.cs @@ -1,9 +1,9 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Country.Dtos; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Country; using MediatR; @@ -71,7 +71,7 @@ from s in snapshotGroup.DefaultIfEmpty() request.Page, request.PageSize, cancellationToken) .ConfigureAwait(false); - return _messages.Ok(ccePage, ApplicationErrors.General.SUCCESS_OPERATION); + return _messages.Ok(ccePage, MessageKeys.General.SUCCESS_OPERATION); } // Simple flat list — no KAPSARC join needed. @@ -90,7 +90,7 @@ from s in snapshotGroup.DefaultIfEmpty() request.Page, request.PageSize, cancellationToken) .ConfigureAwait(false); - return _messages.Ok(page, ApplicationErrors.General.SUCCESS_OPERATION); + return _messages.Ok(page, MessageKeys.General.SUCCESS_OPERATION); } internal static CountryDto MapToDto(CCE.Domain.Country.Country c) => new( diff --git a/backend/src/CCE.Application/CountryPublic/Queries/GetPublicCountryProfile/GetPublicCountryProfileQueryHandler.cs b/backend/src/CCE.Application/CountryPublic/Queries/GetPublicCountryProfile/GetPublicCountryProfileQueryHandler.cs index e2ed59d2..745f316c 100644 --- a/backend/src/CCE.Application/CountryPublic/Queries/GetPublicCountryProfile/GetPublicCountryProfileQueryHandler.cs +++ b/backend/src/CCE.Application/CountryPublic/Queries/GetPublicCountryProfile/GetPublicCountryProfileQueryHandler.cs @@ -1,9 +1,9 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.CountryPublic.Dtos; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Content; using CCE.Domain.Country; using MediatR; @@ -63,7 +63,7 @@ public async Task> Handle( ndcDocument = new NdcDocumentDto(asset.Id, asset.OriginalFileName); } - return _messages.Ok(MapToDto(country, profile, snapshot, ndcDocument), ApplicationErrors.General.SUCCESS_OPERATION); + return _messages.Ok(MapToDto(country, profile, snapshot, ndcDocument), MessageKeys.General.SUCCESS_OPERATION); } internal static PublicCountryProfileDto MapToDto( diff --git a/backend/src/CCE.Application/CountryPublic/Queries/ListPublicCountries/ListPublicCountriesQueryHandler.cs b/backend/src/CCE.Application/CountryPublic/Queries/ListPublicCountries/ListPublicCountriesQueryHandler.cs index 019f8284..0f312510 100644 --- a/backend/src/CCE.Application/CountryPublic/Queries/ListPublicCountries/ListPublicCountriesQueryHandler.cs +++ b/backend/src/CCE.Application/CountryPublic/Queries/ListPublicCountries/ListPublicCountriesQueryHandler.cs @@ -1,9 +1,9 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.CountryPublic.Dtos; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using CCE.Domain.Country; using MediatR; @@ -70,7 +70,7 @@ from s in snapshotGroup.DefaultIfEmpty() request.Page, request.PageSize, cancellationToken) .ConfigureAwait(false); - return _messages.Ok(ccePage, ApplicationErrors.General.SUCCESS_OPERATION); + return _messages.Ok(ccePage, MessageKeys.General.SUCCESS_OPERATION); } // Simple flat list — no KAPSARC join needed. @@ -88,6 +88,6 @@ from s in snapshotGroup.DefaultIfEmpty() request.Page, request.PageSize, cancellationToken) .ConfigureAwait(false); - return _messages.Ok(page, ApplicationErrors.General.SUCCESS_OPERATION); + return _messages.Ok(page, MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/DependencyInjection.cs b/backend/src/CCE.Application/DependencyInjection.cs index f311d055..910b853e 100644 --- a/backend/src/CCE.Application/DependencyInjection.cs +++ b/backend/src/CCE.Application/DependencyInjection.cs @@ -16,10 +16,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services services.AddMediatR(cfg => { cfg.RegisterServicesFromAssembly(assembly); - cfg.AddOpenBehavior(typeof(LoggingBehavior<,>)); cfg.AddOpenBehavior(typeof(ResponseValidationBehavior<,>)); - cfg.AddOpenBehavior(typeof(ResultValidationBehavior<,>)); - cfg.AddOpenBehavior(typeof(ValidationBehavior<,>)); // Last: runs after the handler commits; evicts cache regions for ICacheInvalidatingRequest. cfg.AddOpenBehavior(typeof(CacheInvalidationBehavior<,>)); }); diff --git a/backend/src/CCE.Application/Evaluation/Commands/SubmitEvaluation/SubmitEvaluationCommandHandler.cs b/backend/src/CCE.Application/Evaluation/Commands/SubmitEvaluation/SubmitEvaluationCommandHandler.cs index 9e1bc05f..7ec3cbe6 100644 --- a/backend/src/CCE.Application/Evaluation/Commands/SubmitEvaluation/SubmitEvaluationCommandHandler.cs +++ b/backend/src/CCE.Application/Evaluation/Commands/SubmitEvaluation/SubmitEvaluationCommandHandler.cs @@ -1,7 +1,7 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; -using CCE.Application.Errors; using CCE.Application.Messages; + using CCE.Domain.Common; using DomainEvaluation = CCE.Domain.Evaluation.ServiceEvaluation; using MediatR; @@ -48,6 +48,6 @@ public async Task> Handle( await _repository.AddAsync(evaluation, cancellationToken).ConfigureAwait(false); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messageFactory.Ok(ApplicationErrors.Evaluation.EVALUATION_SUBMITTED); + return _messageFactory.Ok(MessageKeys.Evaluation.EVALUATION_SUBMITTED); } } diff --git a/backend/src/CCE.Application/Evaluation/Queries/GetAllEvaluations/GetAllEvaluationsQueryHandler.cs b/backend/src/CCE.Application/Evaluation/Queries/GetAllEvaluations/GetAllEvaluationsQueryHandler.cs index f8b93aea..e0fbeeda 100644 --- a/backend/src/CCE.Application/Evaluation/Queries/GetAllEvaluations/GetAllEvaluationsQueryHandler.cs +++ b/backend/src/CCE.Application/Evaluation/Queries/GetAllEvaluations/GetAllEvaluationsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Evaluation.DTOs; using CCE.Application.Messages; @@ -39,6 +39,6 @@ public async Task>> Handle( e.UserId, e.CreatedOn, e.CreatedById)); - return _msg.Ok(result, "ITEMS_LISTED"); + return _msg.Ok(result, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Evaluation/Queries/GetEvaluationById/GetEvaluationByIdQueryHandler.cs b/backend/src/CCE.Application/Evaluation/Queries/GetEvaluationById/GetEvaluationByIdQueryHandler.cs index f8e8f18d..30d7343e 100644 --- a/backend/src/CCE.Application/Evaluation/Queries/GetEvaluationById/GetEvaluationByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Evaluation/Queries/GetEvaluationById/GetEvaluationByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Evaluation.DTOs; using CCE.Application.Messages; @@ -40,6 +40,6 @@ public async Task> Handle( evaluation.CreatedOn, evaluation.CreatedById); - return _msg.Ok(dto, "ITEMS_LISTED"); + return _msg.Ok(dto, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Identity/Auth/AdLogin/AdLoginCommandHandler.cs b/backend/src/CCE.Application/Identity/Auth/AdLogin/AdLoginCommandHandler.cs index 91806997..a9ed49b1 100644 --- a/backend/src/CCE.Application/Identity/Auth/AdLogin/AdLoginCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Auth/AdLogin/AdLoginCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Identity.Auth.Common; using CCE.Application.Messages; using MediatR; @@ -29,7 +29,7 @@ public async Task> Handle(AdLoginCommand request, Cancell return result.Failure switch { LoginFailureReason.Deactivated => _msg.AccountDeactivated(), - LoginFailureReason.None => _msg.Ok(result.Token!, "AD_LOGIN_SUCCESS"), + LoginFailureReason.None => _msg.Ok(result.Token!, MessageKeys.Identity.AD_LOGIN_SUCCESS), _ => _msg.InvalidCredentials(), }; } diff --git a/backend/src/CCE.Application/Identity/Auth/AdLogin/AdLoginCommandValidator.cs b/backend/src/CCE.Application/Identity/Auth/AdLogin/AdLoginCommandValidator.cs index d14074ad..d79e5db6 100644 --- a/backend/src/CCE.Application/Identity/Auth/AdLogin/AdLoginCommandValidator.cs +++ b/backend/src/CCE.Application/Identity/Auth/AdLogin/AdLoginCommandValidator.cs @@ -1,3 +1,4 @@ +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.Identity.Auth.AdLogin; @@ -6,12 +7,7 @@ public sealed class AdLoginCommandValidator : AbstractValidator { public AdLoginCommandValidator() { - RuleFor(x => x.Username) - .NotEmpty() - .WithMessage("Username is required."); - - RuleFor(x => x.Password) - .NotEmpty() - .WithMessage("Password is required."); + RuleFor(x => x.Username).NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); + RuleFor(x => x.Password).NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); } } diff --git a/backend/src/CCE.Application/Identity/Auth/Common/LocalAuthOptions.cs b/backend/src/CCE.Application/Identity/Auth/Common/LocalAuthOptions.cs index 863e7764..72350c4e 100644 --- a/backend/src/CCE.Application/Identity/Auth/Common/LocalAuthOptions.cs +++ b/backend/src/CCE.Application/Identity/Auth/Common/LocalAuthOptions.cs @@ -11,6 +11,10 @@ public sealed record LocalAuthOptions public int PasswordResetTokenHours { get; init; } = 2; public bool RequireConfirmedEmail { get; init; } - public LocalAuthJwtProfile GetProfile(LocalAuthApi api) - => api == LocalAuthApi.Internal ? Internal : External; + public LocalAuthJwtProfile GetProfile(LocalAuthApi api) => api switch + { + LocalAuthApi.External => External, + LocalAuthApi.Internal => Internal, + _ => throw new InvalidOperationException($"No JWT profile configured for LocalAuthApi.{api}.") + }; } diff --git a/backend/src/CCE.Application/Identity/Auth/ForgotPassword/ForgotPasswordCommandHandler.cs b/backend/src/CCE.Application/Identity/Auth/ForgotPassword/ForgotPasswordCommandHandler.cs index aac5f29f..42fa7ef5 100644 --- a/backend/src/CCE.Application/Identity/Auth/ForgotPassword/ForgotPasswordCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Auth/ForgotPassword/ForgotPasswordCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Identity.Auth.Common; using CCE.Application.Messages; using MediatR; @@ -20,6 +20,6 @@ public ForgotPasswordCommandHandler(IAuthService auth, MessageFactory msg) public async Task> Handle(ForgotPasswordCommand request, CancellationToken ct) { await _auth.ForgotPasswordAsync(request.EmailAddress, ct).ConfigureAwait(false); - return _msg.Ok(new AuthMessageDto("PASSWORD_RESET"), "PASSWORD_RESET"); + return _msg.Ok(new AuthMessageDto(MessageKeys.Identity.PASSWORD_RESET), MessageKeys.Identity.PASSWORD_RESET); } } diff --git a/backend/src/CCE.Application/Identity/Auth/ForgotPassword/ForgotPasswordCommandValidator.cs b/backend/src/CCE.Application/Identity/Auth/ForgotPassword/ForgotPasswordCommandValidator.cs index 9fe269d2..1f74d1a5 100644 --- a/backend/src/CCE.Application/Identity/Auth/ForgotPassword/ForgotPasswordCommandValidator.cs +++ b/backend/src/CCE.Application/Identity/Auth/ForgotPassword/ForgotPasswordCommandValidator.cs @@ -1,3 +1,4 @@ +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.Identity.Auth.ForgotPassword; @@ -5,5 +6,10 @@ namespace CCE.Application.Identity.Auth.ForgotPassword; public sealed class ForgotPasswordCommandValidator : AbstractValidator { public ForgotPasswordCommandValidator() - => RuleFor(x => x.EmailAddress).NotEmpty().EmailAddress().MaximumLength(100); + { + RuleFor(x => x.EmailAddress) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .EmailAddress().WithErrorCode(MessageKeys.Validation.INVALID_EMAIL) + .MaximumLength(100).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + } } diff --git a/backend/src/CCE.Application/Identity/Auth/Login/LoginCommandHandler.cs b/backend/src/CCE.Application/Identity/Auth/Login/LoginCommandHandler.cs index b383c91e..9ebc9296 100644 --- a/backend/src/CCE.Application/Identity/Auth/Login/LoginCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Auth/Login/LoginCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Identity.Auth.Common; using CCE.Application.Messages; using MediatR; @@ -25,7 +25,7 @@ public async Task> Handle(LoginCommand request, Cancellat { LoginFailureReason.Deactivated => _msg.AccountDeactivated(), LoginFailureReason.ContactNotVerified => _msg.ContactNotVerified(), - LoginFailureReason.None => _msg.Ok(result.Token!, "LOGIN_SUCCESS"), + LoginFailureReason.None => _msg.Ok(result.Token!, MessageKeys.Identity.LOGIN_SUCCESS), _ => _msg.InvalidCredentials(), }; } diff --git a/backend/src/CCE.Application/Identity/Auth/Login/LoginCommandValidator.cs b/backend/src/CCE.Application/Identity/Auth/Login/LoginCommandValidator.cs index 945af1c1..29516bae 100644 --- a/backend/src/CCE.Application/Identity/Auth/Login/LoginCommandValidator.cs +++ b/backend/src/CCE.Application/Identity/Auth/Login/LoginCommandValidator.cs @@ -1,3 +1,4 @@ +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.Identity.Auth.Login; @@ -6,7 +7,11 @@ public sealed class LoginCommandValidator : AbstractValidator { public LoginCommandValidator() { - RuleFor(x => x.EmailAddress).NotEmpty().EmailAddress().MaximumLength(100); - RuleFor(x => x.Password).NotEmpty(); + RuleFor(x => x.EmailAddress) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .EmailAddress().WithErrorCode(MessageKeys.Validation.INVALID_EMAIL) + .MaximumLength(100).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + RuleFor(x => x.Password) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); } } diff --git a/backend/src/CCE.Application/Identity/Auth/Logout/LogoutCommandHandler.cs b/backend/src/CCE.Application/Identity/Auth/Logout/LogoutCommandHandler.cs index daa72103..9d3bcd6b 100644 --- a/backend/src/CCE.Application/Identity/Auth/Logout/LogoutCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Auth/Logout/LogoutCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Identity.Auth.Common; using CCE.Application.Messages; using MediatR; @@ -20,6 +20,6 @@ public LogoutCommandHandler(IAuthService auth, MessageFactory msg) public async Task> Handle(LogoutCommand request, CancellationToken ct) { await _auth.LogoutAsync(request.RefreshToken, request.IpAddress, ct).ConfigureAwait(false); - return _msg.Ok(new AuthMessageDto("LOGOUT_SUCCESS"), "LOGOUT_SUCCESS"); + return _msg.Ok(new AuthMessageDto(MessageKeys.Identity.LOGOUT_SUCCESS), MessageKeys.Identity.LOGOUT_SUCCESS); } } diff --git a/backend/src/CCE.Application/Identity/Auth/Logout/LogoutCommandValidator.cs b/backend/src/CCE.Application/Identity/Auth/Logout/LogoutCommandValidator.cs index 9832d200..ade189c6 100644 --- a/backend/src/CCE.Application/Identity/Auth/Logout/LogoutCommandValidator.cs +++ b/backend/src/CCE.Application/Identity/Auth/Logout/LogoutCommandValidator.cs @@ -1,8 +1,10 @@ +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.Identity.Auth.Logout; public sealed class LogoutCommandValidator : AbstractValidator { - public LogoutCommandValidator() => RuleFor(x => x.RefreshToken).NotEmpty(); + public LogoutCommandValidator() + => RuleFor(x => x.RefreshToken).NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); } diff --git a/backend/src/CCE.Application/Identity/Auth/RefreshToken/RefreshTokenCommandHandler.cs b/backend/src/CCE.Application/Identity/Auth/RefreshToken/RefreshTokenCommandHandler.cs index fbcde08e..5cbdfb4d 100644 --- a/backend/src/CCE.Application/Identity/Auth/RefreshToken/RefreshTokenCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Auth/RefreshToken/RefreshTokenCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Identity.Auth.Common; using CCE.Application.Messages; using MediatR; @@ -21,7 +21,7 @@ public async Task> Handle(RefreshTokenCommand request, Ca { var dto = await _auth.RefreshTokenAsync(request.RefreshToken, request.Api, request.IpAddress, request.UserAgent, ct).ConfigureAwait(false); - if (dto is null) return _msg.Unauthorized("INVALID_REFRESH_TOKEN"); - return _msg.Ok(dto, "TOKEN_REFRESHED"); + if (dto is null) return _msg.Unauthorized(MessageKeys.Identity.INVALID_REFRESH_TOKEN); + return _msg.Ok(dto, MessageKeys.Identity.TOKEN_REFRESHED); } } diff --git a/backend/src/CCE.Application/Identity/Auth/RefreshToken/RefreshTokenCommandValidator.cs b/backend/src/CCE.Application/Identity/Auth/RefreshToken/RefreshTokenCommandValidator.cs index 4fbd580c..75940ba4 100644 --- a/backend/src/CCE.Application/Identity/Auth/RefreshToken/RefreshTokenCommandValidator.cs +++ b/backend/src/CCE.Application/Identity/Auth/RefreshToken/RefreshTokenCommandValidator.cs @@ -1,8 +1,10 @@ +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.Identity.Auth.RefreshToken; public sealed class RefreshTokenCommandValidator : AbstractValidator { - public RefreshTokenCommandValidator() => RuleFor(x => x.RefreshToken).NotEmpty(); + public RefreshTokenCommandValidator() + => RuleFor(x => x.RefreshToken).NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); } diff --git a/backend/src/CCE.Application/Identity/Auth/Register/RegisterUserCommandHandler.cs b/backend/src/CCE.Application/Identity/Auth/Register/RegisterUserCommandHandler.cs index 24cba26e..25325042 100644 --- a/backend/src/CCE.Application/Identity/Auth/Register/RegisterUserCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Auth/Register/RegisterUserCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Identity.Auth.Common; using CCE.Application.Messages; using MediatR; @@ -24,7 +24,7 @@ public async Task> Handle(RegisterUserCommand request, Can request.OrganizationName, request.PhoneNumber, request.CountryId, ct).ConfigureAwait(false); if (result.EmailTaken) return _msg.EmailExists(); - if (result.User is null) return _msg.BusinessRule("REGISTRATION_FAILED"); + if (result.User is null) return _msg.BusinessRule(MessageKeys.Identity.REGISTRATION_FAILED); return _msg.Ok(new AuthUserDto( result.User.Id, @@ -33,6 +33,6 @@ public async Task> Handle(RegisterUserCommand request, Can result.User.LastName, result.User.AvatarUrl, ["cce-user"], - []), "REGISTER_SUCCESS"); + []), MessageKeys.Identity.REGISTER_SUCCESS); } } diff --git a/backend/src/CCE.Application/Identity/Auth/Register/RegisterUserCommandValidator.cs b/backend/src/CCE.Application/Identity/Auth/Register/RegisterUserCommandValidator.cs index 05c40a72..a2349291 100644 --- a/backend/src/CCE.Application/Identity/Auth/Register/RegisterUserCommandValidator.cs +++ b/backend/src/CCE.Application/Identity/Auth/Register/RegisterUserCommandValidator.cs @@ -1,3 +1,4 @@ +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.Identity.Auth.Register; @@ -6,14 +7,43 @@ public sealed class RegisterUserCommandValidator : AbstractValidator x.FirstName).NotEmpty().MaximumLength(50).Must(BeLettersOnly); - RuleFor(x => x.LastName).NotEmpty().MaximumLength(50).Must(BeLettersOnly); - RuleFor(x => x.EmailAddress).NotEmpty().EmailAddress().MaximumLength(100); - RuleFor(x => x.JobTitle).NotEmpty().MaximumLength(50); - RuleFor(x => x.OrganizationName).NotEmpty().MaximumLength(100); - RuleFor(x => x.PhoneNumber).NotEmpty().MaximumLength(15).Must(BenumbersOnly); - RuleFor(x => x.Password).Must(MatchStoryPasswordPolicy).WithMessage("PASSWORD_POLICY"); - RuleFor(x => x.ConfirmPassword).Equal(x => x.Password); + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.FirstName) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(50).WithErrorCode(MessageKeys.Validation.MAX_LENGTH) + .Must(BeLettersOnly).WithErrorCode(MessageKeys.Validation.INVALID_FORMAT); + + RuleFor(x => x.LastName) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(50).WithErrorCode(MessageKeys.Validation.MAX_LENGTH) + .Must(BeLettersOnly).WithErrorCode(MessageKeys.Validation.INVALID_FORMAT); + + RuleFor(x => x.EmailAddress) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .EmailAddress().WithErrorCode(MessageKeys.Validation.INVALID_EMAIL) + .MaximumLength(100).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + + RuleFor(x => x.JobTitle) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(50).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + + RuleFor(x => x.OrganizationName) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(100).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + + RuleFor(x => x.PhoneNumber) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(15).WithErrorCode(MessageKeys.Validation.MAX_LENGTH) + .Must(BenumbersOnly).WithErrorCode(MessageKeys.Validation.INVALID_PHONE); + + RuleFor(x => x.Password) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .Must(MatchStrongPasswordPolicy).WithErrorCode(MessageKeys.Validation.PASSWORD_POLICY); + + RuleFor(x => x.ConfirmPassword) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .Equal(x => x.Password).WithErrorCode(MessageKeys.Validation.PASSWORDS_MUST_MATCH); } private static bool BeLettersOnly(string value) @@ -21,7 +51,7 @@ private static bool BeLettersOnly(string value) private static bool BenumbersOnly(string value) => !string.IsNullOrWhiteSpace(value) && value.All(char.IsNumber); - internal static bool MatchStoryPasswordPolicy(string value) + internal static bool MatchStrongPasswordPolicy(string value) => !string.IsNullOrWhiteSpace(value) && value.Length is >= 12 and <= 20 && value.Any(char.IsUpper) diff --git a/backend/src/CCE.Application/Identity/Auth/ResetPassword/ResetPasswordCommandHandler.cs b/backend/src/CCE.Application/Identity/Auth/ResetPassword/ResetPasswordCommandHandler.cs index 8219f4f0..f5b99786 100644 --- a/backend/src/CCE.Application/Identity/Auth/ResetPassword/ResetPasswordCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Auth/ResetPassword/ResetPasswordCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Identity.Auth.Common; using CCE.Application.Messages; using MediatR; @@ -26,12 +26,12 @@ public async Task> Handle(ResetPasswordCommand request, { return errorKey switch { - "USER_NOT_FOUND" => _msg.UserNotFound(), - "INVALID_RESET_TOKEN" => _msg.Unauthorized("INVALID_RESET_TOKEN"), + MessageKeys.Identity.USER_NOT_FOUND => _msg.UserNotFound(), + MessageKeys.Identity.INVALID_RESET_TOKEN => _msg.Unauthorized(MessageKeys.Identity.INVALID_RESET_TOKEN), _ => _msg.BusinessRule(errorKey), }; } - return _msg.Ok(new AuthMessageDto("PASSWORD_RESET"), "PASSWORD_RESET"); + return _msg.Ok(new AuthMessageDto(MessageKeys.Identity.PASSWORD_RESET), MessageKeys.Identity.PASSWORD_RESET); } } diff --git a/backend/src/CCE.Application/Identity/Auth/ResetPassword/ResetPasswordCommandValidator.cs b/backend/src/CCE.Application/Identity/Auth/ResetPassword/ResetPasswordCommandValidator.cs index bda031f1..f119ec06 100644 --- a/backend/src/CCE.Application/Identity/Auth/ResetPassword/ResetPasswordCommandValidator.cs +++ b/backend/src/CCE.Application/Identity/Auth/ResetPassword/ResetPasswordCommandValidator.cs @@ -1,4 +1,5 @@ using CCE.Application.Identity.Auth.Register; +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.Identity.Auth.ResetPassword; @@ -7,9 +8,22 @@ public sealed class ResetPasswordCommandValidator : AbstractValidator x.EmailAddress).NotEmpty().EmailAddress().MaximumLength(100); - RuleFor(x => x.Token).NotEmpty(); - RuleFor(x => x.NewPassword).Must(RegisterUserCommandValidator.MatchStoryPasswordPolicy).WithMessage("PASSWORD_POLICY"); - RuleFor(x => x.ConfirmPassword).Equal(x => x.NewPassword); + RuleFor(x => x.EmailAddress) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .EmailAddress().WithErrorCode(MessageKeys.Validation.INVALID_EMAIL) + .MaximumLength(100).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + + RuleFor(x => x.Token) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); + + RuleFor(x => x.NewPassword) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .Must(RegisterUserCommandValidator.MatchStrongPasswordPolicy) + .WithErrorCode(MessageKeys.Validation.PASSWORD_POLICY); + + RuleFor(x => x.ConfirmPassword) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .Equal(x => x.NewPassword) + .WithErrorCode(MessageKeys.Validation.PASSWORDS_MUST_MATCH); } } diff --git a/backend/src/CCE.Application/Identity/Commands/ApproveExpertRequest/ApproveExpertRequestCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/ApproveExpertRequest/ApproveExpertRequestCommandHandler.cs index 62700994..6cb42650 100644 --- a/backend/src/CCE.Application/Identity/Commands/ApproveExpertRequest/ApproveExpertRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/ApproveExpertRequest/ApproveExpertRequestCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Dtos; @@ -64,6 +64,6 @@ public async Task> Handle( profile.AcademicTitleAr, profile.AcademicTitleEn, profile.ApprovedOn, - profile.ApprovedById), "EXPERT_REQUEST_APPROVED"); + profile.ApprovedById), MessageKeys.Identity.EXPERT_REQUEST_APPROVED); } } diff --git a/backend/src/CCE.Application/Identity/Commands/AssignUserRoles/AssignUserRolesCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/AssignUserRoles/AssignUserRolesCommandHandler.cs index 09e8a16d..8084832b 100644 --- a/backend/src/CCE.Application/Identity/Commands/AssignUserRoles/AssignUserRolesCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/AssignUserRoles/AssignUserRolesCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Identity.Dtos; using CCE.Application.Identity.Queries.GetUserById; using CCE.Application.Messages; @@ -36,6 +36,6 @@ public async Task> Handle(AssignUserRolesCommand request return result; } - return _msg.Ok(result.Data!, "ROLES_ASSIGNED"); + return _msg.Ok(result.Data!, MessageKeys.Identity.ROLES_ASSIGNED); } } diff --git a/backend/src/CCE.Application/Identity/Commands/ChangeUserStatus/ChangeUserStatusCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/ChangeUserStatus/ChangeUserStatusCommandHandler.cs index 2811e955..96135acc 100644 --- a/backend/src/CCE.Application/Identity/Commands/ChangeUserStatus/ChangeUserStatusCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/ChangeUserStatus/ChangeUserStatusCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Identity.Dtos; using CCE.Application.Identity.Public; @@ -48,6 +48,6 @@ public async Task> Handle(ChangeUserStatusCommand reques return result; } - return _msg.Ok(result.Data!, "USER_STATUS_CHANGED"); + return _msg.Ok(result.Data!, MessageKeys.Identity.USER_STATUS_CHANGED); } } diff --git a/backend/src/CCE.Application/Identity/Commands/CreateStateRepAssignment/CreateStateRepAssignmentCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/CreateStateRepAssignment/CreateStateRepAssignmentCommandHandler.cs index e2403e06..941a2efc 100644 --- a/backend/src/CCE.Application/Identity/Commands/CreateStateRepAssignment/CreateStateRepAssignmentCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/CreateStateRepAssignment/CreateStateRepAssignmentCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Dtos; @@ -49,7 +49,7 @@ public async Task> Handle( var countryExists = await ExistsAsync(_db.Countries.Where(c => c.Id == request.CountryId), cancellationToken).ConfigureAwait(false); if (!countryExists) { - return _msg.NotFound("COUNTRY_NOT_FOUND"); + return _msg.NotFound(MessageKeys.Country.COUNTRY_NOT_FOUND); } var assignedById = _currentUser.GetUserId(); @@ -90,7 +90,7 @@ public async Task> Handle( assignment.AssignedById, assignment.RevokedOn, assignment.RevokedById, - IsActive: true), "STATE_REP_ASSIGNMENT_CREATED"); + IsActive: true), MessageKeys.Identity.STATE_REP_ASSIGNMENT_CREATED); } private static async Task ExistsAsync(IQueryable query, CancellationToken ct) diff --git a/backend/src/CCE.Application/Identity/Commands/CreateUser/CreateUserCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/CreateUser/CreateUserCommandHandler.cs index aa7c1cd1..41ab85c9 100644 --- a/backend/src/CCE.Application/Identity/Commands/CreateUser/CreateUserCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/CreateUser/CreateUserCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Identity.Auth.Common; using CCE.Application.Identity.Dtos; @@ -31,11 +31,11 @@ public async Task> Handle(CreateUserCommand request, Can request.PhoneNumber, request.CountryId, request.Role, createdBy, cancellationToken).ConfigureAwait(false); if (result.EmailTaken) return _msg.EmailExists(); - if (result.Failed || result.User is null) return _msg.BusinessRule("REGISTRATION_FAILED"); + if (result.Failed || result.User is null) return _msg.BusinessRule(MessageKeys.Identity.REGISTRATION_FAILED); var detail = await _mediator.Send(new GetUserByIdQuery(result.User.Id), cancellationToken).ConfigureAwait(false); if (!detail.Success) return detail; - return _msg.Ok(detail.Data!, result.PasswordResetSent ? "USER_CREATED" : "REGISTER_SUCCESS"); + return _msg.Ok(detail.Data!, result.PasswordResetSent ? MessageKeys.Identity.USER_CREATED : MessageKeys.Identity.REGISTER_SUCCESS); } } diff --git a/backend/src/CCE.Application/Identity/Commands/CreateUser/CreateUserCommandValidator.cs b/backend/src/CCE.Application/Identity/Commands/CreateUser/CreateUserCommandValidator.cs index 392c7ea7..b89af57e 100644 --- a/backend/src/CCE.Application/Identity/Commands/CreateUser/CreateUserCommandValidator.cs +++ b/backend/src/CCE.Application/Identity/Commands/CreateUser/CreateUserCommandValidator.cs @@ -1,3 +1,4 @@ +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.Identity.Commands.CreateUser; @@ -13,13 +14,23 @@ public sealed class CreateUserCommandValidator : AbstractValidator c.FirstName).NotEmpty().MaximumLength(50) - .Matches(@"^\p{L}+$").WithMessage("First name must contain letters only."); - RuleFor(c => c.LastName).NotEmpty().MaximumLength(50) - .Matches(@"^\p{L}+$").WithMessage("Last name must contain letters only."); - RuleFor(c => c.Email).NotEmpty().MaximumLength(100).EmailAddress(); - RuleFor(c => c.PhoneNumber).NotEmpty().MaximumLength(15); - RuleFor(c => c.Role).NotEmpty().Must(r => AllowedRoles.Contains(r)) - .WithMessage($"Role must be one of: {string.Join(", ", AllowedRoles)}."); + RuleFor(c => c.FirstName) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(50).WithErrorCode(MessageKeys.Validation.MAX_LENGTH) + .Matches(@"^\p{L}+$").WithErrorCode(MessageKeys.Validation.INVALID_FORMAT); + RuleFor(c => c.LastName) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(50).WithErrorCode(MessageKeys.Validation.MAX_LENGTH) + .Matches(@"^\p{L}+$").WithErrorCode(MessageKeys.Validation.INVALID_FORMAT); + RuleFor(c => c.Email) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(100).WithErrorCode(MessageKeys.Validation.MAX_LENGTH) + .EmailAddress().WithErrorCode(MessageKeys.Validation.INVALID_EMAIL); + RuleFor(c => c.PhoneNumber) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(15).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + RuleFor(c => c.Role) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .Must(r => AllowedRoles.Contains(r)).WithErrorCode(MessageKeys.Validation.INVALID_ENUM); } } diff --git a/backend/src/CCE.Application/Identity/Commands/DeleteUser/DeleteUserCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/DeleteUser/DeleteUserCommandHandler.cs index 8123be3c..30b73770 100644 --- a/backend/src/CCE.Application/Identity/Commands/DeleteUser/DeleteUserCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/DeleteUser/DeleteUserCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Identity.Dtos; using CCE.Application.Identity.Public; @@ -55,6 +55,6 @@ public async Task> Handle(DeleteUserCommand request, Can user.CountryId, user.AvatarUrl, System.Array.Empty(), - user.Status == Domain.Identity.UserStatus.Active), "USER_DELETED"); + user.Status == Domain.Identity.UserStatus.Active), MessageKeys.Identity.USER_DELETED); } } diff --git a/backend/src/CCE.Application/Identity/Commands/RejectExpertRequest/RejectExpertRequestCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/RejectExpertRequest/RejectExpertRequestCommandHandler.cs index 15ee8c11..a26e5281 100644 --- a/backend/src/CCE.Application/Identity/Commands/RejectExpertRequest/RejectExpertRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/RejectExpertRequest/RejectExpertRequestCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Dtos; @@ -71,6 +71,6 @@ public async Task> Handle( registration.ProcessedOn, registration.RejectionReasonAr, registration.RejectionReasonEn, - cvIds.FirstOrDefault()), "EXPERT_REQUEST_REJECTED"); + cvIds.FirstOrDefault()), MessageKeys.Identity.EXPERT_REQUEST_REJECTED); } } diff --git a/backend/src/CCE.Application/Identity/Commands/RevokeStateRepAssignment/RevokeStateRepAssignmentCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/RevokeStateRepAssignment/RevokeStateRepAssignmentCommandHandler.cs index 105afcf0..fa8db205 100644 --- a/backend/src/CCE.Application/Identity/Commands/RevokeStateRepAssignment/RevokeStateRepAssignmentCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/RevokeStateRepAssignment/RevokeStateRepAssignmentCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -33,7 +33,7 @@ public async Task> Handle(RevokeStateRepAssignmentCommand req var assignment = await _service.FindIncludingRevokedAsync(request.Id, cancellationToken).ConfigureAwait(false); if (assignment is null) { - return _msg.NotFound("STATE_REP_ASSIGNMENT_NOT_FOUND"); + return _msg.NotFound(MessageKeys.Identity.STATE_REP_ASSIGNMENT_NOT_FOUND); } var revokedById = _currentUser.GetUserId(); @@ -46,6 +46,6 @@ public async Task> Handle(RevokeStateRepAssignmentCommand req _service.Update(assignment); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok("STATE_REP_ASSIGNMENT_REVOKED"); + return _msg.Ok(MessageKeys.Identity.STATE_REP_ASSIGNMENT_REVOKED); } } diff --git a/backend/src/CCE.Application/Identity/Permissions/Commands/GrantRolePermissionsCommandHandler.cs b/backend/src/CCE.Application/Identity/Permissions/Commands/GrantRolePermissionsCommandHandler.cs index b9cceb3c..127a0abb 100644 --- a/backend/src/CCE.Application/Identity/Permissions/Commands/GrantRolePermissionsCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Permissions/Commands/GrantRolePermissionsCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Identity.Auth.Common; using CCE.Application.Messages; @@ -48,7 +48,7 @@ public async Task> Handle( .ConfigureAwait(false); return result is null - ? _msg.NotFound("ROLE_NOT_FOUND") - : _msg.Ok(result, "PERMISSIONS_GRANTED"); + ? _msg.NotFound(MessageKeys.Identity.ROLE_NOT_FOUND) + : _msg.Ok(result, MessageKeys.Identity.PERMISSIONS_GRANTED); } } diff --git a/backend/src/CCE.Application/Identity/Permissions/Commands/GrantUserClaimsCommandHandler.cs b/backend/src/CCE.Application/Identity/Permissions/Commands/GrantUserClaimsCommandHandler.cs index 0deb2239..c14f0e20 100644 --- a/backend/src/CCE.Application/Identity/Permissions/Commands/GrantUserClaimsCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Permissions/Commands/GrantUserClaimsCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Auth.Common; @@ -41,7 +41,7 @@ public async Task> Handle( .ConfigureAwait(false); if (!userExists) - return _msg.NotFound("USER_NOT_FOUND"); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); var actorId = _currentUser.GetUserId() ?? Guid.Empty; var actorEmail = _currentUser.GetActor(); @@ -81,6 +81,6 @@ public async Task> Handle( all.OrderBy(c => c).ToArray(), toAdd.Count, 0, - all.Count), "CLAIMS_GRANTED"); + all.Count), MessageKeys.Identity.CLAIMS_GRANTED); } } diff --git a/backend/src/CCE.Application/Identity/Permissions/Commands/RevokeRolePermissionsCommandHandler.cs b/backend/src/CCE.Application/Identity/Permissions/Commands/RevokeRolePermissionsCommandHandler.cs index e7fdf065..8a9c6cd8 100644 --- a/backend/src/CCE.Application/Identity/Permissions/Commands/RevokeRolePermissionsCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Permissions/Commands/RevokeRolePermissionsCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Identity.Auth.Common; using CCE.Application.Messages; @@ -48,7 +48,7 @@ public async Task> Handle( .ConfigureAwait(false); return result is null - ? _msg.NotFound("ROLE_NOT_FOUND") - : _msg.Ok(result, "PERMISSIONS_REVOKED"); + ? _msg.NotFound(MessageKeys.Identity.ROLE_NOT_FOUND) + : _msg.Ok(result, MessageKeys.Identity.PERMISSIONS_REVOKED); } } diff --git a/backend/src/CCE.Application/Identity/Permissions/Commands/RevokeUserClaimsCommandHandler.cs b/backend/src/CCE.Application/Identity/Permissions/Commands/RevokeUserClaimsCommandHandler.cs index 77013427..b6a9dee2 100644 --- a/backend/src/CCE.Application/Identity/Permissions/Commands/RevokeUserClaimsCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Permissions/Commands/RevokeUserClaimsCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Auth.Common; @@ -41,7 +41,7 @@ public async Task> Handle( .ConfigureAwait(false); if (!userExists) - return _msg.NotFound("USER_NOT_FOUND"); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); var actorId = _currentUser.GetUserId() ?? Guid.Empty; var actorEmail = _currentUser.GetActor(); @@ -76,6 +76,6 @@ public async Task> Handle( [.. existing.OrderBy(c => c)], 0, toRemove.Count, - existing.Count), "CLAIMS_REVOKED"); + existing.Count), MessageKeys.Identity.CLAIMS_REVOKED); } } diff --git a/backend/src/CCE.Application/Identity/Permissions/Commands/UpsertRolePermissionsCommandHandler.cs b/backend/src/CCE.Application/Identity/Permissions/Commands/UpsertRolePermissionsCommandHandler.cs index aa66d782..39d7549d 100644 --- a/backend/src/CCE.Application/Identity/Permissions/Commands/UpsertRolePermissionsCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Permissions/Commands/UpsertRolePermissionsCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -41,7 +41,7 @@ public async Task> Handle( cancellationToken).ConfigureAwait(false); return result is null - ? _msg.NotFound("ROLE_NOT_FOUND") - : _msg.Ok(result, "PERMISSIONS_UPDATED"); + ? _msg.NotFound(MessageKeys.Identity.ROLE_NOT_FOUND) + : _msg.Ok(result, MessageKeys.Identity.PERMISSIONS_UPDATED); } } diff --git a/backend/src/CCE.Application/Identity/Permissions/Commands/UpsertUserClaimsCommandHandler.cs b/backend/src/CCE.Application/Identity/Permissions/Commands/UpsertUserClaimsCommandHandler.cs index 3b724ebb..d535ee1a 100644 --- a/backend/src/CCE.Application/Identity/Permissions/Commands/UpsertUserClaimsCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Permissions/Commands/UpsertUserClaimsCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Auth.Common; @@ -41,7 +41,7 @@ public async Task> Handle( .ConfigureAwait(false); if (!userExists) - return _msg.NotFound("USER_NOT_FOUND"); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); var actorId = _currentUser.GetUserId() ?? Guid.Empty; var actorEmail = _currentUser.GetActor(); @@ -92,6 +92,6 @@ public async Task> Handle( desired.OrderBy(c => c).ToArray(), toAdd.Count, toRemove.Count, - desired.Count), "USER_CLAIMS_UPDATED"); + desired.Count), MessageKeys.Identity.USER_CLAIMS_UPDATED); } } diff --git a/backend/src/CCE.Application/Identity/Permissions/Queries/GetPermissionMatrixQueryHandler.cs b/backend/src/CCE.Application/Identity/Permissions/Queries/GetPermissionMatrixQueryHandler.cs index 8ea7f7f0..a5ccc054 100644 --- a/backend/src/CCE.Application/Identity/Permissions/Queries/GetPermissionMatrixQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Permissions/Queries/GetPermissionMatrixQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -77,7 +77,7 @@ join r in _db.Roles on rc.RoleId equals r.Id }).ToArray())) .ToArray(); - return _msg.Ok(new PermissionMatrixDto(roles, entities, updatedAt), "ITEMS_LISTED"); + return _msg.Ok(new PermissionMatrixDto(roles, entities, updatedAt), MessageKeys.General.ITEMS_LISTED); } private static string ToTitle(string segment) diff --git a/backend/src/CCE.Application/Identity/Permissions/Queries/GetPermissionsQueryHandler.cs b/backend/src/CCE.Application/Identity/Permissions/Queries/GetPermissionsQueryHandler.cs index 508572c7..c6f542bc 100644 --- a/backend/src/CCE.Application/Identity/Permissions/Queries/GetPermissionsQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Permissions/Queries/GetPermissionsQueryHandler.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; @@ -43,7 +43,7 @@ public async Task> Handle( g.Select(claim => new PermissionItemDto(claim, DeriveDisplayName(claim))).ToArray())) .ToArray(); - return _msg.Ok(new PermissionsListDto(groups, updatedAt), "ITEMS_LISTED"); + return _msg.Ok(new PermissionsListDto(groups, updatedAt), MessageKeys.General.ITEMS_LISTED); } internal static string FirstSegment(string claim) diff --git a/backend/src/CCE.Application/Identity/Permissions/Queries/GetUserClaimsQueryHandler.cs b/backend/src/CCE.Application/Identity/Permissions/Queries/GetUserClaimsQueryHandler.cs index a9c8f1f3..11ed0936 100644 --- a/backend/src/CCE.Application/Identity/Permissions/Queries/GetUserClaimsQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Permissions/Queries/GetUserClaimsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -27,7 +27,7 @@ public async Task> Handle( .ConfigureAwait(false); if (!userExists) - return _msg.NotFound("USER_NOT_FOUND"); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); var claims = await _db.UserClaims .Where(uc => uc.UserId == request.UserId && uc.ClaimValue != null) @@ -46,6 +46,6 @@ public async Task> Handle( .Select(c => new UserClaimItemDto(c, GetPermissionsQueryHandler.DeriveDisplayName(c))) .ToArray(); - return _msg.Ok(new UserClaimsListDto(request.UserId, items, updatedAt), "ITEMS_LISTED"); + return _msg.Ok(new UserClaimsListDto(request.UserId, items, updatedAt), MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandHandler.cs index 64672679..1ea5f652 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Application.Verification; @@ -58,7 +58,7 @@ public async Task> Handle( // Ownership validation — OTP must belong to the authenticated user if (otp.UserId.HasValue && otp.UserId.Value != request.UserId) - return _msg.Unauthorized("OTP_UNAUTHORIZED"); + return _msg.Unauthorized(MessageKeys.Verification.OTP_UNAUTHORIZED); otp.IncrementAttempt(); @@ -80,12 +80,12 @@ public async Task> Handle( // Use UserManager to ensure NormalizedEmail and SecurityStamp are properly updated var setEmailResult = await _userManager.SetEmailAsync(user, otp.Contact).ConfigureAwait(false); if (!setEmailResult.Succeeded) - return _msg.BusinessRule("EMAIL_CHANGE_FAILED"); + return _msg.BusinessRule(MessageKeys.Identity.EMAIL_CHANGE_FAILED); // Update UserName to match the new email var setUserNameResult = await _userManager.SetUserNameAsync(user, otp.Contact).ConfigureAwait(false); if (!setUserNameResult.Succeeded) - return _msg.BusinessRule("EMAIL_CHANGE_FAILED"); + return _msg.BusinessRule(MessageKeys.Identity.EMAIL_CHANGE_FAILED); // domain methods otp.MarkVerified(); diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandHandler.cs index 4c283f31..857d7d38 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Identity.Public.Commands.RequestPhoneChange; using CCE.Application.Messages; @@ -54,7 +54,7 @@ public async Task> Handle( // Ownership validation — OTP must belong to the authenticated user if (otp.UserId.HasValue && otp.UserId.Value != request.UserId) - return _msg.Unauthorized("OTP_UNAUTHORIZED"); + return _msg.Unauthorized(MessageKeys.Verification.OTP_UNAUTHORIZED); otp.IncrementAttempt(); diff --git a/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandHandler.cs index c06df468..9de14e2e 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Identity; using CCE.Application.Messages; @@ -56,6 +56,6 @@ public async Task> Handle( // ICceDbContext as unit of work await _db.SaveChangesAsync(ct).ConfigureAwait(false); - return _msg.Ok(new RequestVerificationResponseDto(entity!.Id, entity.ExpiresAt), "OTP_SENT"); + return _msg.Ok(new RequestVerificationResponseDto(entity!.Id, entity.ExpiresAt), MessageKeys.Verification.OTP_SENT); } } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandHandler.cs index 7817457c..d9c424fa 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Identity; using CCE.Application.Messages; @@ -65,7 +65,7 @@ public async Task> Handle( // ICceDbContext as unit of work await _db.SaveChangesAsync(ct).ConfigureAwait(false); - return _msg.Ok(new RequestVerificationResponseDto(entity!.Id, entity.ExpiresAt), "OTP_SENT"); + return _msg.Ok(new RequestVerificationResponseDto(entity!.Id, entity.ExpiresAt), MessageKeys.Verification.OTP_SENT); } } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/SubmitExpertRequest/SubmitExpertRequestCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/SubmitExpertRequest/SubmitExpertRequestCommandHandler.cs index 85ed9bf0..c0270cc8 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/SubmitExpertRequest/SubmitExpertRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/SubmitExpertRequest/SubmitExpertRequestCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Public.Dtos; @@ -68,6 +68,6 @@ public async Task> Handle(SubmitExpertRequestCo entity.Status, entity.ProcessedOn, entity.RejectionReasonAr, - entity.RejectionReasonEn), "EXPERT_REQUEST_SUBMITTED"); + entity.RejectionReasonEn), MessageKeys.Identity.EXPERT_REQUEST_SUBMITTED); } } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/SubmitExpertRequest/SubmitExpertRequestCommandValidator.cs b/backend/src/CCE.Application/Identity/Public/Commands/SubmitExpertRequest/SubmitExpertRequestCommandValidator.cs index 16dcf04b..05faa17f 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/SubmitExpertRequest/SubmitExpertRequestCommandValidator.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/SubmitExpertRequest/SubmitExpertRequestCommandValidator.cs @@ -1,3 +1,4 @@ +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.Identity.Public.Commands.SubmitExpertRequest; @@ -6,11 +7,18 @@ public sealed class SubmitExpertRequestCommandValidator : AbstractValidator x.RequesterId).NotEmpty(); - RuleFor(x => x.RequestedBioAr).NotEmpty().MaximumLength(500); - RuleFor(x => x.RequestedBioEn).NotEmpty().MaximumLength(500); - RuleFor(x => x.RequestedTags).NotNull().NotEmpty() - .WithMessage("At least one expertise tag is required."); - RuleFor(x => x.CvAssetFileId).NotEmpty(); + RuleFor(x => x.RequesterId) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); + RuleFor(x => x.RequestedBioAr) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(500).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + RuleFor(x => x.RequestedBioEn) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(500).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + RuleFor(x => x.RequestedTags) + .NotNull().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); + RuleFor(x => x.CvAssetFileId) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); } } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandHandler.cs index fb189238..44b2a5b7 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Identity.Public.Dtos; using CCE.Application.InterestManagement.Dtos; @@ -64,6 +64,6 @@ public async Task> Handle(UpdateMyProfileCommand reques user.KnowledgeLevel, interestTopics, user.CountryId, - user.AvatarUrl), "PROFILE_UPDATED"); + user.AvatarUrl), MessageKeys.Identity.PROFILE_UPDATED); } } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandValidator.cs b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandValidator.cs index 0bcf30cc..a3d7f905 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandValidator.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandValidator.cs @@ -1,3 +1,4 @@ +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.Identity.Public.Commands.UpdateMyProfile; @@ -6,18 +7,25 @@ public sealed class UpdateMyProfileCommandValidator : AbstractValidator x.FirstName).NotEmpty().MaximumLength(100); - RuleFor(x => x.LastName).NotEmpty().MaximumLength(100); - RuleFor(x => x.JobTitle).NotEmpty().MaximumLength(200); - RuleFor(x => x.OrganizationName).NotEmpty().MaximumLength(200); + RuleFor(x => x.FirstName) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(100).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + RuleFor(x => x.LastName) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(100).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + RuleFor(x => x.JobTitle) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(200).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); + RuleFor(x => x.OrganizationName) + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(200).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.LocalePreference) - .NotEmpty() - .Must(l => l == "ar" || l == "en") - .WithMessage("LocalePreference must be 'ar' or 'en'."); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .Must(l => l == "ar" || l == "en").WithErrorCode(MessageKeys.Validation.INVALID_ENUM); RuleFor(x => x.AvatarUrl) .Must(url => url is null || url.StartsWith("https://", System.StringComparison.OrdinalIgnoreCase)) - .WithMessage("AvatarUrl must be null or start with 'https://'."); + .WithErrorCode(MessageKeys.Validation.INVALID_FORMAT); } } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs index 94636c2c..17560752 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.InterestManagement.Dtos; using CCE.Application.Messages; @@ -74,11 +74,11 @@ public async Task> Handle( .AnyAsync(c => c.Id == request.TargetCountryId.Value, cancellationToken) .ConfigureAwait(false); if (!countryExists) - errors.Add(_msg.Field("targetCountryId", "COUNTRY_NOT_FOUND")); + errors.Add(_msg.Field("targetCountryId", MessageKeys.Country.COUNTRY_NOT_FOUND)); } if (errors.Count > 0) - return _msg.ValidationError("VALIDATION_ERROR", errors); + return _msg.ValidationError(MessageKeys.General.VALIDATION_ERROR, errors); // Load category mapping for all interest topics (for filtering by category) var topicCategoryMap = validTopics diff --git a/backend/src/CCE.Application/Identity/Public/Queries/GetMyExpertStatus/GetMyExpertStatusQueryHandler.cs b/backend/src/CCE.Application/Identity/Public/Queries/GetMyExpertStatus/GetMyExpertStatusQueryHandler.cs index a6342628..10f59c44 100644 --- a/backend/src/CCE.Application/Identity/Public/Queries/GetMyExpertStatus/GetMyExpertStatusQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Queries/GetMyExpertStatus/GetMyExpertStatusQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Public.Dtos; @@ -47,6 +47,6 @@ public async Task> Handle(GetMyExpertStatusQuer entity.Status, entity.ProcessedOn, entity.RejectionReasonAr, - entity.RejectionReasonEn), "SUCCESS_OPERATION"); + entity.RejectionReasonEn), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Identity/Public/Queries/GetMyInterests/GetMyInterestsQueryHandler.cs b/backend/src/CCE.Application/Identity/Public/Queries/GetMyInterests/GetMyInterestsQueryHandler.cs index 86f7a265..50ed26a3 100644 --- a/backend/src/CCE.Application/Identity/Public/Queries/GetMyInterests/GetMyInterestsQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Queries/GetMyInterests/GetMyInterestsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Identity.Public.Dtos; using CCE.Application.InterestManagement.Dtos; @@ -56,6 +56,6 @@ knowledgeAssessmentTopic is not null jobSectorTopic is not null ? new InterestTopicDto(jobSectorTopic.Id, jobSectorTopic.NameAr, jobSectorTopic.NameEn, jobSectorTopic.Category, jobSectorTopic.IsActive) : null, - user.CountryId), "SUCCESS_OPERATION"); + user.CountryId), MessageKeys.General.SUCCESS_OPERATION); } } \ No newline at end of file diff --git a/backend/src/CCE.Application/Identity/Public/Queries/GetMyProfile/GetMyProfileQueryHandler.cs b/backend/src/CCE.Application/Identity/Public/Queries/GetMyProfile/GetMyProfileQueryHandler.cs index 2f89e72b..1d909a48 100644 --- a/backend/src/CCE.Application/Identity/Public/Queries/GetMyProfile/GetMyProfileQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Queries/GetMyProfile/GetMyProfileQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Public.Dtos; @@ -55,6 +55,6 @@ public async Task> Handle(GetMyProfileQuery request, Ca user.KnowledgeLevel, interestTopics, user.CountryId, - user.AvatarUrl), "SUCCESS_OPERATION"); + user.AvatarUrl), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Identity/Queries/GetExpertRequestById/GetExpertRequestByIdQueryHandler.cs b/backend/src/CCE.Application/Identity/Queries/GetExpertRequestById/GetExpertRequestByIdQueryHandler.cs index 613b1030..0e5fa092 100644 --- a/backend/src/CCE.Application/Identity/Queries/GetExpertRequestById/GetExpertRequestByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Queries/GetExpertRequestById/GetExpertRequestByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Dtos; @@ -58,6 +58,6 @@ public async Task> Handle( row.ProcessedOn, row.RejectionReasonAr, row.RejectionReasonEn, - cvAssetFileIds.FirstOrDefault()), "SUCCESS_OPERATION"); + cvAssetFileIds.FirstOrDefault()), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Identity/Queries/GetUserById/GetUserByIdQueryHandler.cs b/backend/src/CCE.Application/Identity/Queries/GetUserById/GetUserByIdQueryHandler.cs index d9da81fb..efb303ba 100644 --- a/backend/src/CCE.Application/Identity/Queries/GetUserById/GetUserByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Queries/GetUserById/GetUserByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; @@ -65,6 +65,6 @@ join r in _db.Roles on ur.RoleId equals r.Id user.CountryId, user.AvatarUrl, roles, - isActive), "SUCCESS_OPERATION"); + isActive), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Identity/Queries/ListExpertProfiles/ListExpertProfilesQuery.cs b/backend/src/CCE.Application/Identity/Queries/ListExpertProfiles/ListExpertProfilesQuery.cs index 6df34853..14e3bd9b 100644 --- a/backend/src/CCE.Application/Identity/Queries/ListExpertProfiles/ListExpertProfilesQuery.cs +++ b/backend/src/CCE.Application/Identity/Queries/ListExpertProfiles/ListExpertProfilesQuery.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Dtos; using MediatR; @@ -7,4 +8,4 @@ namespace CCE.Application.Identity.Queries.ListExpertProfiles; public sealed record ListExpertProfilesQuery( int Page = 1, int PageSize = 20, - string? Search = null) : IRequest>; + string? Search = null) : IRequest>>; diff --git a/backend/src/CCE.Application/Identity/Queries/ListExpertProfiles/ListExpertProfilesQueryHandler.cs b/backend/src/CCE.Application/Identity/Queries/ListExpertProfiles/ListExpertProfilesQueryHandler.cs index dfa3de8f..94e4434b 100644 --- a/backend/src/CCE.Application/Identity/Queries/ListExpertProfiles/ListExpertProfilesQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Queries/ListExpertProfiles/ListExpertProfilesQueryHandler.cs @@ -1,18 +1,25 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Dtos; +using CCE.Application.Messages; using MediatR; namespace CCE.Application.Identity.Queries.ListExpertProfiles; public sealed class ListExpertProfilesQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListExpertProfilesQueryHandler(ICceDbContext db) => _db = db; + public ListExpertProfilesQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task> Handle( + public async Task>> Handle( ListExpertProfilesQuery request, CancellationToken cancellationToken) { @@ -34,8 +41,8 @@ join u in _db.Users on p.UserId equals u.Id if (paged.Items.Count == 0) { - return new PagedResult( - Array.Empty(), paged.Page, paged.PageSize, paged.Total); + return _msg.Ok(new PagedResult( + Array.Empty(), paged.Page, paged.PageSize, paged.Total), MessageKeys.General.ITEMS_LISTED); } var userIds = paged.Items.Select(p => p.UserId).Distinct().ToList(); @@ -58,7 +65,7 @@ where userIds.Contains(u.Id) p.ApprovedOn, p.ApprovedById)).ToList(); - return new PagedResult(items, paged.Page, paged.PageSize, paged.Total); + return _msg.Ok(new PagedResult(items, paged.Page, paged.PageSize, paged.Total), MessageKeys.General.ITEMS_LISTED); } private sealed record UserNameRow(Guid UserId, string? UserName); diff --git a/backend/src/CCE.Application/Identity/Queries/ListExpertRequests/ListExpertRequestsQuery.cs b/backend/src/CCE.Application/Identity/Queries/ListExpertRequests/ListExpertRequestsQuery.cs index ef6e8cc9..20774fc5 100644 --- a/backend/src/CCE.Application/Identity/Queries/ListExpertRequests/ListExpertRequestsQuery.cs +++ b/backend/src/CCE.Application/Identity/Queries/ListExpertRequests/ListExpertRequestsQuery.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Dtos; using CCE.Domain.Identity; @@ -9,4 +10,4 @@ public sealed record ListExpertRequestsQuery( int Page = 1, int PageSize = 20, ExpertRegistrationStatus? Status = null, - System.Guid? RequestedById = null) : IRequest>; + System.Guid? RequestedById = null) : IRequest>>; diff --git a/backend/src/CCE.Application/Identity/Queries/ListExpertRequests/ListExpertRequestsQueryHandler.cs b/backend/src/CCE.Application/Identity/Queries/ListExpertRequests/ListExpertRequestsQueryHandler.cs index 9a6246ac..ad6a87dc 100644 --- a/backend/src/CCE.Application/Identity/Queries/ListExpertRequests/ListExpertRequestsQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Queries/ListExpertRequests/ListExpertRequestsQueryHandler.cs @@ -1,19 +1,26 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Dtos; +using CCE.Application.Messages; using CCE.Domain.Identity; using MediatR; namespace CCE.Application.Identity.Queries.ListExpertRequests; public sealed class ListExpertRequestsQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListExpertRequestsQueryHandler(ICceDbContext db) => _db = db; + public ListExpertRequestsQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task> Handle( + public async Task>> Handle( ListExpertRequestsQuery request, CancellationToken cancellationToken) { @@ -32,8 +39,8 @@ public async Task> Handle( if (paged.Items.Count == 0) { - return new PagedResult( - Array.Empty(), paged.Page, paged.PageSize, paged.Total); + return _msg.Ok(new PagedResult( + Array.Empty(), paged.Page, paged.PageSize, paged.Total), MessageKeys.General.ITEMS_LISTED); } var requesterIds = paged.Items.Select(r => r.RequestedById).Distinct().ToList(); @@ -67,7 +74,7 @@ where requestIds.Contains(att.ExpertRequestId) && att.AttachmentType == ExpertRe r.RejectionReasonEn, cvByRequestId.TryGetValue(r.Id, out var cvAssetFileId) ? cvAssetFileId : null)).ToList(); - return new PagedResult(items, paged.Page, paged.PageSize, paged.Total); + return _msg.Ok(new PagedResult(items, paged.Page, paged.PageSize, paged.Total), MessageKeys.General.ITEMS_LISTED); } private sealed record UserNameRow(Guid UserId, string? UserName); diff --git a/backend/src/CCE.Application/Identity/Queries/ListStateRepAssignments/ListStateRepAssignmentsQuery.cs b/backend/src/CCE.Application/Identity/Queries/ListStateRepAssignments/ListStateRepAssignmentsQuery.cs index 12972721..db4cdce9 100644 --- a/backend/src/CCE.Application/Identity/Queries/ListStateRepAssignments/ListStateRepAssignmentsQuery.cs +++ b/backend/src/CCE.Application/Identity/Queries/ListStateRepAssignments/ListStateRepAssignmentsQuery.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Dtos; using MediatR; @@ -14,4 +15,4 @@ public sealed record ListStateRepAssignmentsQuery( int PageSize = 20, System.Guid? UserId = null, System.Guid? CountryId = null, - bool Active = true) : IRequest>; + bool Active = true) : IRequest>>; diff --git a/backend/src/CCE.Application/Identity/Queries/ListStateRepAssignments/ListStateRepAssignmentsQueryHandler.cs b/backend/src/CCE.Application/Identity/Queries/ListStateRepAssignments/ListStateRepAssignmentsQueryHandler.cs index 16e1a209..0c790661 100644 --- a/backend/src/CCE.Application/Identity/Queries/ListStateRepAssignments/ListStateRepAssignmentsQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Queries/ListStateRepAssignments/ListStateRepAssignmentsQueryHandler.cs @@ -1,19 +1,26 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Dtos; +using CCE.Application.Messages; using CCE.Domain.Identity; using MediatR; namespace CCE.Application.Identity.Queries.ListStateRepAssignments; public sealed class ListStateRepAssignmentsQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListStateRepAssignmentsQueryHandler(ICceDbContext db) => _db = db; + public ListStateRepAssignmentsQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task> Handle( + public async Task>> Handle( ListStateRepAssignmentsQuery request, CancellationToken cancellationToken) { @@ -36,8 +43,8 @@ public async Task> Handle( if (paged.Items.Count == 0) { - return new PagedResult( - Array.Empty(), paged.Page, paged.PageSize, paged.Total); + return _msg.Ok(new PagedResult( + Array.Empty(), paged.Page, paged.PageSize, paged.Total), MessageKeys.General.ITEMS_LISTED); } var userIds = paged.Items.Select(a => a.UserId).Distinct().ToList(); @@ -59,7 +66,7 @@ where userIds.Contains(u.Id) a.RevokedById, IsActive: a.RevokedOn is null && !a.IsDeleted)).ToList(); - return new PagedResult(items, paged.Page, paged.PageSize, paged.Total); + return _msg.Ok(new PagedResult(items, paged.Page, paged.PageSize, paged.Total), MessageKeys.General.ITEMS_LISTED); } private sealed record UserNameRow(Guid UserId, string? UserName); diff --git a/backend/src/CCE.Application/Identity/Queries/ListUsers/ListUsersQueryHandler.cs b/backend/src/CCE.Application/Identity/Queries/ListUsers/ListUsersQueryHandler.cs index ef5bf635..159ad856 100644 --- a/backend/src/CCE.Application/Identity/Queries/ListUsers/ListUsersQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Queries/ListUsers/ListUsersQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Identity.Dtos; @@ -60,6 +60,6 @@ public async Task>> Handle( .ToPagedResultAsync(request.Page, request.PageSize, cancellationToken) .ConfigureAwait(false); - return _msg.Ok(paged, "ITEMS_LISTED"); + return _msg.Ok(paged, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/InteractiveCity/Public/Commands/DeleteMyScenario/DeleteMyScenarioCommand.cs b/backend/src/CCE.Application/InteractiveCity/Public/Commands/DeleteMyScenario/DeleteMyScenarioCommand.cs index 7acc4568..28912d05 100644 --- a/backend/src/CCE.Application/InteractiveCity/Public/Commands/DeleteMyScenario/DeleteMyScenarioCommand.cs +++ b/backend/src/CCE.Application/InteractiveCity/Public/Commands/DeleteMyScenario/DeleteMyScenarioCommand.cs @@ -1,5 +1,6 @@ +using CCE.Application.Common; using MediatR; namespace CCE.Application.InteractiveCity.Public.Commands.DeleteMyScenario; -public sealed record DeleteMyScenarioCommand(System.Guid Id, System.Guid UserId) : IRequest; +public sealed record DeleteMyScenarioCommand(System.Guid Id, System.Guid UserId) : IRequest>; diff --git a/backend/src/CCE.Application/InteractiveCity/Public/Commands/DeleteMyScenario/DeleteMyScenarioCommandHandler.cs b/backend/src/CCE.Application/InteractiveCity/Public/Commands/DeleteMyScenario/DeleteMyScenarioCommandHandler.cs index 8321d909..fe23fbbb 100644 --- a/backend/src/CCE.Application/InteractiveCity/Public/Commands/DeleteMyScenario/DeleteMyScenarioCommandHandler.cs +++ b/backend/src/CCE.Application/InteractiveCity/Public/Commands/DeleteMyScenario/DeleteMyScenarioCommandHandler.cs @@ -1,20 +1,24 @@ +using CCE.Application.Common; +using CCE.Application.Messages; using CCE.Domain.Common; using MediatR; namespace CCE.Application.InteractiveCity.Public.Commands.DeleteMyScenario; -public sealed class DeleteMyScenarioCommandHandler : IRequestHandler +public sealed class DeleteMyScenarioCommandHandler : IRequestHandler> { private readonly ICityScenarioService _service; private readonly ISystemClock _clock; + private readonly MessageFactory _msg; - public DeleteMyScenarioCommandHandler(ICityScenarioService service, ISystemClock clock) + public DeleteMyScenarioCommandHandler(ICityScenarioService service, ISystemClock clock, MessageFactory msg) { _service = service; _clock = clock; + _msg = msg; } - public async Task Handle(DeleteMyScenarioCommand request, CancellationToken cancellationToken) + public async Task> Handle(DeleteMyScenarioCommand request, CancellationToken cancellationToken) { var scenario = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); @@ -26,6 +30,6 @@ public async Task Handle(DeleteMyScenarioCommand request, CancellationToke scenario.SoftDelete(request.UserId, _clock); await _service.UpdateAsync(scenario, cancellationToken).ConfigureAwait(false); - return Unit.Value; + return _msg.Ok(MessageKeys.General.SUCCESS_DELETED); } } diff --git a/backend/src/CCE.Application/InteractiveCity/Public/Commands/RunScenario/RunScenarioCommand.cs b/backend/src/CCE.Application/InteractiveCity/Public/Commands/RunScenario/RunScenarioCommand.cs index 6f4a8487..9da5efc4 100644 --- a/backend/src/CCE.Application/InteractiveCity/Public/Commands/RunScenario/RunScenarioCommand.cs +++ b/backend/src/CCE.Application/InteractiveCity/Public/Commands/RunScenario/RunScenarioCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.InteractiveCity.Public.Dtos; using CCE.Domain.InteractiveCity; using MediatR; @@ -7,4 +8,4 @@ namespace CCE.Application.InteractiveCity.Public.Commands.RunScenario; public sealed record RunScenarioCommand( CityType CityType, int TargetYear, - string ConfigurationJson) : IRequest; + string ConfigurationJson) : IRequest>; diff --git a/backend/src/CCE.Application/InteractiveCity/Public/Commands/RunScenario/RunScenarioCommandHandler.cs b/backend/src/CCE.Application/InteractiveCity/Public/Commands/RunScenario/RunScenarioCommandHandler.cs index dc81ba07..f082e655 100644 --- a/backend/src/CCE.Application/InteractiveCity/Public/Commands/RunScenario/RunScenarioCommandHandler.cs +++ b/backend/src/CCE.Application/InteractiveCity/Public/Commands/RunScenario/RunScenarioCommandHandler.cs @@ -1,21 +1,25 @@ -using System.Text.Json; +using System.Text.Json; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.InteractiveCity.Public.Dtos; +using CCE.Application.Messages; using MediatR; namespace CCE.Application.InteractiveCity.Public.Commands.RunScenario; -public sealed class RunScenarioCommandHandler : IRequestHandler +public sealed class RunScenarioCommandHandler : IRequestHandler> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public RunScenarioCommandHandler(ICceDbContext db) + public RunScenarioCommandHandler(ICceDbContext db, MessageFactory msg) { _db = db; + _msg = msg; } - public async Task Handle(RunScenarioCommand request, CancellationToken cancellationToken) + public async Task> Handle(RunScenarioCommand request, CancellationToken cancellationToken) { // Parse configurationJson — on failure return zero totals (don't expose 500 to anonymous callers). List technologyIds; @@ -25,27 +29,28 @@ public async Task Handle(RunScenarioCommand request, C if (!doc.RootElement.TryGetProperty("technologyIds", out var idsElement) || idsElement.ValueKind != JsonValueKind.Array) { - return InvalidConfig(); + return _msg.Ok(InvalidConfig(), MessageKeys.General.SUCCESS_OPERATION); } technologyIds = new List(); foreach (var el in idsElement.EnumerateArray()) { if (!el.TryGetGuid(out var id)) - return InvalidConfig(); + return _msg.Ok(InvalidConfig(), MessageKeys.General.SUCCESS_OPERATION); technologyIds.Add(id); } } catch (JsonException) { - return InvalidConfig(); + return _msg.Ok(InvalidConfig(), MessageKeys.General.SUCCESS_OPERATION); } if (technologyIds.Count == 0) { - return new CityScenarioRunResultDto(0m, 0m, + var noTech = new CityScenarioRunResultDto(0m, 0m, "لا توجد تقنيات محددة", "No technologies selected"); + return _msg.Ok(noTech, MessageKeys.General.SUCCESS_OPERATION); } var techs = await _db.CityTechnologies @@ -56,11 +61,12 @@ public async Task Handle(RunScenarioCommand request, C var totalCarbon = techs.Sum(t => t.CarbonImpactKgPerYear); var totalCost = techs.Sum(t => t.CostUsd); - return new CityScenarioRunResultDto( + var dto = new CityScenarioRunResultDto( totalCarbon, totalCost, $"إجمالي تأثير الكربون: {totalCarbon} كغ/سنة، التكلفة الإجمالية: {totalCost} دولار", $"Total carbon impact: {totalCarbon} kg/year, total cost: {totalCost} USD"); + return _msg.Ok(dto, MessageKeys.General.SUCCESS_OPERATION); } private static CityScenarioRunResultDto InvalidConfig() => diff --git a/backend/src/CCE.Application/InteractiveCity/Public/Commands/SaveScenario/SaveScenarioCommand.cs b/backend/src/CCE.Application/InteractiveCity/Public/Commands/SaveScenario/SaveScenarioCommand.cs index 8f5d424c..34a437c2 100644 --- a/backend/src/CCE.Application/InteractiveCity/Public/Commands/SaveScenario/SaveScenarioCommand.cs +++ b/backend/src/CCE.Application/InteractiveCity/Public/Commands/SaveScenario/SaveScenarioCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.InteractiveCity.Public.Dtos; using CCE.Domain.InteractiveCity; using MediatR; @@ -10,4 +11,4 @@ public sealed record SaveScenarioCommand( string NameEn, CityType CityType, int TargetYear, - string ConfigurationJson) : IRequest; + string ConfigurationJson) : IRequest>; diff --git a/backend/src/CCE.Application/InteractiveCity/Public/Commands/SaveScenario/SaveScenarioCommandHandler.cs b/backend/src/CCE.Application/InteractiveCity/Public/Commands/SaveScenario/SaveScenarioCommandHandler.cs index d46f4911..2938881c 100644 --- a/backend/src/CCE.Application/InteractiveCity/Public/Commands/SaveScenario/SaveScenarioCommandHandler.cs +++ b/backend/src/CCE.Application/InteractiveCity/Public/Commands/SaveScenario/SaveScenarioCommandHandler.cs @@ -1,22 +1,26 @@ +using CCE.Application.Common; using CCE.Application.InteractiveCity.Public.Dtos; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.InteractiveCity; using MediatR; namespace CCE.Application.InteractiveCity.Public.Commands.SaveScenario; -public sealed class SaveScenarioCommandHandler : IRequestHandler +public sealed class SaveScenarioCommandHandler : IRequestHandler> { private readonly ICityScenarioService _service; private readonly ISystemClock _clock; + private readonly MessageFactory _msg; - public SaveScenarioCommandHandler(ICityScenarioService service, ISystemClock clock) + public SaveScenarioCommandHandler(ICityScenarioService service, ISystemClock clock, MessageFactory msg) { _service = service; _clock = clock; + _msg = msg; } - public async Task Handle(SaveScenarioCommand request, CancellationToken cancellationToken) + public async Task> Handle(SaveScenarioCommand request, CancellationToken cancellationToken) { var scenario = CityScenario.Create( request.UserId, @@ -29,7 +33,7 @@ public async Task Handle(SaveScenarioCommand request, Cancellat await _service.SaveAsync(scenario, cancellationToken).ConfigureAwait(false); - return new CityScenarioDto( + var dto = new CityScenarioDto( scenario.Id, scenario.NameAr, scenario.NameEn, @@ -38,5 +42,6 @@ public async Task Handle(SaveScenarioCommand request, Cancellat scenario.ConfigurationJson, scenario.CreatedOn, scenario.LastModifiedOn); + return _msg.Ok(dto, MessageKeys.General.SUCCESS_CREATED); } } diff --git a/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListCityTechnologies/ListCityTechnologiesQuery.cs b/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListCityTechnologies/ListCityTechnologiesQuery.cs index 858c556c..e8c5aba1 100644 --- a/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListCityTechnologies/ListCityTechnologiesQuery.cs +++ b/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListCityTechnologies/ListCityTechnologiesQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.InteractiveCity.Public.Dtos; using MediatR; namespace CCE.Application.InteractiveCity.Public.Queries.ListCityTechnologies; -public sealed record ListCityTechnologiesQuery : IRequest>; +public sealed record ListCityTechnologiesQuery : IRequest>>; diff --git a/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListCityTechnologies/ListCityTechnologiesQueryHandler.cs b/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListCityTechnologies/ListCityTechnologiesQueryHandler.cs index c1495000..7ce914e4 100644 --- a/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListCityTechnologies/ListCityTechnologiesQueryHandler.cs +++ b/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListCityTechnologies/ListCityTechnologiesQueryHandler.cs @@ -1,22 +1,26 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.InteractiveCity.Public.Dtos; +using CCE.Application.Messages; using CCE.Domain.InteractiveCity; using MediatR; namespace CCE.Application.InteractiveCity.Public.Queries.ListCityTechnologies; public sealed class ListCityTechnologiesQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListCityTechnologiesQueryHandler(ICceDbContext db) + public ListCityTechnologiesQueryHandler(ICceDbContext db, MessageFactory msg) { _db = db; + _msg = msg; } - public async Task> Handle( + public async Task>> Handle( ListCityTechnologiesQuery request, CancellationToken cancellationToken) { var techs = await _db.CityTechnologies @@ -26,7 +30,8 @@ public ListCityTechnologiesQueryHandler(ICceDbContext db) .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); - return techs.Select(MapToDto).ToList(); + System.Collections.Generic.IReadOnlyList list = techs.Select(MapToDto).ToList(); + return _msg.Ok(list, MessageKeys.General.ITEMS_LISTED); } internal static CityTechnologyDto MapToDto(CityTechnology t) => new( diff --git a/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListMyScenarios/ListMyScenariosQuery.cs b/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListMyScenarios/ListMyScenariosQuery.cs index 36692723..e15e72fc 100644 --- a/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListMyScenarios/ListMyScenariosQuery.cs +++ b/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListMyScenarios/ListMyScenariosQuery.cs @@ -1,7 +1,8 @@ +using CCE.Application.Common; using CCE.Application.InteractiveCity.Public.Dtos; using MediatR; namespace CCE.Application.InteractiveCity.Public.Queries.ListMyScenarios; public sealed record ListMyScenariosQuery(System.Guid UserId) - : IRequest>; + : IRequest>>; diff --git a/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListMyScenarios/ListMyScenariosQueryHandler.cs b/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListMyScenarios/ListMyScenariosQueryHandler.cs index b005a73e..729a1d60 100644 --- a/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListMyScenarios/ListMyScenariosQueryHandler.cs +++ b/backend/src/CCE.Application/InteractiveCity/Public/Queries/ListMyScenarios/ListMyScenariosQueryHandler.cs @@ -1,22 +1,26 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.InteractiveCity.Public.Dtos; +using CCE.Application.Messages; using CCE.Domain.InteractiveCity; using MediatR; namespace CCE.Application.InteractiveCity.Public.Queries.ListMyScenarios; public sealed class ListMyScenariosQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListMyScenariosQueryHandler(ICceDbContext db) + public ListMyScenariosQueryHandler(ICceDbContext db, MessageFactory msg) { _db = db; + _msg = msg; } - public async Task> Handle( + public async Task>> Handle( ListMyScenariosQuery request, CancellationToken cancellationToken) { var scenarios = await _db.CityScenarios @@ -25,7 +29,8 @@ public ListMyScenariosQueryHandler(ICceDbContext db) .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); - return scenarios.Select(MapToDto).ToList(); + System.Collections.Generic.IReadOnlyList list = scenarios.Select(MapToDto).ToList(); + return _msg.Ok(list, MessageKeys.General.ITEMS_LISTED); } internal static CityScenarioDto MapToDto(CityScenario s) => new( diff --git a/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMap/CreateInteractiveMapCommandValidator.cs b/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMap/CreateInteractiveMapCommandValidator.cs index cc589c6a..1b1b9b56 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMap/CreateInteractiveMapCommandValidator.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMap/CreateInteractiveMapCommandValidator.cs @@ -1,4 +1,4 @@ -using CCE.Application.Errors; +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.InteractiveMaps.Commands.CreateInteractiveMap; @@ -8,14 +8,14 @@ internal sealed class CreateInteractiveMapCommandValidator : AbstractValidator x.NameAr) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(256).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(256).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.NameEn) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(256).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(256).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.DescriptionAr) - .MaximumLength(512).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .MaximumLength(512).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.DescriptionEn) - .MaximumLength(512).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .MaximumLength(512).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMapNode/CreateInteractiveMapNodeCommandValidator.cs b/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMapNode/CreateInteractiveMapNodeCommandValidator.cs index b90c4540..fdfa5b26 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMapNode/CreateInteractiveMapNodeCommandValidator.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMapNode/CreateInteractiveMapNodeCommandValidator.cs @@ -1,4 +1,4 @@ -using CCE.Application.Errors; +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.InteractiveMaps.Commands.CreateInteractiveMapNode; @@ -8,19 +8,19 @@ internal sealed class CreateInteractiveMapNodeCommandValidator : AbstractValidat public CreateInteractiveMapNodeCommandValidator() { RuleFor(x => x.InteractiveMapId) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); RuleFor(x => x.NameAr) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(256).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(256).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.NameEn) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(256).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(256).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.IconKey) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(128).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(128).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.Level) - .GreaterThanOrEqualTo(0).WithErrorCode(ApplicationErrors.Validation.INVALID_FORMAT); + .GreaterThanOrEqualTo(0).WithErrorCode(MessageKeys.Validation.INVALID_FORMAT); RuleFor(x => x.TopicId) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMap/UpdateInteractiveMapCommandValidator.cs b/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMap/UpdateInteractiveMapCommandValidator.cs index 978d48d9..6a6c30c9 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMap/UpdateInteractiveMapCommandValidator.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMap/UpdateInteractiveMapCommandValidator.cs @@ -1,4 +1,4 @@ -using CCE.Application.Errors; +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.InteractiveMaps.Commands.UpdateInteractiveMap; @@ -8,14 +8,14 @@ internal sealed class UpdateInteractiveMapCommandValidator : AbstractValidator x.NameAr) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(256).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(256).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.NameEn) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(256).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(256).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.DescriptionAr) - .MaximumLength(512).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .MaximumLength(512).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.DescriptionEn) - .MaximumLength(512).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .MaximumLength(512).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMapNode/UpdateInteractiveMapNodeCommandValidator.cs b/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMapNode/UpdateInteractiveMapNodeCommandValidator.cs index d02a99fc..91e0c5b8 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMapNode/UpdateInteractiveMapNodeCommandValidator.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMapNode/UpdateInteractiveMapNodeCommandValidator.cs @@ -1,4 +1,4 @@ -using CCE.Application.Errors; +using CCE.Application.Messages; using FluentValidation; namespace CCE.Application.InteractiveMaps.Commands.UpdateInteractiveMapNode; @@ -8,17 +8,17 @@ internal sealed class UpdateInteractiveMapNodeCommandValidator : AbstractValidat public UpdateInteractiveMapNodeCommandValidator() { RuleFor(x => x.NameAr) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(256).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(256).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.NameEn) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(256).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(256).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.IconKey) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD) - .MaximumLength(128).WithErrorCode(ApplicationErrors.Validation.MAX_LENGTH); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD) + .MaximumLength(128).WithErrorCode(MessageKeys.Validation.MAX_LENGTH); RuleFor(x => x.Level) - .GreaterThanOrEqualTo(0).WithErrorCode(ApplicationErrors.Validation.INVALID_FORMAT); + .GreaterThanOrEqualTo(0).WithErrorCode(MessageKeys.Validation.INVALID_FORMAT); RuleFor(x => x.TopicId) - .NotEmpty().WithErrorCode(ApplicationErrors.Validation.REQUIRED_FIELD); + .NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapById/GetInteractiveMapBySlugQueryHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapById/GetInteractiveMapBySlugQueryHandler.cs index 550e4a06..57df621d 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapById/GetInteractiveMapBySlugQueryHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapById/GetInteractiveMapBySlugQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.InteractiveMaps.Public.Dtos; using CCE.Application.Messages; @@ -43,6 +43,6 @@ public async Task> Handle( PublicInteractiveMapDto.FromEntity( map, nodes.Select(PublicInteractiveMapNodeDto.FromEntity).ToList()), - "ITEMS_LISTED"); + MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapNodeDetails/GetInteractiveMapNodeDetailsQueryHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapNodeDetails/GetInteractiveMapNodeDetailsQueryHandler.cs index c741b9e8..dd206399 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapNodeDetails/GetInteractiveMapNodeDetailsQueryHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapNodeDetails/GetInteractiveMapNodeDetailsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.InteractiveMaps.Public.Dtos; using CCE.Application.Messages; @@ -132,6 +132,6 @@ public async Task> Handle( Events: events, Posts: posts); - return _msg.Ok(dto, "ITEMS_LISTED"); + return _msg.Ok(dto, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Public/Queries/ListInteractiveMaps/ListInteractiveMapsQueryHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Public/Queries/ListInteractiveMaps/ListInteractiveMapsQueryHandler.cs index 9dc4e76c..d8d41653 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Public/Queries/ListInteractiveMaps/ListInteractiveMapsQueryHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Public/Queries/ListInteractiveMaps/ListInteractiveMapsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.InteractiveMaps.Public.Dtos; using CCE.Application.Messages; @@ -43,6 +43,6 @@ public async Task>> Handle( PublicInteractiveMapDto.FromEntity(m, nodesByMapId.GetValueOrDefault(m.Id) ?? []) ).ToList(); - return _msg.Ok(dtos as IReadOnlyList, "ITEMS_LISTED"); + return _msg.Ok(dtos as IReadOnlyList, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Queries/GetInteractiveMapById/GetInteractiveMapByIdQueryHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Queries/GetInteractiveMapById/GetInteractiveMapByIdQueryHandler.cs index af41d982..4bf81e82 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Queries/GetInteractiveMapById/GetInteractiveMapByIdQueryHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Queries/GetInteractiveMapById/GetInteractiveMapByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.InteractiveMaps.Dtos; using CCE.Application.Messages; @@ -38,6 +38,6 @@ public async Task> Handle( if (dto is null) return _msg.MapNotFound(); - return _msg.Ok(dto, "ITEMS_LISTED"); + return _msg.Ok(dto, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Queries/ListInteractiveMapNodes/ListInteractiveMapNodesQueryHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Queries/ListInteractiveMapNodes/ListInteractiveMapNodesQueryHandler.cs index d09779e4..99ad26b1 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Queries/ListInteractiveMapNodes/ListInteractiveMapNodesQueryHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Queries/ListInteractiveMapNodes/ListInteractiveMapNodesQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.InteractiveMaps.Dtos; @@ -33,7 +33,7 @@ public async Task>> Handle( .ThenBy(n => n.Level); var result = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); - return _msg.Ok(result.Map(MapToDto), "ITEMS_LISTED"); + return _msg.Ok(result.Map(MapToDto), MessageKeys.General.ITEMS_LISTED); } internal static InteractiveMapNodeDto MapToDto(InteractiveMapNode n) => new( diff --git a/backend/src/CCE.Application/InteractiveMaps/Queries/ListInteractiveMaps/ListInteractiveMapsQueryHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Queries/ListInteractiveMaps/ListInteractiveMapsQueryHandler.cs index b1c9ee62..c947bc88 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Queries/ListInteractiveMaps/ListInteractiveMapsQueryHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Queries/ListInteractiveMaps/ListInteractiveMapsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.InteractiveMaps.Dtos; @@ -29,7 +29,7 @@ public async Task>> Handle( .OrderBy(m => m.NameEn); var result = await query.ToPagedResultAsync(request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); - return _msg.Ok(result.Map(MapToDto), "ITEMS_LISTED"); + return _msg.Ok(result.Map(MapToDto), MessageKeys.General.ITEMS_LISTED); } internal static InteractiveMapDto MapToDto(InteractiveMap m) => new( diff --git a/backend/src/CCE.Application/InterestManagement/Queries/GetInterestQuestions/GetInterestQuestionsQueryHandler.cs b/backend/src/CCE.Application/InterestManagement/Queries/GetInterestQuestions/GetInterestQuestionsQueryHandler.cs index 716031f1..372ca1dd 100644 --- a/backend/src/CCE.Application/InterestManagement/Queries/GetInterestQuestions/GetInterestQuestionsQueryHandler.cs +++ b/backend/src/CCE.Application/InterestManagement/Queries/GetInterestQuestions/GetInterestQuestionsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.InterestManagement.Dtos; @@ -56,6 +56,6 @@ public async Task>> Handle( .ToList()), }; - return _msg.Ok>(questions, "SUCCESS_OPERATION"); + return _msg.Ok>(questions, MessageKeys.General.SUCCESS_OPERATION); } } \ No newline at end of file diff --git a/backend/src/CCE.Application/InterestManagement/Queries/ListInterestTopics/ListInterestTopicsQueryHandler.cs b/backend/src/CCE.Application/InterestManagement/Queries/ListInterestTopics/ListInterestTopicsQueryHandler.cs index 633d5815..0b97964c 100644 --- a/backend/src/CCE.Application/InterestManagement/Queries/ListInterestTopics/ListInterestTopicsQueryHandler.cs +++ b/backend/src/CCE.Application/InterestManagement/Queries/ListInterestTopics/ListInterestTopicsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.InterestManagement.Dtos; @@ -28,6 +28,6 @@ public async Task>> Handle( .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); - return _msg.Ok>(topics, "SUCCESS_OPERATION"); + return _msg.Ok>(topics, MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/Kapsarc/Queries/GetLatestKapsarcSnapshot/GetLatestKapsarcSnapshotQuery.cs b/backend/src/CCE.Application/Kapsarc/Queries/GetLatestKapsarcSnapshot/GetLatestKapsarcSnapshotQuery.cs index f8e35127..f8e3ab8c 100644 --- a/backend/src/CCE.Application/Kapsarc/Queries/GetLatestKapsarcSnapshot/GetLatestKapsarcSnapshotQuery.cs +++ b/backend/src/CCE.Application/Kapsarc/Queries/GetLatestKapsarcSnapshot/GetLatestKapsarcSnapshotQuery.cs @@ -1,7 +1,8 @@ +using CCE.Application.Common; using CCE.Application.Kapsarc.Dtos; using MediatR; namespace CCE.Application.Kapsarc.Queries.GetLatestKapsarcSnapshot; public sealed record GetLatestKapsarcSnapshotQuery(System.Guid CountryId) - : IRequest; + : IRequest>; diff --git a/backend/src/CCE.Application/Kapsarc/Queries/GetLatestKapsarcSnapshot/GetLatestKapsarcSnapshotQueryHandler.cs b/backend/src/CCE.Application/Kapsarc/Queries/GetLatestKapsarcSnapshot/GetLatestKapsarcSnapshotQueryHandler.cs index 4b405476..86e46567 100644 --- a/backend/src/CCE.Application/Kapsarc/Queries/GetLatestKapsarcSnapshot/GetLatestKapsarcSnapshotQueryHandler.cs +++ b/backend/src/CCE.Application/Kapsarc/Queries/GetLatestKapsarcSnapshot/GetLatestKapsarcSnapshotQueryHandler.cs @@ -1,22 +1,26 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Kapsarc.Dtos; +using CCE.Application.Messages; using CCE.Domain.Country; using MediatR; namespace CCE.Application.Kapsarc.Queries.GetLatestKapsarcSnapshot; public sealed class GetLatestKapsarcSnapshotQueryHandler - : IRequestHandler + : IRequestHandler> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public GetLatestKapsarcSnapshotQueryHandler(ICceDbContext db) + public GetLatestKapsarcSnapshotQueryHandler(ICceDbContext db, MessageFactory msg) { _db = db; + _msg = msg; } - public async Task Handle( + public async Task> Handle( GetLatestKapsarcSnapshotQuery request, CancellationToken cancellationToken) { @@ -29,7 +33,10 @@ public GetLatestKapsarcSnapshotQueryHandler(ICceDbContext db) .OrderByDescending(s => s.SnapshotTakenOn) .FirstOrDefault(); - return latest is null ? null : MapToDto(latest); + if (latest is null) + return _msg.NotFound(MessageKeys.Country.KAPSARC_DATA_UNAVAILABLE); + + return _msg.Ok(MapToDto(latest), MessageKeys.General.SUCCESS_OPERATION); } internal static KapsarcSnapshotDto MapToDto(CountryKapsarcSnapshot s) => new( diff --git a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/GetKnowledgeMapById/GetKnowledgeMapByIdQuery.cs b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/GetKnowledgeMapById/GetKnowledgeMapByIdQuery.cs index 353cf796..ea7443f9 100644 --- a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/GetKnowledgeMapById/GetKnowledgeMapByIdQuery.cs +++ b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/GetKnowledgeMapById/GetKnowledgeMapByIdQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.KnowledgeMaps.Public.Dtos; using MediatR; namespace CCE.Application.KnowledgeMaps.Public.Queries.GetKnowledgeMapById; -public sealed record GetKnowledgeMapByIdQuery(System.Guid Id) : IRequest; +public sealed record GetKnowledgeMapByIdQuery(System.Guid Id) : IRequest>; diff --git a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/GetKnowledgeMapById/GetKnowledgeMapByIdQueryHandler.cs b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/GetKnowledgeMapById/GetKnowledgeMapByIdQueryHandler.cs index c11172fe..09f4cec7 100644 --- a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/GetKnowledgeMapById/GetKnowledgeMapByIdQueryHandler.cs +++ b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/GetKnowledgeMapById/GetKnowledgeMapByIdQueryHandler.cs @@ -1,19 +1,27 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; +using CCE.Application.Messages; using CCE.Application.KnowledgeMaps.Public.Dtos; using CCE.Application.KnowledgeMaps.Public.Queries.ListKnowledgeMaps; + using MediatR; namespace CCE.Application.KnowledgeMaps.Public.Queries.GetKnowledgeMapById; public sealed class GetKnowledgeMapByIdQueryHandler - : IRequestHandler + : IRequestHandler> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public GetKnowledgeMapByIdQueryHandler(ICceDbContext db) => _db = db; + public GetKnowledgeMapByIdQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task Handle( + public async Task> Handle( GetKnowledgeMapByIdQuery request, CancellationToken cancellationToken) { var list = await _db.KnowledgeMaps @@ -22,6 +30,8 @@ public sealed class GetKnowledgeMapByIdQueryHandler .ConfigureAwait(false); var map = list.SingleOrDefault(); - return map is null ? null : ListKnowledgeMapsQueryHandler.MapToDto(map); + if (map is null) + return _msg.NotFound(MessageKeys.KnowledgeMap.MAP_NOT_FOUND); + return _msg.Ok(ListKnowledgeMapsQueryHandler.MapToDto(map), MessageKeys.General.SUCCESS_OPERATION); } } diff --git a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapEdges/ListKnowledgeMapEdgesQuery.cs b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapEdges/ListKnowledgeMapEdgesQuery.cs index 4b6ca5ba..53ee644e 100644 --- a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapEdges/ListKnowledgeMapEdgesQuery.cs +++ b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapEdges/ListKnowledgeMapEdgesQuery.cs @@ -1,7 +1,8 @@ +using CCE.Application.Common; using CCE.Application.KnowledgeMaps.Public.Dtos; using MediatR; namespace CCE.Application.KnowledgeMaps.Public.Queries.ListKnowledgeMapEdges; public sealed record ListKnowledgeMapEdgesQuery(System.Guid MapId) - : IRequest>; + : IRequest>>; diff --git a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapEdges/ListKnowledgeMapEdgesQueryHandler.cs b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapEdges/ListKnowledgeMapEdgesQueryHandler.cs index 8f905da1..784042c6 100644 --- a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapEdges/ListKnowledgeMapEdgesQueryHandler.cs +++ b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapEdges/ListKnowledgeMapEdgesQueryHandler.cs @@ -1,19 +1,26 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.KnowledgeMaps.Public.Dtos; +using CCE.Application.Messages; using CCE.Domain.KnowledgeMaps; using MediatR; namespace CCE.Application.KnowledgeMaps.Public.Queries.ListKnowledgeMapEdges; public sealed class ListKnowledgeMapEdgesQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListKnowledgeMapEdgesQueryHandler(ICceDbContext db) => _db = db; + public ListKnowledgeMapEdgesQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task> Handle( + public async Task>> Handle( ListKnowledgeMapEdgesQuery request, CancellationToken cancellationToken) { var items = await _db.KnowledgeMapEdges @@ -22,7 +29,8 @@ public async Task> Handle( .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); - return items.Select(MapToDto).ToList(); + IReadOnlyList list = items.Select(MapToDto).ToList(); + return _msg.Ok(list, MessageKeys.General.ITEMS_LISTED); } internal static PublicKnowledgeMapEdgeDto MapToDto(KnowledgeMapEdge e) => new( diff --git a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapNodes/ListKnowledgeMapNodesQuery.cs b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapNodes/ListKnowledgeMapNodesQuery.cs index fabdab36..58f43df5 100644 --- a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapNodes/ListKnowledgeMapNodesQuery.cs +++ b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapNodes/ListKnowledgeMapNodesQuery.cs @@ -1,7 +1,8 @@ +using CCE.Application.Common; using CCE.Application.KnowledgeMaps.Public.Dtos; using MediatR; namespace CCE.Application.KnowledgeMaps.Public.Queries.ListKnowledgeMapNodes; public sealed record ListKnowledgeMapNodesQuery(System.Guid MapId) - : IRequest>; + : IRequest>>; diff --git a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapNodes/ListKnowledgeMapNodesQueryHandler.cs b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapNodes/ListKnowledgeMapNodesQueryHandler.cs index 72aafe5d..f7d4a6a6 100644 --- a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapNodes/ListKnowledgeMapNodesQueryHandler.cs +++ b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMapNodes/ListKnowledgeMapNodesQueryHandler.cs @@ -1,19 +1,26 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.KnowledgeMaps.Public.Dtos; +using CCE.Application.Messages; using CCE.Domain.KnowledgeMaps; using MediatR; namespace CCE.Application.KnowledgeMaps.Public.Queries.ListKnowledgeMapNodes; public sealed class ListKnowledgeMapNodesQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListKnowledgeMapNodesQueryHandler(ICceDbContext db) => _db = db; + public ListKnowledgeMapNodesQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task> Handle( + public async Task>> Handle( ListKnowledgeMapNodesQuery request, CancellationToken cancellationToken) { var items = await _db.KnowledgeMapNodes @@ -22,7 +29,8 @@ public async Task> Handle( .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); - return items.Select(MapToDto).ToList(); + IReadOnlyList list = items.Select(MapToDto).ToList(); + return _msg.Ok(list, MessageKeys.General.ITEMS_LISTED); } internal static PublicKnowledgeMapNodeDto MapToDto(KnowledgeMapNode n) => new( diff --git a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMaps/ListKnowledgeMapsQuery.cs b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMaps/ListKnowledgeMapsQuery.cs index c6457e79..916da9e6 100644 --- a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMaps/ListKnowledgeMapsQuery.cs +++ b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMaps/ListKnowledgeMapsQuery.cs @@ -1,6 +1,7 @@ +using CCE.Application.Common; using CCE.Application.KnowledgeMaps.Public.Dtos; using MediatR; namespace CCE.Application.KnowledgeMaps.Public.Queries.ListKnowledgeMaps; -public sealed record ListKnowledgeMapsQuery : IRequest>; +public sealed record ListKnowledgeMapsQuery : IRequest>>; diff --git a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMaps/ListKnowledgeMapsQueryHandler.cs b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMaps/ListKnowledgeMapsQueryHandler.cs index 313efbeb..551d01b8 100644 --- a/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMaps/ListKnowledgeMapsQueryHandler.cs +++ b/backend/src/CCE.Application/KnowledgeMaps/Public/Queries/ListKnowledgeMaps/ListKnowledgeMapsQueryHandler.cs @@ -1,19 +1,26 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.KnowledgeMaps.Public.Dtos; +using CCE.Application.Messages; using CCE.Domain.KnowledgeMaps; using MediatR; namespace CCE.Application.KnowledgeMaps.Public.Queries.ListKnowledgeMaps; public sealed class ListKnowledgeMapsQueryHandler - : IRequestHandler> + : IRequestHandler>> { private readonly ICceDbContext _db; + private readonly MessageFactory _msg; - public ListKnowledgeMapsQueryHandler(ICceDbContext db) => _db = db; + public ListKnowledgeMapsQueryHandler(ICceDbContext db, MessageFactory msg) + { + _db = db; + _msg = msg; + } - public async Task> Handle( + public async Task>> Handle( ListKnowledgeMapsQuery request, CancellationToken cancellationToken) { var items = await _db.KnowledgeMaps @@ -21,7 +28,8 @@ public async Task> Handle( .ToListAsyncEither(cancellationToken) .ConfigureAwait(false); - return items.Select(MapToDto).ToList(); + IReadOnlyList list = items.Select(MapToDto).ToList(); + return _msg.Ok(list, MessageKeys.General.ITEMS_LISTED); } internal static PublicKnowledgeMapDto MapToDto(KnowledgeMap m) => new( diff --git a/backend/src/CCE.Application/Lookups/Commands/UpsertCountryCode/UpsertCountryCodeCommandHandler.cs b/backend/src/CCE.Application/Lookups/Commands/UpsertCountryCode/UpsertCountryCodeCommandHandler.cs index b27eb514..f5dee64f 100644 --- a/backend/src/CCE.Application/Lookups/Commands/UpsertCountryCode/UpsertCountryCodeCommandHandler.cs +++ b/backend/src/CCE.Application/Lookups/Commands/UpsertCountryCode/UpsertCountryCodeCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -41,7 +41,7 @@ public async Task> Handle( await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); return _msg.Ok( CCE.Application.Lookups.Queries.ListCountryCodes.ListCountryCodesQueryHandler.MapToDto(entity), - "LOOKUP_CREATED"); + MessageKeys.Lookups.LOOKUP_CREATED); } else { @@ -53,7 +53,7 @@ public async Task> Handle( await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); return _msg.Ok( CCE.Application.Lookups.Queries.ListCountryCodes.ListCountryCodesQueryHandler.MapToDto(entity), - "LOOKUP_UPDATED"); + MessageKeys.Lookups.LOOKUP_UPDATED); } } } diff --git a/backend/src/CCE.Application/Lookups/Queries/GetCountryCodeById/GetCountryCodeByIdQueryHandler.cs b/backend/src/CCE.Application/Lookups/Queries/GetCountryCodeById/GetCountryCodeByIdQueryHandler.cs index f28d8af8..cbd63cb2 100644 --- a/backend/src/CCE.Application/Lookups/Queries/GetCountryCodeById/GetCountryCodeByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Lookups/Queries/GetCountryCodeById/GetCountryCodeByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -29,6 +29,6 @@ public async Task> Handle( var entity = list.SingleOrDefault(); return entity is null ? _msg.CountryCodeNotFound() - : _msg.Ok(CCE.Application.Lookups.Queries.ListCountryCodes.ListCountryCodesQueryHandler.MapToDto(entity), "ITEMS_LISTED"); + : _msg.Ok(CCE.Application.Lookups.Queries.ListCountryCodes.ListCountryCodesQueryHandler.MapToDto(entity), MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Lookups/Queries/ListCountryCodes/ListCountryCodesQueryHandler.cs b/backend/src/CCE.Application/Lookups/Queries/ListCountryCodes/ListCountryCodesQueryHandler.cs index b9a3333f..994b734c 100644 --- a/backend/src/CCE.Application/Lookups/Queries/ListCountryCodes/ListCountryCodesQueryHandler.cs +++ b/backend/src/CCE.Application/Lookups/Queries/ListCountryCodes/ListCountryCodesQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -44,7 +44,7 @@ public async Task>> Handle( .ConfigureAwait(false); IReadOnlyList dtos = items.Select(MapToDto).ToList(); - return _msg.Ok(dtos, "ITEMS_LISTED"); + return _msg.Ok(dtos, MessageKeys.General.ITEMS_LISTED); } internal static CountryCodeDto MapToDto(CCE.Domain.Country.Country c) => diff --git a/backend/src/CCE.Application/Media/Commands/DeleteMedia/DeleteMediaCommandHandler.cs b/backend/src/CCE.Application/Media/Commands/DeleteMedia/DeleteMediaCommandHandler.cs index 5c911f5c..2a431d72 100644 --- a/backend/src/CCE.Application/Media/Commands/DeleteMedia/DeleteMediaCommandHandler.cs +++ b/backend/src/CCE.Application/Media/Commands/DeleteMedia/DeleteMediaCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content; using CCE.Application.Media.Dtos; @@ -41,6 +41,6 @@ public async Task> Handle( await _db.SaveChangesAsync(ct).ConfigureAwait(false); var dto = new MediaFileBriefDto(mediaFile.Id, mediaFile.StorageKey, mediaFile.Url); - return _msg.Ok(dto, "MEDIA_DELETED"); + return _msg.Ok(dto, MessageKeys.Media.MEDIA_DELETED); } } diff --git a/backend/src/CCE.Application/Media/Commands/UpdateMediaMetadata/UpdateMediaMetadataCommandHandler.cs b/backend/src/CCE.Application/Media/Commands/UpdateMediaMetadata/UpdateMediaMetadataCommandHandler.cs index a3d57e87..ee51bb5b 100644 --- a/backend/src/CCE.Application/Media/Commands/UpdateMediaMetadata/UpdateMediaMetadataCommandHandler.cs +++ b/backend/src/CCE.Application/Media/Commands/UpdateMediaMetadata/UpdateMediaMetadataCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Media.Dtos; using CCE.Application.Messages; @@ -41,6 +41,6 @@ public async Task> Handle( await _db.SaveChangesAsync(ct).ConfigureAwait(false); var dto = new MediaFileBriefDto(mediaFile.Id, mediaFile.StorageKey, mediaFile.Url); - return _msg.Ok(dto, "MEDIA_UPDATED"); + return _msg.Ok(dto, MessageKeys.Media.MEDIA_UPDATED); } } diff --git a/backend/src/CCE.Application/Media/Commands/UploadMedia/UploadMediaCommandHandler.cs b/backend/src/CCE.Application/Media/Commands/UploadMedia/UploadMediaCommandHandler.cs index bfc41cd3..ad79bf47 100644 --- a/backend/src/CCE.Application/Media/Commands/UploadMedia/UploadMediaCommandHandler.cs +++ b/backend/src/CCE.Application/Media/Commands/UploadMedia/UploadMediaCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content; using CCE.Application.Media.Dtos; @@ -83,6 +83,6 @@ public async Task> Handle( await _db.SaveChangesAsync(ct).ConfigureAwait(false); var dto = new MediaFileBriefDto(mediaFile.Id, mediaFile.StorageKey, mediaFile.Url); - return _msg.Ok(dto, "MEDIA_UPLOADED"); + return _msg.Ok(dto, MessageKeys.Media.MEDIA_UPLOADED); } } diff --git a/backend/src/CCE.Application/Media/Queries/GetMediaById/GetMediaByIdQueryHandler.cs b/backend/src/CCE.Application/Media/Queries/GetMediaById/GetMediaByIdQueryHandler.cs index c2b86971..0b8c8c43 100644 --- a/backend/src/CCE.Application/Media/Queries/GetMediaById/GetMediaByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Media/Queries/GetMediaById/GetMediaByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Media.Dtos; using CCE.Application.Messages; using MediatR; @@ -26,6 +26,6 @@ public async Task> Handle( if (mediaFile is null) return _msg.MediaFileNotFound(); - return _msg.Ok(MediaFileDto.FromEntity(mediaFile), "ITEMS_LISTED"); + return _msg.Ok(MediaFileDto.FromEntity(mediaFile), MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Messages/MessageFactory.cs b/backend/src/CCE.Application/Messages/MessageFactory.cs index d4188499..de63fc6f 100644 --- a/backend/src/CCE.Application/Messages/MessageFactory.cs +++ b/backend/src/CCE.Application/Messages/MessageFactory.cs @@ -1,5 +1,4 @@ -using CCE.Application.Common; -using CCE.Application.Errors; +using CCE.Application.Common; using CCE.Application.Localization; using CCE.Domain.Common; using Microsoft.Extensions.Logging; @@ -55,6 +54,9 @@ public Response Forbidden(string domainKey) public Response BusinessRule(string domainKey) => Fail(domainKey, MessageType.BusinessRule); + // For domain-level validation that produces named field errors (e.g. business rules on + // a multi-field object). FluentValidation schema failures go through ExceptionHandlingMiddleware + // instead and never reach this overload. public Response ValidationError( string domainKey, IReadOnlyList fieldErrors) { @@ -74,105 +76,105 @@ public FieldError Field(string fieldName, string domainKey) // ─── Convenience shortcuts (Identity domain) ─── - public Response UserNotFound() => NotFound(ApplicationErrors.Identity.USER_NOT_FOUND); - public Response InterestUpserted(T data) => Ok(data, "INTEREST_UPSERTED"); - public Response EmailExists() => Conflict(ApplicationErrors.Identity.EMAIL_EXISTS); - public Response InvalidCredentials() => Unauthorized(ApplicationErrors.Identity.INVALID_CREDENTIALS); - public Response NotAuthenticated() => Unauthorized(ApplicationErrors.Identity.NOT_AUTHENTICATED); - public Response AccountDeactivated() => Forbidden(ApplicationErrors.Identity.ACCOUNT_DEACTIVATED); - public Response ContactNotVerified() => Forbidden(ApplicationErrors.Identity.CONTACT_NOT_VERIFIED); + public Response UserNotFound() => NotFound(MessageKeys.Identity.USER_NOT_FOUND); + public Response InterestUpserted(T data) => Ok(data, MessageKeys.Identity.INTEREST_UPSERTED); + public Response EmailExists() => Conflict(MessageKeys.Identity.EMAIL_EXISTS); + public Response InvalidCredentials() => Unauthorized(MessageKeys.Identity.INVALID_CREDENTIALS); + public Response NotAuthenticated() => Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); + public Response AccountDeactivated() => Forbidden(MessageKeys.Identity.ACCOUNT_DEACTIVATED); + public Response ContactNotVerified() => Forbidden(MessageKeys.Identity.CONTACT_NOT_VERIFIED); // ─── Convenience shortcuts (Content domain) ─── - 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 TopicNotFound() => NotFound("TOPIC_NOT_FOUND"); + public Response NewsNotFound() => NotFound(MessageKeys.Content.NEWS_NOT_FOUND); + public Response EventNotFound() => NotFound(MessageKeys.Content.EVENT_NOT_FOUND); + public Response ResourceNotFound() => NotFound(MessageKeys.Content.RESOURCE_NOT_FOUND); + public Response PageNotFound() => NotFound(MessageKeys.Content.PAGE_NOT_FOUND); + public Response TopicNotFound() => NotFound(MessageKeys.Community.TOPIC_NOT_FOUND); public Response CannotFollowSelf() => ValidationError( - ApplicationErrors.Community.CANNOT_FOLLOW_SELF, - new[] { Field("userId", ApplicationErrors.Community.CANNOT_FOLLOW_SELF) }); - public Response CategoryNotFound() => NotFound("CATEGORY_NOT_FOUND"); - public Response AssetNotFound() => NotFound("ASSET_NOT_FOUND"); - public Response AssetNotClean() => BusinessRule("ASSET_NOT_CLEAN"); + MessageKeys.Community.CANNOT_FOLLOW_SELF, + new[] { Field("userId", MessageKeys.Community.CANNOT_FOLLOW_SELF) }); + public Response CategoryNotFound() => NotFound(MessageKeys.Content.CATEGORY_NOT_FOUND); + public Response AssetNotFound() => NotFound(MessageKeys.Content.ASSET_NOT_FOUND); + public Response AssetNotClean() => BusinessRule(MessageKeys.Content.ASSET_NOT_CLEAN); // ─── Convenience shortcuts (Identity / Expert domain) ─── - public Response ExpertRequestNotFound() => NotFound(ApplicationErrors.Identity.EXPERT_REQUEST_NOT_FOUND); + public Response ExpertRequestNotFound() => NotFound(MessageKeys.Identity.EXPERT_REQUEST_NOT_FOUND); // ─── Convenience shortcuts (Platform Settings domain) ─── - public Response HomepageSettingsNotFound() => NotFound(ApplicationErrors.PlatformSettings.HOMEPAGE_SETTINGS_NOT_FOUND); - public Response AboutSettingsNotFound() => NotFound(ApplicationErrors.PlatformSettings.ABOUT_SETTINGS_NOT_FOUND); - public Response PoliciesSettingsNotFound() => NotFound(ApplicationErrors.PlatformSettings.POLICIES_SETTINGS_NOT_FOUND); - public Response GlossaryEntryNotFound() => NotFound(ApplicationErrors.PlatformSettings.GLOSSARY_ENTRY_NOT_FOUND); - public Response KnowledgePartnerNotFound() => NotFound(ApplicationErrors.PlatformSettings.KNOWLEDGE_PARTNER_NOT_FOUND); - public Response PolicySectionNotFound() => NotFound(ApplicationErrors.PlatformSettings.POLICY_SECTION_NOT_FOUND); - public Response ContentUpdateFailed() => BusinessRule(ApplicationErrors.PlatformSettings.CONTENT_UPDATE_FAILED); + public Response HomepageSettingsNotFound() => NotFound(MessageKeys.PlatformSettings.HOMEPAGE_SETTINGS_NOT_FOUND); + public Response AboutSettingsNotFound() => NotFound(MessageKeys.PlatformSettings.ABOUT_SETTINGS_NOT_FOUND); + public Response PoliciesSettingsNotFound() => NotFound(MessageKeys.PlatformSettings.POLICIES_SETTINGS_NOT_FOUND); + public Response GlossaryEntryNotFound() => NotFound(MessageKeys.PlatformSettings.GLOSSARY_ENTRY_NOT_FOUND); + public Response KnowledgePartnerNotFound() => NotFound(MessageKeys.PlatformSettings.KNOWLEDGE_PARTNER_NOT_FOUND); + public Response PolicySectionNotFound() => NotFound(MessageKeys.PlatformSettings.POLICY_SECTION_NOT_FOUND); + public Response ContentUpdateFailed() => BusinessRule(MessageKeys.PlatformSettings.CONTENT_UPDATE_FAILED); // ─── Convenience shortcuts (Media domain) ─── - public Response MediaFileNotFound() => NotFound(ApplicationErrors.Media.MEDIA_FILE_NOT_FOUND); - public Response InvalidFileType() => BusinessRule(ApplicationErrors.Media.INVALID_FILE_TYPE); - public Response FileTooLarge() => BusinessRule(ApplicationErrors.Media.FILE_TOO_LARGE); - public Response EmptyFile() => BusinessRule(ApplicationErrors.Media.EMPTY_FILE); + public Response MediaFileNotFound() => NotFound(MessageKeys.Media.MEDIA_FILE_NOT_FOUND); + public Response InvalidFileType() => BusinessRule(MessageKeys.Media.INVALID_FILE_TYPE); + public Response FileTooLarge() => BusinessRule(MessageKeys.Media.FILE_TOO_LARGE); + public Response EmptyFile() => BusinessRule(MessageKeys.Media.EMPTY_FILE); // ─── Convenience shortcuts (Verification domain) ─── - public Response OtpNotFound() => NotFound(ApplicationErrors.Verification.OTP_NOT_FOUND); - public Response OtpExpired() => BusinessRule(ApplicationErrors.Verification.OTP_EXPIRED); - public Response OtpInvalidCode() => BusinessRule(ApplicationErrors.Verification.OTP_INVALID_CODE); - public Response OtpMaxAttempts() => BusinessRule(ApplicationErrors.Verification.OTP_MAX_ATTEMPTS); - public Response OtpCooldownActive() => BusinessRule(ApplicationErrors.Verification.OTP_COOLDOWN_ACTIVE); - public Response OtpInvalidated() => BusinessRule(ApplicationErrors.Verification.OTP_INVALIDATED); - public Response ContactAlreadyTaken() => Conflict(ApplicationErrors.Verification.CONTACT_ALREADY_TAKEN); - public Response EmailUpdated() => Ok(ApplicationErrors.Verification.EMAIL_UPDATED); - public Response PhoneUpdated() => Ok(ApplicationErrors.Verification.PHONE_UPDATED); + public Response OtpNotFound() => NotFound(MessageKeys.Verification.OTP_NOT_FOUND); + public Response OtpExpired() => BusinessRule(MessageKeys.Verification.OTP_EXPIRED); + public Response OtpInvalidCode() => BusinessRule(MessageKeys.Verification.OTP_INVALID_CODE); + public Response OtpMaxAttempts() => BusinessRule(MessageKeys.Verification.OTP_MAX_ATTEMPTS); + public Response OtpCooldownActive() => BusinessRule(MessageKeys.Verification.OTP_COOLDOWN_ACTIVE); + public Response OtpInvalidated() => BusinessRule(MessageKeys.Verification.OTP_INVALIDATED); + public Response ContactAlreadyTaken() => Conflict(MessageKeys.Verification.CONTACT_ALREADY_TAKEN); + public Response EmailUpdated() => Ok(MessageKeys.Verification.EMAIL_UPDATED); + public Response PhoneUpdated() => Ok(MessageKeys.Verification.PHONE_UPDATED); // ─── Convenience shortcuts (Country domain) ─── - public Response CountryNotFound() => NotFound(ApplicationErrors.Country.COUNTRY_NOT_FOUND); - public Response CountryProfileNotFound() => NotFound(ApplicationErrors.Country.COUNTRY_PROFILE_NOT_FOUND); - public Response NoCountryAssigned() => NotFound(ApplicationErrors.Country.NO_COUNTRY_ASSIGNED); - public Response CountryScopeForbidden() => Forbidden(ApplicationErrors.Country.COUNTRY_SCOPE_FORBIDDEN); - public Response CountryContentRequestNotFound() => NotFound(ApplicationErrors.Content.COUNTRY_RESOURCE_REQUEST_NOT_FOUND); - public Response CountryRequestProcessed(T data) => Ok(data, ApplicationErrors.Content.COUNTRY_REQUEST_PROCESSED); - public Response CountryRequestProcessingFailed() => BusinessRule(ApplicationErrors.Content.COUNTRY_REQUEST_PROCESSING_FAILED); - public Response KapsarcDataUnavailable() => BusinessRule(ApplicationErrors.Country.KAPSARC_DATA_UNAVAILABLE); - public Response KapsarcSnapshotRefreshed(T data) => Ok(data, ApplicationErrors.Country.KAPSARC_SNAPSHOT_REFRESHED); + public Response CountryNotFound() => NotFound(MessageKeys.Country.COUNTRY_NOT_FOUND); + public Response CountryProfileNotFound() => NotFound(MessageKeys.Country.COUNTRY_PROFILE_NOT_FOUND); + public Response NoCountryAssigned() => NotFound(MessageKeys.Country.NO_COUNTRY_ASSIGNED); + public Response CountryScopeForbidden() => Forbidden(MessageKeys.Country.COUNTRY_SCOPE_FORBIDDEN); + public Response CountryContentRequestNotFound() => NotFound(MessageKeys.Content.COUNTRY_RESOURCE_REQUEST_NOT_FOUND); + public Response CountryRequestProcessed(T data) => Ok(data, MessageKeys.Content.COUNTRY_REQUEST_PROCESSED); + public Response CountryRequestProcessingFailed() => BusinessRule(MessageKeys.Content.COUNTRY_REQUEST_PROCESSING_FAILED); + public Response KapsarcDataUnavailable() => BusinessRule(MessageKeys.Country.KAPSARC_DATA_UNAVAILABLE); + public Response KapsarcSnapshotRefreshed(T data) => Ok(data, MessageKeys.Country.KAPSARC_SNAPSHOT_REFRESHED); // ─── Convenience shortcuts (InteractiveMaps domain) ─── - public Response MapNotFound() => NotFound(ApplicationErrors.InteractiveMaps.MAP_NOT_FOUND); - public Response MapCreated() => Ok(ApplicationErrors.InteractiveMaps.MAP_CREATED); - public Response MapUpdated() => Ok(ApplicationErrors.InteractiveMaps.MAP_UPDATED); - public Response MapDeleted() => Ok(ApplicationErrors.InteractiveMaps.MAP_DELETED); - public Response NodeNotFound() => NotFound(ApplicationErrors.InteractiveMaps.NODE_NOT_FOUND); - public Response NodeCreated() => Ok(ApplicationErrors.InteractiveMaps.NODE_CREATED); - public Response NodeUpdated() => Ok(ApplicationErrors.InteractiveMaps.NODE_UPDATED); - public Response NodeDeleted() => Ok(ApplicationErrors.InteractiveMaps.NODE_DELETED); + public Response MapNotFound() => NotFound(MessageKeys.InteractiveMaps.MAP_NOT_FOUND); + public Response MapCreated() => Ok(MessageKeys.InteractiveMaps.MAP_CREATED); + public Response MapUpdated() => Ok(MessageKeys.InteractiveMaps.MAP_UPDATED); + public Response MapDeleted() => Ok(MessageKeys.InteractiveMaps.MAP_DELETED); + public Response NodeNotFound() => NotFound(MessageKeys.InteractiveMaps.NODE_NOT_FOUND); + public Response NodeCreated() => Ok(MessageKeys.InteractiveMaps.NODE_CREATED); + public Response NodeUpdated() => Ok(MessageKeys.InteractiveMaps.NODE_UPDATED); + public Response NodeDeleted() => Ok(MessageKeys.InteractiveMaps.NODE_DELETED); // ─── Convenience shortcuts (Evaluation domain) ─── - public Response EvaluationSubmitted() => Ok(ApplicationErrors.Evaluation.EVALUATION_SUBMITTED); - public Response EvaluationNotFound() => NotFound(ApplicationErrors.Evaluation.EVALUATION_NOT_FOUND); + public Response EvaluationSubmitted() => Ok(MessageKeys.Evaluation.EVALUATION_SUBMITTED); + public Response EvaluationNotFound() => NotFound(MessageKeys.Evaluation.EVALUATION_NOT_FOUND); // ─── Convenience shortcuts (Notification domain) ─── - public Response NotificationTemplateNotFound() => NotFound(ApplicationErrors.Notifications.TEMPLATE_NOT_FOUND); - public Response NotificationLogNotFound() => NotFound(ApplicationErrors.Notifications.NOTIFICATION_NOT_FOUND); - public Response NotificationSettingsUpdated() => Ok(ApplicationErrors.Notifications.NOTIFICATION_SETTINGS_UPDATED); - public Response NotificationMarkedRead() => Ok(ApplicationErrors.Notifications.NOTIFICATION_MARKED_READ); - public Response NotificationsMarkedRead(int count) => Ok(count, ApplicationErrors.Notifications.NOTIFICATIONS_MARKED_READ); - public Response NotificationRetried(T data) => Ok(data, ApplicationErrors.Notifications.NOTIFICATION_RETRIED); - public Response NotificationTemplateCreated(T data) => Ok(data, ApplicationErrors.Notifications.NOTIFICATION_TEMPLATE_CREATED); - public Response NotificationTemplateUpdated(T data) => Ok(data, ApplicationErrors.Notifications.NOTIFICATION_TEMPLATE_UPDATED); + public Response NotificationTemplateNotFound() => NotFound(MessageKeys.Notifications.TEMPLATE_NOT_FOUND); + public Response NotificationLogNotFound() => NotFound(MessageKeys.Notifications.NOTIFICATION_NOT_FOUND); + public Response NotificationSettingsUpdated() => Ok(MessageKeys.Notifications.NOTIFICATION_SETTINGS_UPDATED); + public Response NotificationMarkedRead() => Ok(MessageKeys.Notifications.NOTIFICATION_MARKED_READ); + public Response NotificationsMarkedRead(int count) => Ok(count, MessageKeys.Notifications.NOTIFICATIONS_MARKED_READ); + public Response NotificationRetried(T data) => Ok(data, MessageKeys.Notifications.NOTIFICATION_RETRIED); + public Response NotificationTemplateCreated(T data) => Ok(data, MessageKeys.Notifications.NOTIFICATION_TEMPLATE_CREATED); + public Response NotificationTemplateUpdated(T data) => Ok(data, MessageKeys.Notifications.NOTIFICATION_TEMPLATE_UPDATED); // ─── Convenience shortcuts (Lookups domain) ─── - public Response CountryCodeNotFound() => NotFound(ApplicationErrors.Lookups.COUNTRY_CODE_NOT_FOUND); - public Response LookupCreated(T data) => Ok(data, ApplicationErrors.Lookups.LOOKUP_CREATED); - public Response LookupUpdated(T data) => Ok(data, ApplicationErrors.Lookups.LOOKUP_UPDATED); + public Response CountryCodeNotFound() => NotFound(MessageKeys.Lookups.COUNTRY_CODE_NOT_FOUND); + public Response LookupCreated(T data) => Ok(data, MessageKeys.Lookups.LOOKUP_CREATED); + public Response LookupUpdated(T data) => Ok(data, MessageKeys.Lookups.LOOKUP_UPDATED); // ─── Private ─── @@ -186,7 +188,7 @@ private Response Fail(string domainKey, MessageType type) private string ResolveCode(string domainKey) { var code = SystemCodeMap.ToSystemCode(domainKey); - if (code == SystemCode.ERR900 && domainKey != ApplicationErrors.General.INTERNAL_ERROR) + if (code == SystemCode.ERR900 && domainKey != MessageKeys.General.INTERNAL_ERROR) _logger.LogWarning("Domain key {DomainKey} has no SystemCodeMap entry and fell back to ERR900", domainKey); return code; } diff --git a/backend/src/CCE.Application/Errors/ApplicationErrors.cs b/backend/src/CCE.Application/Messages/MessageKeys.cs similarity index 74% rename from backend/src/CCE.Application/Errors/ApplicationErrors.cs rename to backend/src/CCE.Application/Messages/MessageKeys.cs index b91e60c8..28439ed1 100644 --- a/backend/src/CCE.Application/Errors/ApplicationErrors.cs +++ b/backend/src/CCE.Application/Messages/MessageKeys.cs @@ -1,6 +1,6 @@ -namespace CCE.Application.Errors; +namespace CCE.Application.Messages; -public static class ApplicationErrors +public static class MessageKeys { public static class General { @@ -8,13 +8,19 @@ public static class General public const string INTERNAL_ERROR = "INTERNAL_ERROR"; public const string UNAUTHORIZED = "UNAUTHORIZED_ACCESS"; public const string FORBIDDEN = "FORBIDDEN_ACCESS"; - public const string NOT_FOUND = "RESOURCE_NOT_FOUND"; + public const string RESOURCE_NOT_FOUND_GENERIC = "RESOURCE_NOT_FOUND_GENERIC"; public const string BAD_REQUEST = "BAD_REQUEST"; public const string SUCCESS_CREATED = "SUCCESS_CREATED"; public const string SUCCESS_UPDATED = "SUCCESS_UPDATED"; public const string SUCCESS_DELETED = "SUCCESS_DELETED"; public const string SUCCESS_OPERATION = "SUCCESS_OPERATION"; public const string DUPLICATE_VALUE = "DUPLICATE_VALUE"; + public const string CONCURRENCY_CONFLICT = "CONCURRENCY_CONFLICT"; + public const string EXTERNAL_API_ERROR = "EXTERNAL_API_ERROR"; + public const string EXTERNAL_API_NOT_CONFIGURED = "EXTERNAL_API_NOT_CONFIGURED"; + public const string RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"; + public const string BUSINESS_RULE_VIOLATION = "BUSINESS_RULE_VIOLATION"; + public const string ITEMS_LISTED = "ITEMS_LISTED"; } public static class Identity @@ -31,6 +37,7 @@ public static class Identity public const string INVALID_CREDENTIALS = "INVALID_CREDENTIALS"; public const string INVALID_TOKEN = "INVALID_TOKEN"; public const string INVALID_REFRESH_TOKEN = "INVALID_REFRESH_TOKEN"; + public const string INVALID_RESET_TOKEN = "INVALID_RESET_TOKEN"; public const string REGISTRATION_FAILED = "REGISTRATION_FAILED"; public const string LOGIN_FAILED = "LOGIN_FAILED"; public const string PASSWORD_RECOVERY_FAILED = "PASSWORD_RECOVERY_FAILED"; @@ -44,12 +51,34 @@ public static class Identity public const string STATE_REP_ASSIGNMENT_NOT_FOUND = "STATE_REP_ASSIGNMENT_NOT_FOUND"; public const string STATE_REP_ASSIGNMENT_EXISTS = "STATE_REP_ASSIGNMENT_EXISTS"; public const string CONTACT_NOT_VERIFIED = "CONTACT_NOT_VERIFIED"; + public const string LOGIN_SUCCESS = "LOGIN_SUCCESS"; + public const string AD_LOGIN_SUCCESS = "AD_LOGIN_SUCCESS"; + public const string REGISTER_SUCCESS = "REGISTER_SUCCESS"; + public const string PROFILE_UPDATED = "PROFILE_UPDATED"; + public const string TOKEN_REFRESHED = "TOKEN_REFRESHED"; + public const string USER_STATUS_CHANGED = "USER_STATUS_CHANGED"; + public const string EXPERT_REQUEST_SUBMITTED = "EXPERT_REQUEST_SUBMITTED"; + public const string EXPERT_REQUEST_APPROVED = "EXPERT_REQUEST_APPROVED"; + public const string EXPERT_REQUEST_REJECTED = "EXPERT_REQUEST_REJECTED"; + public const string STATE_REP_ASSIGNMENT_CREATED = "STATE_REP_ASSIGNMENT_CREATED"; + public const string STATE_REP_ASSIGNMENT_REVOKED = "STATE_REP_ASSIGNMENT_REVOKED"; + public const string INTEREST_UPSERTED = "INTEREST_UPSERTED"; + public const string ROLE_NOT_FOUND = "ROLE_NOT_FOUND"; + public const string PERMISSIONS_GRANTED = "PERMISSIONS_GRANTED"; + public const string PERMISSIONS_REVOKED = "PERMISSIONS_REVOKED"; + public const string PERMISSIONS_UPDATED = "PERMISSIONS_UPDATED"; + public const string CLAIMS_GRANTED = "CLAIMS_GRANTED"; + public const string CLAIMS_REVOKED = "CLAIMS_REVOKED"; + public const string USER_CLAIMS_UPDATED = "USER_CLAIMS_UPDATED"; + public const string EMAIL_CHANGE_FAILED = "EMAIL_CHANGE_FAILED"; } public static class Content { public const string RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"; public const string RESOURCE_DUPLICATE = "RESOURCE_DUPLICATE"; + public const string TAG_NOT_FOUND = "TAG_NOT_FOUND"; + public const string NEWSLETTER_SUBSCRIBED = "NEWSLETTER_SUBSCRIBED"; public const string RESOURCE_CREATED = "RESOURCE_CREATED"; public const string RESOURCE_UPDATED = "RESOURCE_UPDATED"; public const string RESOURCE_DELETED = "RESOURCE_DELETED"; @@ -75,10 +104,17 @@ public static class Content public const string CONTENT_PUBLISHED = "CONTENT_PUBLISHED"; public const string CONTENT_ARCHIVED = "CONTENT_ARCHIVED"; public const string ASSET_UPLOADED = "ASSET_UPLOADED"; + public const string RESOURCE_DOWNLOAD_SUCCESS = "RESOURCE_DOWNLOAD_SUCCESS"; + public const string RESOURCE_SHARE_SUCCESS = "RESOURCE_SHARE_SUCCESS"; + public const string RESOURCE_SHARE_FAILED = "RESOURCE_SHARE_FAILED"; + public const string RESOURCE_DOWNLOAD_FAILED = "RESOURCE_DOWNLOAD_FAILED"; + public const string RESOURCE_UPLOAD_FAILED = "RESOURCE_UPLOAD_FAILED"; + public const string RESOURCE_DELETE_FAILED = "RESOURCE_DELETE_FAILED"; } public static class Community { + public const string TOPICS_LISTED = "TOPICS_LISTED"; public const string TOPIC_NOT_FOUND = "TOPIC_NOT_FOUND"; public const string TOPIC_DUPLICATE = "TOPIC_DUPLICATE"; public const string POST_NOT_FOUND = "POST_NOT_FOUND"; @@ -137,6 +173,8 @@ public static class KnowledgeMap public static class PlatformSettings { public const string HOMEPAGE_SETTINGS_NOT_FOUND = "HOMEPAGE_SETTINGS_NOT_FOUND"; + public const string HOMEPAGE_SECTION_NOT_FOUND = "HOMEPAGE_SECTION_NOT_FOUND"; + public const string SECTION_REORDERED = "SECTION_REORDERED"; public const string ABOUT_SETTINGS_NOT_FOUND = "ABOUT_SETTINGS_NOT_FOUND"; public const string POLICIES_SETTINGS_NOT_FOUND = "POLICIES_SETTINGS_NOT_FOUND"; public const string GLOSSARY_ENTRY_NOT_FOUND = "GLOSSARY_ENTRY_NOT_FOUND"; @@ -160,6 +198,7 @@ public static class Media public static class Verification { public const string OTP_NOT_FOUND = "OTP_NOT_FOUND"; + public const string OTP_UNAUTHORIZED = "OTP_UNAUTHORIZED"; public const string OTP_EXPIRED = "OTP_EXPIRED"; public const string OTP_INVALID_CODE = "OTP_INVALID_CODE"; public const string OTP_MAX_ATTEMPTS = "OTP_MAX_ATTEMPTS"; @@ -168,6 +207,8 @@ public static class Verification public const string CONTACT_ALREADY_TAKEN = "CONTACT_ALREADY_TAKEN"; public const string EMAIL_UPDATED = "EMAIL_UPDATED"; public const string PHONE_UPDATED = "PHONE_UPDATED"; + public const string OTP_SENT = "OTP_SENT"; + public const string OTP_VERIFIED = "OTP_VERIFIED"; } public static class Lookups @@ -201,13 +242,27 @@ public static class Evaluation public const string EVALUATION_SUBMITTED = "EVALUATION_SUBMITTED"; } + public static class InterestTopic + { + public const string INTEREST_TOPIC_NOT_FOUND = "INTEREST_TOPIC_NOT_FOUND"; + public const string INTEREST_TOPIC_CREATED = "INTEREST_TOPIC_CREATED"; + public const string INTEREST_TOPIC_UPDATED = "INTEREST_TOPIC_UPDATED"; + public const string INTEREST_TOPIC_DELETED = "INTEREST_TOPIC_DELETED"; + } + public static class Validation { public const string REQUIRED_FIELD = "REQUIRED_FIELD"; public const string INVALID_EMAIL = "INVALID_EMAIL"; + public const string INVALID_PHONE = "INVALID_PHONE"; public const string MIN_LENGTH = "MIN_LENGTH"; public const string MAX_LENGTH = "MAX_LENGTH"; public const string INVALID_FORMAT = "INVALID_FORMAT"; public const string INVALID_ENUM = "INVALID_ENUM"; + public const string PASSWORD_UPPERCASE = "PASSWORD_UPPERCASE"; + public const string PASSWORD_LOWERCASE = "PASSWORD_LOWERCASE"; + public const string PASSWORD_NUMBER = "PASSWORD_NUMBER"; + public const string PASSWORD_POLICY = "PASSWORD_POLICY"; + public const string PASSWORDS_MUST_MATCH = "PASSWORDS_MUST_MATCH"; } } diff --git a/backend/src/CCE.Application/Messages/SystemCode.cs b/backend/src/CCE.Application/Messages/SystemCode.cs index bc6133d4..bcb6fd40 100644 --- a/backend/src/CCE.Application/Messages/SystemCode.cs +++ b/backend/src/CCE.Application/Messages/SystemCode.cs @@ -35,6 +35,7 @@ public static class SystemCode public const string ERR028 = "ERR028"; // News/event deletion failure (appendix) public const string ERR029 = "ERR029"; // Resource upload failure (appendix) public const string ERR030 = "ERR030"; // Resource deletion failure (appendix) + public const string ERR032 = "ERR032"; // Login failed // ─── Backend-only Identity Errors (moved to free appendix numbers) ─── public const string ERR400 = "ERR400"; // Expert request not found @@ -47,6 +48,9 @@ public static class SystemCode public const string ERR407 = "ERR407"; // Not authenticated public const string ERR408 = "ERR408"; // Expert request already exists public const string ERR409 = "ERR409"; // State rep assignment already exists + public const string ERR410 = "ERR410"; // Role not found + public const string ERR411 = "ERR411"; // Invalid password reset token + public const string ERR412 = "ERR412"; // Email change failed // ─── Content Errors ─── public const string ERR040 = "ERR040"; // News not found @@ -135,6 +139,7 @@ public static class SystemCode public const string ERR124 = "ERR124"; // OTP cooldown active public const string ERR125 = "ERR125"; // OTP invalidated public const string ERR126 = "ERR126"; // Contact already taken + public const string ERR127 = "ERR127"; // OTP unauthorized (wrong owner) // ─── Evaluation Errors ─── public const string ERR009 = "ERR009"; // Evaluation not found @@ -149,6 +154,8 @@ public static class SystemCode public const string ERR906 = "ERR906"; // External API not configured public const string ERR907 = "ERR907"; // Concurrency conflict public const string ERR908 = "ERR908"; // Duplicate value (generic) + public const string ERR909 = "ERR909"; // Rate limit exceeded + public const string ERR910 = "ERR910"; // Business rule violation (DomainException) // ════════════════════════════════════════════════════════════════ // CON — Confirmation / Success codes @@ -246,6 +253,20 @@ public static class SystemCode // ─── Lookups Success ─── public const string CON070 = "CON070"; // Lookup created public const string CON071 = "CON071"; // Lookup updated + public const string CON073 = "CON073"; // User created + public const string CON074 = "CON074"; // User updated + public const string CON075 = "CON075"; // User activated + public const string CON076 = "CON076"; // User deactivated + public const string CON077 = "CON077"; // Permissions granted to role + public const string CON078 = "CON078"; // Permissions revoked from role + public const string CON079 = "CON079"; // Role permissions updated + public const string CON080 = "CON080"; // Claims granted to user + public const string CON081 = "CON081"; // Claims revoked from user + public const string CON082 = "CON082"; // User claims updated + public const string CON083 = "CON083"; // AD login success + public const string CON084 = "CON084"; // Newsletter subscribed + public const string CON085 = "CON085"; // Topics listed + public const string CON086 = "CON086"; // Homepage section reordered // ─── InteractiveMap Success ─── public const string CON150 = "CON150"; // Interactive map created @@ -277,4 +298,6 @@ public static class SystemCode public const string VAL009 = "VAL009"; // Password uppercase required public const string VAL010 = "VAL010"; // Password lowercase required public const string VAL011 = "VAL011"; // Password number required + public const string VAL012 = "VAL012"; // Password policy violated (length + complexity combined) + public const string VAL013 = "VAL013"; // Passwords do not match } diff --git a/backend/src/CCE.Application/Messages/SystemCodeMap.cs b/backend/src/CCE.Application/Messages/SystemCodeMap.cs index 61d57bfa..ec118d85 100644 --- a/backend/src/CCE.Application/Messages/SystemCodeMap.cs +++ b/backend/src/CCE.Application/Messages/SystemCodeMap.cs @@ -15,6 +15,7 @@ public static class SystemCodeMap ["CONTACT_NOT_VERIFIED"] = SystemCode.ERR004, ["PASSWORD_RECOVERY_FAILED"] = SystemCode.ERR023, ["LOGOUT_FAILED"] = SystemCode.ERR024, + ["LOGIN_FAILED"] = SystemCode.ERR032, // ─── Backend-only Identity Errors (moved to free appendix numbers) ─── ["EXPERT_REQUEST_NOT_FOUND"] = SystemCode.ERR400, @@ -27,6 +28,9 @@ public static class SystemCodeMap ["NOT_AUTHENTICATED"] = SystemCode.ERR407, ["EXPERT_REQUEST_ALREADY_EXISTS"] = SystemCode.ERR408, ["STATE_REP_ASSIGNMENT_EXISTS"] = SystemCode.ERR409, + ["ROLE_NOT_FOUND"] = SystemCode.ERR410, + ["INVALID_RESET_TOKEN"] = SystemCode.ERR411, + ["EMAIL_CHANGE_FAILED"] = SystemCode.ERR412, // ─── Content Errors ─── ["NEWS_NOT_FOUND"] = SystemCode.ERR040, @@ -132,6 +136,7 @@ public static class SystemCodeMap ["OTP_COOLDOWN_ACTIVE"] = SystemCode.ERR124, ["OTP_INVALIDATED"] = SystemCode.ERR125, ["CONTACT_ALREADY_TAKEN"] = SystemCode.ERR126, + ["OTP_UNAUTHORIZED"] = SystemCode.ERR127, // ─── Evaluation Errors ─── ["EVALUATION_NOT_FOUND"] = SystemCode.ERR009, @@ -146,6 +151,8 @@ public static class SystemCodeMap ["EXTERNAL_API_NOT_CONFIGURED"] = SystemCode.ERR906, ["CONCURRENCY_CONFLICT"] = SystemCode.ERR907, ["DUPLICATE_VALUE"] = SystemCode.ERR908, + ["RATE_LIMIT_EXCEEDED"] = SystemCode.ERR909, + ["BUSINESS_RULE_VIOLATION"] = SystemCode.ERR910, // ─── Identity Success (appendix-aligned) ─── ["LOGIN_SUCCESS"] = SystemCode.CON056, @@ -157,6 +164,19 @@ public static class SystemCodeMap ["REGISTER_SUCCESS"] = SystemCode.CON017, ["INTEREST_UPSERTED"] = SystemCode.CON019, ["USER_DELETED"] = SystemCode.CON018, + ["USER_CREATED"] = SystemCode.CON073, + ["USER_UPDATED"] = SystemCode.CON074, + ["USER_ACTIVATED"] = SystemCode.CON075, + ["USER_DEACTIVATED"] = SystemCode.CON076, + + // ─── Claims / Permissions Success ─── + ["PERMISSIONS_GRANTED"] = SystemCode.CON077, + ["PERMISSIONS_REVOKED"] = SystemCode.CON078, + ["PERMISSIONS_UPDATED"] = SystemCode.CON079, + ["CLAIMS_GRANTED"] = SystemCode.CON080, + ["CLAIMS_REVOKED"] = SystemCode.CON081, + ["USER_CLAIMS_UPDATED"] = SystemCode.CON082, + ["AD_LOGIN_SUCCESS"] = SystemCode.CON083, // ─── Backend-only Identity Success (appendix numbers already taken) ─── ["EXPERT_REQUEST_APPROVED"] = SystemCode.CON050, @@ -228,6 +248,11 @@ public static class SystemCodeMap ["INTERACTIVE_MAP_NODE_UPDATED"] = SystemCode.CON154, ["INTERACTIVE_MAP_NODE_DELETED"] = SystemCode.CON155, + // ─── Content / Community / Platform Success ─── + ["NEWSLETTER_SUBSCRIBED"] = SystemCode.CON084, + ["TOPICS_LISTED"] = SystemCode.CON085, + ["SECTION_REORDERED"] = SystemCode.CON086, + // ─── General Success ─── ["ITEMS_LISTED"] = SystemCode.CON100, ["SUCCESS_OPERATION"] = SystemCode.CON900, @@ -247,6 +272,8 @@ public static class SystemCodeMap ["PASSWORD_UPPERCASE"] = SystemCode.VAL009, ["PASSWORD_LOWERCASE"] = SystemCode.VAL010, ["PASSWORD_NUMBER"] = SystemCode.VAL011, + ["PASSWORD_POLICY"] = SystemCode.VAL012, + ["PASSWORDS_MUST_MATCH"] = SystemCode.VAL013, }; private static readonly Dictionary CodeToDomain = diff --git a/backend/src/CCE.Application/Notifications/Admin/Queries/GetNotificationLogById/GetNotificationLogByIdQueryHandler.cs b/backend/src/CCE.Application/Notifications/Admin/Queries/GetNotificationLogById/GetNotificationLogByIdQueryHandler.cs index 32095736..1ad2e1f4 100644 --- a/backend/src/CCE.Application/Notifications/Admin/Queries/GetNotificationLogById/GetNotificationLogByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Notifications/Admin/Queries/GetNotificationLogById/GetNotificationLogByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -31,7 +31,7 @@ public async Task> Handle( return log is null ? _msg.NotificationLogNotFound() - : _msg.Ok(MapToDto(log), "ITEMS_LISTED"); + : _msg.Ok(MapToDto(log), MessageKeys.General.ITEMS_LISTED); } internal static NotificationLogDto MapToDto(NotificationLog l) => new( diff --git a/backend/src/CCE.Application/Notifications/Admin/Queries/ListNotificationLogs/ListNotificationLogsQueryHandler.cs b/backend/src/CCE.Application/Notifications/Admin/Queries/ListNotificationLogs/ListNotificationLogsQueryHandler.cs index 3c93a457..e8158ec6 100644 --- a/backend/src/CCE.Application/Notifications/Admin/Queries/ListNotificationLogs/ListNotificationLogsQueryHandler.cs +++ b/backend/src/CCE.Application/Notifications/Admin/Queries/ListNotificationLogs/ListNotificationLogsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -47,7 +47,7 @@ public async Task>> Handle( var items = page.Items.Select(MapToDto).ToList(); var result = new PagedResult(items, page.Page, page.PageSize, page.Total); - return _msg.Ok(result, "ITEMS_LISTED"); + return _msg.Ok(result, MessageKeys.General.ITEMS_LISTED); } internal static NotificationLogListItemDto MapToDto(NotificationLog l) => new( diff --git a/backend/src/CCE.Application/Notifications/Handlers/ExpertRegistrationApprovedNotificationHandler.cs b/backend/src/CCE.Application/Notifications/Handlers/ExpertRegistrationApprovedNotificationHandler.cs index e14ac6ca..0b54a6db 100644 --- a/backend/src/CCE.Application/Notifications/Handlers/ExpertRegistrationApprovedNotificationHandler.cs +++ b/backend/src/CCE.Application/Notifications/Handlers/ExpertRegistrationApprovedNotificationHandler.cs @@ -1,3 +1,4 @@ +using CCE.Application.Messages; using CCE.Application.Notifications.Messages; using CCE.Domain.Identity.Events; using CCE.Domain.Notifications; @@ -20,7 +21,7 @@ public async Task Handle( CancellationToken cancellationToken) { await _dispatcher.DispatchAsync(new NotificationMessage( - TemplateCode: "EXPERT_REQUEST_APPROVED", + TemplateCode: MessageKeys.Identity.EXPERT_REQUEST_APPROVED, RecipientUserId: notification.RequestedById, EventType: NotificationEventType.ExpertRequestApproved, Channels: [NotificationChannel.InApp, NotificationChannel.Email], diff --git a/backend/src/CCE.Application/Notifications/Handlers/ExpertRegistrationRejectedNotificationHandler.cs b/backend/src/CCE.Application/Notifications/Handlers/ExpertRegistrationRejectedNotificationHandler.cs index 38ca34df..095bab84 100644 --- a/backend/src/CCE.Application/Notifications/Handlers/ExpertRegistrationRejectedNotificationHandler.cs +++ b/backend/src/CCE.Application/Notifications/Handlers/ExpertRegistrationRejectedNotificationHandler.cs @@ -1,3 +1,4 @@ +using CCE.Application.Messages; using CCE.Application.Notifications.Messages; using CCE.Domain.Identity.Events; using CCE.Domain.Notifications; @@ -20,7 +21,7 @@ public async Task Handle( CancellationToken cancellationToken) { await _dispatcher.DispatchAsync(new NotificationMessage( - TemplateCode: "EXPERT_REQUEST_REJECTED", + TemplateCode: MessageKeys.Identity.EXPERT_REQUEST_REJECTED, RecipientUserId: notification.RequestedById, EventType: NotificationEventType.ExpertRequestRejected, Channels: [NotificationChannel.InApp, NotificationChannel.Email], diff --git a/backend/src/CCE.Application/Notifications/Public/Queries/GetMyNotificationSettings/GetMyNotificationSettingsQueryHandler.cs b/backend/src/CCE.Application/Notifications/Public/Queries/GetMyNotificationSettings/GetMyNotificationSettingsQueryHandler.cs index cfdd6b15..8d9b1e9c 100644 --- a/backend/src/CCE.Application/Notifications/Public/Queries/GetMyNotificationSettings/GetMyNotificationSettingsQueryHandler.cs +++ b/backend/src/CCE.Application/Notifications/Public/Queries/GetMyNotificationSettings/GetMyNotificationSettingsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -44,6 +44,6 @@ public async Task>> Handle } } - return _msg.Ok>(dtos, "ITEMS_LISTED"); + return _msg.Ok>(dtos, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Notifications/Public/Queries/GetMyUnreadCount/GetMyUnreadCountQueryHandler.cs b/backend/src/CCE.Application/Notifications/Public/Queries/GetMyUnreadCount/GetMyUnreadCountQueryHandler.cs index 87d584cc..0dac4a92 100644 --- a/backend/src/CCE.Application/Notifications/Public/Queries/GetMyUnreadCount/GetMyUnreadCountQueryHandler.cs +++ b/backend/src/CCE.Application/Notifications/Public/Queries/GetMyUnreadCount/GetMyUnreadCountQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -25,6 +25,6 @@ public async Task> Handle(GetMyUnreadCountQuery request, Cancellat .Where(n => n.UserId == userId && n.Status == NotificationStatus.Sent) .CountAsyncEither(cancellationToken) .ConfigureAwait(false); - return _msg.Ok(count, "ITEMS_LISTED"); + return _msg.Ok(count, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Notifications/Public/Queries/ListMyNotifications/ListMyNotificationsQueryHandler.cs b/backend/src/CCE.Application/Notifications/Public/Queries/ListMyNotifications/ListMyNotificationsQueryHandler.cs index 10dca722..747fcc7b 100644 --- a/backend/src/CCE.Application/Notifications/Public/Queries/ListMyNotifications/ListMyNotificationsQueryHandler.cs +++ b/backend/src/CCE.Application/Notifications/Public/Queries/ListMyNotifications/ListMyNotificationsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -39,7 +39,7 @@ public async Task>> Handle( var items = page.Items.Select(MapToDto).ToList(); var result = new PagedResult(items, page.Page, page.PageSize, page.Total); - return _msg.Ok(result, "ITEMS_LISTED"); + return _msg.Ok(result, MessageKeys.General.ITEMS_LISTED); } internal static UserNotificationDto MapToDto(UserNotification n) => new( diff --git a/backend/src/CCE.Application/Notifications/Queries/GetNotificationTemplateById/GetNotificationTemplateByIdQueryHandler.cs b/backend/src/CCE.Application/Notifications/Queries/GetNotificationTemplateById/GetNotificationTemplateByIdQueryHandler.cs index 983b220f..3b024d28 100644 --- a/backend/src/CCE.Application/Notifications/Queries/GetNotificationTemplateById/GetNotificationTemplateByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Notifications/Queries/GetNotificationTemplateById/GetNotificationTemplateByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -31,6 +31,6 @@ public async Task> Handle( var template = list.SingleOrDefault(); return template is null ? _msg.NotificationTemplateNotFound() - : _msg.Ok(ListNotificationTemplatesQueryHandler.MapToDto(template), "ITEMS_LISTED"); + : _msg.Ok(ListNotificationTemplatesQueryHandler.MapToDto(template), MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Notifications/Queries/ListNotificationTemplates/ListNotificationTemplatesQueryHandler.cs b/backend/src/CCE.Application/Notifications/Queries/ListNotificationTemplates/ListNotificationTemplatesQueryHandler.cs index 86ae779b..8e9dcc97 100644 --- a/backend/src/CCE.Application/Notifications/Queries/ListNotificationTemplates/ListNotificationTemplatesQueryHandler.cs +++ b/backend/src/CCE.Application/Notifications/Queries/ListNotificationTemplates/ListNotificationTemplatesQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -43,7 +43,7 @@ public async Task>> Handle( var items = page.Items.Select(MapToDto).ToList(); var result = new PagedResult(items, page.Page, page.PageSize, page.Total); - return _msg.Ok(result, "ITEMS_LISTED"); + return _msg.Ok(result, MessageKeys.General.ITEMS_LISTED); } internal static NotificationTemplateDto MapToDto(NotificationTemplate t) => new( diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/CreateGlossaryEntry/CreateGlossaryEntryCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/CreateGlossaryEntry/CreateGlossaryEntryCommandHandler.cs index 450e0310..eea3c841 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/CreateGlossaryEntry/CreateGlossaryEntryCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/CreateGlossaryEntry/CreateGlossaryEntryCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -46,6 +46,6 @@ public CreateGlossaryEntryCommandHandler( var entry = about.AddGlossaryEntry(term, definition, userId, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(entry.Id, "CONTENT_CREATED"); + return _msg.Ok(entry.Id, MessageKeys.Content.CONTENT_CREATED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/CreateKnowledgePartner/CreateKnowledgePartnerCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/CreateKnowledgePartner/CreateKnowledgePartnerCommandHandler.cs index 56cbfbb7..89424097 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/CreateKnowledgePartner/CreateKnowledgePartnerCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/CreateKnowledgePartner/CreateKnowledgePartnerCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -50,6 +50,6 @@ public CreateKnowledgePartnerCommandHandler( var partner = about.AddKnowledgePartner(name, description, request.LogoUrl, request.WebsiteUrl, userId, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(partner.Id, "CONTENT_CREATED"); + return _msg.Ok(partner.Id, MessageKeys.Content.CONTENT_CREATED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/CreatePolicySection/CreatePolicySectionCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/CreatePolicySection/CreatePolicySectionCommandHandler.cs index 8474b785..6ee4a72b 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/CreatePolicySection/CreatePolicySectionCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/CreatePolicySection/CreatePolicySectionCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -47,6 +47,6 @@ public CreatePolicySectionCommandHandler( var section = settings.AddSection(type, title, content, userId, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(section.Id, "CONTENT_CREATED"); + return _msg.Ok(section.Id, MessageKeys.Content.CONTENT_CREATED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/DeleteGlossaryEntry/DeleteGlossaryEntryCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/DeleteGlossaryEntry/DeleteGlossaryEntryCommandHandler.cs index 37fccf71..00b4bdd2 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/DeleteGlossaryEntry/DeleteGlossaryEntryCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/DeleteGlossaryEntry/DeleteGlossaryEntryCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.PlatformSettings; @@ -37,6 +37,6 @@ public async Task> Handle( about.RemoveGlossaryEntry(entry); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok("CONTENT_DELETED"); + return _msg.Ok(MessageKeys.Content.CONTENT_DELETED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/DeleteKnowledgePartner/DeleteKnowledgePartnerCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/DeleteKnowledgePartner/DeleteKnowledgePartnerCommandHandler.cs index 98bb1e54..abff7262 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/DeleteKnowledgePartner/DeleteKnowledgePartnerCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/DeleteKnowledgePartner/DeleteKnowledgePartnerCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.PlatformSettings; @@ -37,6 +37,6 @@ public async Task> Handle( about.RemoveKnowledgePartner(partner); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok("CONTENT_DELETED"); + return _msg.Ok(MessageKeys.Content.CONTENT_DELETED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/DeletePolicySection/DeletePolicySectionCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/DeletePolicySection/DeletePolicySectionCommandHandler.cs index 3bbceec8..0740c52f 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/DeletePolicySection/DeletePolicySectionCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/DeletePolicySection/DeletePolicySectionCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.PlatformSettings; @@ -37,6 +37,6 @@ public async Task> Handle( settings.RemoveSection(section); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok("CONTENT_DELETED"); + return _msg.Ok(MessageKeys.Content.CONTENT_DELETED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/ReorderPolicySection/ReorderPolicySectionCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/ReorderPolicySection/ReorderPolicySectionCommandHandler.cs index 25c2a68c..7334b3c6 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/ReorderPolicySection/ReorderPolicySectionCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/ReorderPolicySection/ReorderPolicySectionCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.PlatformSettings; @@ -37,6 +37,6 @@ public ReorderPolicySectionCommandHandler( settings.ReorderSection(section, request.OrderIndex); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(section.Id, "SECTION_REORDERED"); + return _msg.Ok(section.Id, MessageKeys.PlatformSettings.SECTION_REORDERED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateAboutSettings/UpdateAboutSettingsCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateAboutSettings/UpdateAboutSettingsCommandHandler.cs index 60aa7744..7056ddd3 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateAboutSettings/UpdateAboutSettingsCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateAboutSettings/UpdateAboutSettingsCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -46,6 +46,6 @@ public UpdateAboutSettingsCommandHandler( await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(settings.Id, "SETTINGS_UPDATED"); + return _msg.Ok(settings.Id, MessageKeys.PlatformSettings.SETTINGS_UPDATED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateGlossaryEntry/UpdateGlossaryEntryCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateGlossaryEntry/UpdateGlossaryEntryCommandHandler.cs index f5b85aed..7a63f7b6 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateGlossaryEntry/UpdateGlossaryEntryCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateGlossaryEntry/UpdateGlossaryEntryCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -50,6 +50,6 @@ public UpdateGlossaryEntryCommandHandler( about.UpdateGlossaryEntry(entry, term, definition, userId, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(entry.Id, "CONTENT_UPDATED"); + return _msg.Ok(entry.Id, MessageKeys.Content.CONTENT_UPDATED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateHomepageSettings/UpdateHomepageSettingsCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateHomepageSettings/UpdateHomepageSettingsCommandHandler.cs index 6bf2086f..5916462b 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateHomepageSettings/UpdateHomepageSettingsCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateHomepageSettings/UpdateHomepageSettingsCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -54,6 +54,6 @@ public UpdateHomepageSettingsCommandHandler( await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(settings.Id, "SETTINGS_UPDATED"); + return _msg.Ok(settings.Id, MessageKeys.PlatformSettings.SETTINGS_UPDATED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateKnowledgePartner/UpdateKnowledgePartnerCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateKnowledgePartner/UpdateKnowledgePartnerCommandHandler.cs index df103191..8ddfb9c0 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateKnowledgePartner/UpdateKnowledgePartnerCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateKnowledgePartner/UpdateKnowledgePartnerCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -54,6 +54,6 @@ public UpdateKnowledgePartnerCommandHandler( about.UpdateKnowledgePartner(partner, name, description, request.LogoUrl, request.WebsiteUrl, userId, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(partner.Id, "CONTENT_UPDATED"); + return _msg.Ok(partner.Id, MessageKeys.Content.CONTENT_UPDATED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/UpdatePolicySection/UpdatePolicySectionCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/UpdatePolicySection/UpdatePolicySectionCommandHandler.cs index 5c57309d..373e6898 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/UpdatePolicySection/UpdatePolicySectionCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/UpdatePolicySection/UpdatePolicySectionCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.Common; @@ -50,6 +50,6 @@ public UpdatePolicySectionCommandHandler( settings.UpdateSection(section, title, content, userId, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.Ok(section.Id, "CONTENT_UPDATED"); + return _msg.Ok(section.Id, MessageKeys.Content.CONTENT_UPDATED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicAboutSettings/GetPublicAboutSettingsQueryHandler.cs b/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicAboutSettings/GetPublicAboutSettingsQueryHandler.cs index 157d6dc5..ce1caecc 100644 --- a/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicAboutSettings/GetPublicAboutSettingsQueryHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicAboutSettings/GetPublicAboutSettingsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -51,6 +51,6 @@ public async Task> Handle( new LocalizedTextDto(p.Name.Ar, p.Name.En), p.LogoUrl, p.WebsiteUrl, - p.Description is null ? null : new LocalizedTextDto(p.Description.Ar, p.Description.En))).ToList()), "ITEMS_LISTED"); + p.Description is null ? null : new LocalizedTextDto(p.Description.Ar, p.Description.En))).ToList()), MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicHomepage/GetPublicHomepageQueryHandler.cs b/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicHomepage/GetPublicHomepageQueryHandler.cs index 7d783120..ca9a65bc 100644 --- a/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicHomepage/GetPublicHomepageQueryHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicHomepage/GetPublicHomepageQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Content.Public.Dtos; @@ -52,6 +52,6 @@ orderby hc.OrderIndex settings.CceConceptsEn, countries, sections.Select(s => new PublicHomepageSectionDto( - s.Id, s.SectionType, s.OrderIndex, s.ContentAr, s.ContentEn)).ToList()), "ITEMS_LISTED"); + s.Id, s.SectionType, s.OrderIndex, s.ContentAr, s.ContentEn)).ToList()), MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicPoliciesSettings/GetPublicPoliciesSettingsQueryHandler.cs b/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicPoliciesSettings/GetPublicPoliciesSettingsQueryHandler.cs index 79eeffea..bb2fb44e 100644 --- a/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicPoliciesSettings/GetPublicPoliciesSettingsQueryHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicPoliciesSettings/GetPublicPoliciesSettingsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -39,6 +39,6 @@ public async Task> Handle( sections.Select(s => new PublicPolicySectionDto( (int)s.Type, new LocalizedTextDto(s.Title.Ar, s.Title.En), - new LocalizedTextDto(s.Content.Ar, s.Content.En))).ToList()), "ITEMS_LISTED"); + new LocalizedTextDto(s.Content.Ar, s.Content.En))).ToList()), MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Queries/GetAboutSettings/GetAboutSettingsQueryHandler.cs b/backend/src/CCE.Application/PlatformSettings/Queries/GetAboutSettings/GetAboutSettingsQueryHandler.cs index dd5b10d7..bcfa5644 100644 --- a/backend/src/CCE.Application/PlatformSettings/Queries/GetAboutSettings/GetAboutSettingsQueryHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Queries/GetAboutSettings/GetAboutSettingsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -55,6 +55,6 @@ public async Task> Handle( p.LogoUrl, p.WebsiteUrl, p.Description is null ? null : new LocalizedTextDto(p.Description.Ar, p.Description.En), - p.OrderIndex)).ToList()), "ITEMS_LISTED"); + p.OrderIndex)).ToList()), MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Queries/GetHomepageSettings/GetHomepageSettingsQueryHandler.cs b/backend/src/CCE.Application/PlatformSettings/Queries/GetHomepageSettings/GetHomepageSettingsQueryHandler.cs index 72f992e4..6628a2a3 100644 --- a/backend/src/CCE.Application/PlatformSettings/Queries/GetHomepageSettings/GetHomepageSettingsQueryHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Queries/GetHomepageSettings/GetHomepageSettingsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -41,6 +41,6 @@ public async Task> Handle( new LocalizedTextDto(settings.Objective.Ar, settings.Objective.En), settings.CceConceptsAr, settings.CceConceptsEn, - countries), "ITEMS_LISTED"); + countries), MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/PlatformSettings/Queries/GetPoliciesSettings/GetPoliciesSettingsQueryHandler.cs b/backend/src/CCE.Application/PlatformSettings/Queries/GetPoliciesSettings/GetPoliciesSettingsQueryHandler.cs index f1f60891..b74215f1 100644 --- a/backend/src/CCE.Application/PlatformSettings/Queries/GetPoliciesSettings/GetPoliciesSettingsQueryHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Queries/GetPoliciesSettings/GetPoliciesSettingsQueryHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; using CCE.Application.Messages; @@ -40,6 +40,6 @@ public async Task> Handle( s.Id, (int)s.Type, new LocalizedTextDto(s.Title.Ar, s.Title.En), new LocalizedTextDto(s.Content.Ar, s.Content.En), - s.OrderIndex)).ToList()), "ITEMS_LISTED"); + s.OrderIndex)).ToList()), MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Search/Queries/SearchQuery.cs b/backend/src/CCE.Application/Search/Queries/SearchQuery.cs index c0e57970..4edbf77e 100644 --- a/backend/src/CCE.Application/Search/Queries/SearchQuery.cs +++ b/backend/src/CCE.Application/Search/Queries/SearchQuery.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using CCE.Application.Common.Pagination; using MediatR; @@ -7,4 +8,4 @@ public sealed record SearchQuery( string Q, SearchableType? Type = null, int Page = 1, - int PageSize = 20) : IRequest>; + int PageSize = 20) : IRequest>>; diff --git a/backend/src/CCE.Application/Search/Queries/SearchQueryHandler.cs b/backend/src/CCE.Application/Search/Queries/SearchQueryHandler.cs index 3f484b3d..501c3169 100644 --- a/backend/src/CCE.Application/Search/Queries/SearchQueryHandler.cs +++ b/backend/src/CCE.Application/Search/Queries/SearchQueryHandler.cs @@ -1,27 +1,31 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Common.Pagination; +using CCE.Application.Messages; using MediatR; namespace CCE.Application.Search.Queries; -public sealed class SearchQueryHandler : IRequestHandler> +public sealed class SearchQueryHandler : IRequestHandler>> { private readonly ISearchClient _client; private readonly ISearchQueryLogger _logger; private readonly ICurrentUserAccessor _currentUser; + private readonly MessageFactory _msg; - public SearchQueryHandler(ISearchClient client, ISearchQueryLogger logger, ICurrentUserAccessor currentUser) + public SearchQueryHandler(ISearchClient client, ISearchQueryLogger logger, ICurrentUserAccessor currentUser, MessageFactory msg) { _client = client; _logger = logger; _currentUser = currentUser; + _msg = msg; } [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Defensive double-catch inside fire-and-forget lambda; analytics failure must never propagate to the caller.")] - public async Task> Handle(SearchQuery request, CancellationToken cancellationToken) + public async Task>> Handle(SearchQuery request, CancellationToken cancellationToken) { var sw = Stopwatch.StartNew(); var result = await _client.SearchAsync(request.Q, request.Type, request.Page, request.PageSize, cancellationToken).ConfigureAwait(false); @@ -39,6 +43,6 @@ public async Task> Handle(SearchQuery request, Cancell catch (Exception) { /* swallowed in logger; defensive double-catch */ } }, CancellationToken.None); - return result; + return _msg.Ok(result, MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Surveys/Commands/SubmitServiceRating/SubmitServiceRatingCommand.cs b/backend/src/CCE.Application/Surveys/Commands/SubmitServiceRating/SubmitServiceRatingCommand.cs index 4b393145..5d345e55 100644 --- a/backend/src/CCE.Application/Surveys/Commands/SubmitServiceRating/SubmitServiceRatingCommand.cs +++ b/backend/src/CCE.Application/Surveys/Commands/SubmitServiceRating/SubmitServiceRatingCommand.cs @@ -1,3 +1,4 @@ +using CCE.Application.Common; using MediatR; namespace CCE.Application.Surveys.Commands.SubmitServiceRating; @@ -7,4 +8,4 @@ public sealed record SubmitServiceRatingCommand( string? CommentAr, string? CommentEn, string Page, - string Locale) : IRequest; + string Locale) : IRequest>; diff --git a/backend/src/CCE.Application/Surveys/Commands/SubmitServiceRating/SubmitServiceRatingCommandHandler.cs b/backend/src/CCE.Application/Surveys/Commands/SubmitServiceRating/SubmitServiceRatingCommandHandler.cs index 4a80f306..dd671b94 100644 --- a/backend/src/CCE.Application/Surveys/Commands/SubmitServiceRating/SubmitServiceRatingCommandHandler.cs +++ b/backend/src/CCE.Application/Surveys/Commands/SubmitServiceRating/SubmitServiceRatingCommandHandler.cs @@ -1,4 +1,6 @@ +using CCE.Application.Common; using CCE.Application.Common.Interfaces; +using CCE.Application.Messages; using CCE.Domain.Common; using CCE.Domain.Surveys; using MediatR; @@ -6,23 +8,26 @@ namespace CCE.Application.Surveys.Commands.SubmitServiceRating; public sealed class SubmitServiceRatingCommandHandler - : IRequestHandler + : IRequestHandler> { private readonly IServiceRatingService _service; private readonly ICurrentUserAccessor _currentUser; private readonly ISystemClock _clock; + private readonly MessageFactory _msg; public SubmitServiceRatingCommandHandler( IServiceRatingService service, ICurrentUserAccessor currentUser, - ISystemClock clock) + ISystemClock clock, + MessageFactory msg) { _service = service; _currentUser = currentUser; _clock = clock; + _msg = msg; } - public async Task Handle( + public async Task> Handle( SubmitServiceRatingCommand request, CancellationToken cancellationToken) { @@ -39,6 +44,6 @@ public SubmitServiceRatingCommandHandler( await _service.SaveAsync(rating, cancellationToken).ConfigureAwait(false); - return rating.Id; + return _msg.Ok(rating.Id, MessageKeys.General.SUCCESS_CREATED); } } diff --git a/backend/src/CCE.Application/Verification/Commands/RequestVerification/RequestVerificationCommandHandler.cs b/backend/src/CCE.Application/Verification/Commands/RequestVerification/RequestVerificationCommandHandler.cs index f3cb313a..9beea48f 100644 --- a/backend/src/CCE.Application/Verification/Commands/RequestVerification/RequestVerificationCommandHandler.cs +++ b/backend/src/CCE.Application/Verification/Commands/RequestVerification/RequestVerificationCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Application.Notifications; @@ -75,6 +75,6 @@ await _gateway.SendAsync(new NotificationDispatchRequest( return _msg.Ok( new RequestVerificationResponseDto(entity.Id, entity.ExpiresAt), - "OTP_SENT"); + MessageKeys.Verification.OTP_SENT); } } diff --git a/backend/src/CCE.Application/Verification/Commands/VerifyOtp/VerifyOtpCommandHandler.cs b/backend/src/CCE.Application/Verification/Commands/VerifyOtp/VerifyOtpCommandHandler.cs index 9f32b933..f5519366 100644 --- a/backend/src/CCE.Application/Verification/Commands/VerifyOtp/VerifyOtpCommandHandler.cs +++ b/backend/src/CCE.Application/Verification/Commands/VerifyOtp/VerifyOtpCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Identity; using CCE.Application.Messages; @@ -84,7 +84,7 @@ public async Task> Handle( await _db.SaveChangesAsync(ct).ConfigureAwait(false); - return _msg.Ok(new VerifyOtpResponseDto(true, resolvedUserId), "OTP_VERIFIED"); + return _msg.Ok(new VerifyOtpResponseDto(true, resolvedUserId), MessageKeys.Verification.OTP_VERIFIED); } private async Task StampUserConfirmedAsync(OtpVerification entity, CancellationToken ct) diff --git a/backend/src/CCE.Domain/Common/DomainException.cs b/backend/src/CCE.Domain/Common/DomainException.cs index 6af8b243..b216bc53 100644 --- a/backend/src/CCE.Domain/Common/DomainException.cs +++ b/backend/src/CCE.Domain/Common/DomainException.cs @@ -9,7 +9,8 @@ namespace CCE.Domain.Common; /// /// Sub-projects derive concrete types per bounded context, e.g., /// DuplicateException, InvalidStatusTransitionException. -/// Phase 08 middleware translates these to RFC 7807 ProblemDetails. +/// The API middleware (ExceptionHandlingMiddleware) translates these +/// to a 422 response with the BUSINESS_RULE_VIOLATION error envelope. /// public class DomainException : Exception { diff --git a/backend/src/CCE.Domain/Common/Error.cs b/backend/src/CCE.Domain/Common/Error.cs deleted file mode 100644 index ff157975..00000000 --- a/backend/src/CCE.Domain/Common/Error.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Text.Json.Serialization; - -namespace CCE.Domain.Common; - -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum ErrorType -{ - None, - Validation, - NotFound, - Conflict, - Unauthorized, - Forbidden, - BusinessRule, - Internal -} - -public sealed record Error( - string Code, - string MessageAr, - string MessageEn, - ErrorType Type = ErrorType.Internal, - IDictionary? Details = null); diff --git a/backend/tests/CCE.Api.IntegrationTests/Middleware/ExceptionHandlingMiddlewareTests.cs b/backend/tests/CCE.Api.IntegrationTests/Middleware/ExceptionHandlingMiddlewareTests.cs index 0cd34b57..e4e8f09f 100644 --- a/backend/tests/CCE.Api.IntegrationTests/Middleware/ExceptionHandlingMiddlewareTests.cs +++ b/backend/tests/CCE.Api.IntegrationTests/Middleware/ExceptionHandlingMiddlewareTests.cs @@ -1,19 +1,21 @@ using System.Net; using System.Text.Json; using CCE.Api.Common.Middleware; +using CCE.Application.Localization; using FluentValidation; using FluentValidation.Results; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace CCE.Api.IntegrationTests.Middleware; public class ExceptionHandlingMiddlewareTests { - private static IHost BuildHost(RequestDelegate handler) => + private static IHost BuildHost(RequestDelegate handler, ILocalizationService? localization = null) => new HostBuilder() .ConfigureWebHost(web => { @@ -24,6 +26,10 @@ private static IHost BuildHost(RequestDelegate handler) => app.UseMiddleware(); app.Run(handler); }); + if (localization is not null) + { + web.ConfigureTestServices(s => s.AddSingleton(localization)); + } }) .Start(); @@ -77,4 +83,63 @@ public async Task Includes_trace_id_in_response_body() var doc = JsonDocument.Parse(body).RootElement; doc.GetProperty("traceId").GetString().Should().NotBeNullOrEmpty(); } + + [Fact] + public async Task Returns_401_response_on_unauthorized_access_exception() + { + using var host = BuildHost(_ => throw new UnauthorizedAccessException("nope")); + var client = host.GetTestClient(); + + var resp = await client.GetAsync(new Uri("/", UriKind.Relative)); + + resp.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + var body = await resp.Content.ReadAsStringAsync(); + var doc = JsonDocument.Parse(body).RootElement; + doc.GetProperty("success").GetBoolean().Should().BeFalse(); + doc.GetProperty("code").GetString().Should().Be("ERR901"); + } + + [Fact] + public async Task Message_language_follows_Accept_Language_header() + { + var localization = new StubLocalization(); + using var host = BuildHost(_ => throw new UnauthorizedAccessException(), localization); + var client = host.GetTestClient(); + + using var request = new HttpRequestMessage(HttpMethod.Get, "/"); + request.Headers.AcceptLanguage.Add(new System.Net.Http.Headers.StringWithQualityHeaderValue("en")); + var resp = await client.SendAsync(request); + + var body = await resp.Content.ReadAsStringAsync(); + var doc = JsonDocument.Parse(body).RootElement; + doc.GetProperty("message").GetString().Should().Be("en:UNAUTHORIZED_ACCESS"); + } + + [Fact] + public async Task Includes_correlation_id_in_response_body() + { + using var host = BuildHost(_ => throw new InvalidOperationException("x")); + var client = host.GetTestClient(); + + var correlationId = "abc-123-correlation"; + client.DefaultRequestHeaders.Add(CorrelationIdMiddleware.HeaderName, correlationId); + + var resp = await client.GetAsync(new Uri("/", UriKind.Relative)); + + var body = await resp.Content.ReadAsStringAsync(); + var doc = JsonDocument.Parse(body).RootElement; + doc.GetProperty("correlationId").GetString().Should().Be(correlationId); + } + + private sealed class StubLocalization : ILocalizationService + { + public string GetString(string key, string? culture = null) + => culture == "en" ? $"en:{key}" : $"ar:{key}"; + + public string GetStringOrDefault(string key, string defaultMessage, string? culture = null) + => GetString(key, culture); + + public LocalizedMessage GetLocalizedMessage(string key) + => new($"ar:{key}", $"en:{key}"); + } } diff --git a/backend/tests/CCE.Application.Tests/Country/Commands/UpdateCountryCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Country/Commands/UpdateCountryCommandHandlerTests.cs index fc168be7..4a7f23f8 100644 --- a/backend/tests/CCE.Application.Tests/Country/Commands/UpdateCountryCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Country/Commands/UpdateCountryCommandHandlerTests.cs @@ -1,5 +1,7 @@ using CCE.Application.Country; using CCE.Application.Country.Commands.UpdateCountry; +using CCE.Application.Localization; +using CCE.Application.Messages; namespace CCE.Application.Tests.Country.Commands; @@ -10,11 +12,12 @@ public async Task Returns_null_when_country_not_found() { var service = Substitute.For(); service.FindAsync(Arg.Any(), Arg.Any()).Returns((CCE.Domain.Country.Country?)null); - var sut = new UpdateCountryCommandHandler(service); + var sut = new UpdateCountryCommandHandler(service, BuildMessages()); var result = await sut.Handle(BuildCommand(System.Guid.NewGuid(), isActive: true), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); + result.Data.Should().BeNull(); } [Fact] @@ -23,15 +26,16 @@ public async Task Updates_names_and_calls_UpdateAsync() var country = CCE.Domain.Country.Country.Register("USA", "US", "أمريكا", "United States", "أمريكا الشمالية", "North America", "https://example/flag.png"); var service = Substitute.For(); service.FindAsync(country.Id, Arg.Any()).Returns(country); - var sut = new UpdateCountryCommandHandler(service); + var sut = new UpdateCountryCommandHandler(service, BuildMessages()); var cmd = new UpdateCountryCommand(country.Id, "الولايات المتحدة", "USA Updated", "أمريكا الشمالية", "North America", true); var result = await sut.Handle(cmd, CancellationToken.None); - result.Should().NotBeNull(); - result!.NameAr.Should().Be("الولايات المتحدة"); - result.NameEn.Should().Be("USA Updated"); - result.IsActive.Should().BeTrue(); + result.Success.Should().BeTrue(); + result.Data.Should().NotBeNull(); + result.Data!.NameAr.Should().Be("الولايات المتحدة"); + result.Data.NameEn.Should().Be("USA Updated"); + result.Data.IsActive.Should().BeTrue(); await service.Received(1).UpdateAsync(country, Arg.Any()); } @@ -41,16 +45,24 @@ public async Task Deactivates_when_IsActive_is_false() var country = CCE.Domain.Country.Country.Register("USA", "US", "أمريكا", "United States", "أمريكا الشمالية", "North America", "https://example/flag.png"); var service = Substitute.For(); service.FindAsync(country.Id, Arg.Any()).Returns(country); - var sut = new UpdateCountryCommandHandler(service); + var sut = new UpdateCountryCommandHandler(service, BuildMessages()); var cmd = new UpdateCountryCommand(country.Id, "أمريكا", "United States", "أمريكا الشمالية", "North America", false); var result = await sut.Handle(cmd, CancellationToken.None); - result.Should().NotBeNull(); - result!.IsActive.Should().BeFalse(); + result.Success.Should().BeTrue(); + result.Data.Should().NotBeNull(); + result.Data!.IsActive.Should().BeFalse(); country.IsActive.Should().BeFalse(); } + private static MessageFactory BuildMessages() + { + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + } + private static UpdateCountryCommand BuildCommand(System.Guid id, bool isActive) => new(id, "أمريكا", "United States", "أمريكا الشمالية", "North America", isActive); } diff --git a/backend/tests/CCE.Application.Tests/Country/Queries/GetCountryByIdQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Country/Queries/GetCountryByIdQueryHandlerTests.cs index e1e9989c..3fd794e0 100644 --- a/backend/tests/CCE.Application.Tests/Country/Queries/GetCountryByIdQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Country/Queries/GetCountryByIdQueryHandlerTests.cs @@ -1,19 +1,22 @@ using CCE.Application.Common.Interfaces; using CCE.Application.Country.Queries.GetCountryById; +using CCE.Application.Localization; +using CCE.Application.Messages; namespace CCE.Application.Tests.Country.Queries; public class GetCountryByIdQueryHandlerTests { [Fact] - public async Task Returns_null_when_country_not_found() + public async Task Returns_not_found_when_country_not_found() { var db = BuildDb(System.Array.Empty()); - var sut = new GetCountryByIdQueryHandler(db); + var sut = new GetCountryByIdQueryHandler(db, BuildMessages()); var result = await sut.Handle(new GetCountryByIdQuery(System.Guid.NewGuid()), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); + result.Data.Should().BeNull(); } [Fact] @@ -22,20 +25,28 @@ public async Task Returns_dto_with_all_fields_when_found() var country = CCE.Domain.Country.Country.Register("USA", "US", "أمريكا", "United States", "أمريكا الشمالية", "North America", "https://example/flag.png"); var db = BuildDb(new[] { country }); - var sut = new GetCountryByIdQueryHandler(db); + var sut = new GetCountryByIdQueryHandler(db, BuildMessages()); var result = await sut.Handle(new GetCountryByIdQuery(country.Id), CancellationToken.None); - result.Should().NotBeNull(); - result!.Id.Should().Be(country.Id); - result.IsoAlpha3.Should().Be("USA"); - result.IsoAlpha2.Should().Be("US"); - result.NameAr.Should().Be("أمريكا"); - result.NameEn.Should().Be("United States"); - result.RegionAr.Should().Be("أمريكا الشمالية"); - result.RegionEn.Should().Be("North America"); - result.FlagUrl.Should().Be("https://example/flag.png"); - result.IsActive.Should().BeTrue(); + result.Success.Should().BeTrue(); + result.Data.Should().NotBeNull(); + result.Data!.Id.Should().Be(country.Id); + result.Data.IsoAlpha3.Should().Be("USA"); + result.Data.IsoAlpha2.Should().Be("US"); + result.Data.NameAr.Should().Be("أمريكا"); + result.Data.NameEn.Should().Be("United States"); + result.Data.RegionAr.Should().Be("أمريكا الشمالية"); + result.Data.RegionEn.Should().Be("North America"); + result.Data.FlagUrl.Should().Be("https://example/flag.png"); + result.Data.IsActive.Should().BeTrue(); + } + + private static MessageFactory BuildMessages() + { + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); } private static ICceDbContext BuildDb(IEnumerable countries) diff --git a/backend/tests/CCE.Application.Tests/Country/Queries/GetCountryProfileQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Country/Queries/GetCountryProfileQueryHandlerTests.cs index 524e6bfd..ecec5c5f 100644 --- a/backend/tests/CCE.Application.Tests/Country/Queries/GetCountryProfileQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Country/Queries/GetCountryProfileQueryHandlerTests.cs @@ -1,5 +1,8 @@ +using CCE.Application.Common.Interfaces; using CCE.Application.Country; using CCE.Application.Country.Queries.GetCountryProfile; +using CCE.Application.Localization; +using CCE.Application.Messages; using CCE.TestInfrastructure.Time; namespace CCE.Application.Tests.Country.Queries; @@ -14,11 +17,12 @@ public async Task Returns_null_when_no_profile_exists() var service = Substitute.For(); service.FindByCountryIdAsync(Arg.Any(), Arg.Any()) .Returns((CCE.Domain.Country.CountryProfile?)null); - var sut = new GetCountryProfileQueryHandler(service); + var sut = new GetCountryProfileQueryHandler(service, BuildDb(), BuildMessages()); var result = await sut.Handle(new GetCountryProfileQuery(System.Guid.NewGuid()), CancellationToken.None); - result.Should().BeNull(); + result.Success.Should().BeFalse(); + result.Data.Should().BeNull(); } [Fact] @@ -31,18 +35,33 @@ public async Task Returns_dto_with_all_fields_when_profile_exists() var service = Substitute.For(); service.FindByCountryIdAsync(countryId, Arg.Any()).Returns(profile); - var sut = new GetCountryProfileQueryHandler(service); + var sut = new GetCountryProfileQueryHandler(service, BuildDb(), BuildMessages()); var result = await sut.Handle(new GetCountryProfileQuery(countryId), CancellationToken.None); - result.Should().NotBeNull(); - result!.CountryId.Should().Be(countryId); - result.DescriptionAr.Should().Be("ar-desc"); - result.DescriptionEn.Should().Be("en-desc"); - result.KeyInitiativesAr.Should().Be("ar-init"); - result.KeyInitiativesEn.Should().Be("en-init"); - result.ContactInfoAr.Should().BeNull(); - result.ContactInfoEn.Should().BeNull(); - result.LastUpdatedById.Should().Be(adminId); + result.Success.Should().BeTrue(); + result.Data.Should().NotBeNull(); + result.Data!.CountryId.Should().Be(countryId); + result.Data.DescriptionAr.Should().Be("ar-desc"); + result.Data.DescriptionEn.Should().Be("en-desc"); + result.Data.KeyInitiativesAr.Should().Be("ar-init"); + result.Data.KeyInitiativesEn.Should().Be("en-init"); + result.Data.ContactInfoAr.Should().BeNull(); + result.Data.ContactInfoEn.Should().BeNull(); + result.Data.LastUpdatedById.Should().Be(adminId); + } + + private static MessageFactory BuildMessages() + { + var localization = Substitute.For(); + localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); + return new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + } + + private static ICceDbContext BuildDb() + { + var db = Substitute.For(); + db.Countries.Returns(System.Array.Empty().AsQueryable()); + return db; } } diff --git a/backend/tests/CCE.Application.Tests/Identity/Queries/ListExpertProfilesQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Identity/Queries/ListExpertProfilesQueryHandlerTests.cs index 2d174322..84425d74 100644 --- a/backend/tests/CCE.Application.Tests/Identity/Queries/ListExpertProfilesQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Identity/Queries/ListExpertProfilesQueryHandlerTests.cs @@ -11,14 +11,15 @@ public class ListExpertProfilesQueryHandlerTests public async Task Returns_empty_paged_result_when_no_profiles_exist() { var db = BuildDb(System.Array.Empty(), System.Array.Empty()); - var sut = new ListExpertProfilesQueryHandler(db); + var sut = new ListExpertProfilesQueryHandler(db, IdentityTestHelpers.BuildMsg()); var result = await sut.Handle(new ListExpertProfilesQuery(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] @@ -36,14 +37,15 @@ public async Task Returns_profiles_with_user_names_populated() }; var db = BuildDb(new[] { aliceProfile }, users); - var sut = new ListExpertProfilesQueryHandler(db); + var sut = new ListExpertProfilesQueryHandler(db, IdentityTestHelpers.BuildMsg()); var result = await sut.Handle(new ListExpertProfilesQuery(Page: 1, PageSize: 20), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Should().HaveCount(1); + result.Success.Should().BeTrue(); + result.Data!.Total.Should().Be(1); + result.Data.Items.Should().HaveCount(1); - var item = result.Items.Single(); + var item = result.Data.Items.Single(); item.UserId.Should().Be(aliceId); item.UserName.Should().Be("alice"); item.BioEn.Should().Be("Alice Bio"); @@ -70,14 +72,15 @@ public async Task Search_filter_restricts_results_to_matching_user_name_or_email }; var db = BuildDb(new[] { aliceProfile, bobProfile }, users); - var sut = new ListExpertProfilesQueryHandler(db); + var sut = new ListExpertProfilesQueryHandler(db, IdentityTestHelpers.BuildMsg()); var result = await sut.Handle( new ListExpertProfilesQuery(Search: "alice"), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().UserId.Should().Be(aliceId); + result.Success.Should().BeTrue(); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().UserId.Should().Be(aliceId); } private static ExpertProfile BuildProfile( diff --git a/backend/tests/CCE.Application.Tests/Identity/Queries/ListExpertRequestsQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Identity/Queries/ListExpertRequestsQueryHandlerTests.cs index a9326580..501c5cd3 100644 --- a/backend/tests/CCE.Application.Tests/Identity/Queries/ListExpertRequestsQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Identity/Queries/ListExpertRequestsQueryHandlerTests.cs @@ -11,14 +11,15 @@ public class ListExpertRequestsQueryHandlerTests public async Task Returns_empty_paged_result_when_no_requests_exist() { var db = BuildDb(System.Array.Empty(), System.Array.Empty()); - var sut = new ListExpertRequestsQueryHandler(db); + var sut = new ListExpertRequestsQueryHandler(db, IdentityTestHelpers.BuildMsg()); var result = await sut.Handle(new ListExpertRequestsQuery(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] @@ -38,20 +39,21 @@ public async Task Returns_requests_with_requester_user_names_populated() }; var db = BuildDb(new[] { aliceRequest, bobRequest }, users); - var sut = new ListExpertRequestsQueryHandler(db); + var sut = new ListExpertRequestsQueryHandler(db, IdentityTestHelpers.BuildMsg()); var result = await sut.Handle(new ListExpertRequestsQuery(Page: 1, PageSize: 20), CancellationToken.None); - result.Total.Should().Be(2); - result.Items.Should().HaveCount(2); + result.Success.Should().BeTrue(); + result.Data!.Total.Should().Be(2); + result.Data.Items.Should().HaveCount(2); - var aliceItem = result.Items.Single(i => i.RequestedById == aliceId); + var aliceItem = result.Data.Items.Single(i => i.RequestedById == aliceId); aliceItem.RequestedByUserName.Should().Be("alice"); aliceItem.RequestedBioEn.Should().Be("Alice Bio"); aliceItem.RequestedTags.Should().BeEquivalentTo(new[] { "energy", "solar" }); aliceItem.Status.Should().Be(ExpertRegistrationStatus.Pending); - var bobItem = result.Items.Single(i => i.RequestedById == bobId); + var bobItem = result.Data.Items.Single(i => i.RequestedById == bobId); bobItem.RequestedByUserName.Should().Be("bob"); } @@ -68,14 +70,15 @@ public async Task Status_filter_restricts_results() var users = new[] { BuildUser(aliceId, "alice@cce.local", "alice") }; var db = BuildDb(new[] { pendingRequest, approvedRequest }, users); - var sut = new ListExpertRequestsQueryHandler(db); + var sut = new ListExpertRequestsQueryHandler(db, IdentityTestHelpers.BuildMsg()); var result = await sut.Handle( new ListExpertRequestsQuery(Status: ExpertRegistrationStatus.Pending), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().Status.Should().Be(ExpertRegistrationStatus.Pending); + result.Success.Should().BeTrue(); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().Status.Should().Be(ExpertRegistrationStatus.Pending); } [Fact] @@ -95,14 +98,15 @@ public async Task RequestedById_filter_restricts_results() }; var db = BuildDb(new[] { aliceRequest, bobRequest }, users); - var sut = new ListExpertRequestsQueryHandler(db); + var sut = new ListExpertRequestsQueryHandler(db, IdentityTestHelpers.BuildMsg()); var result = await sut.Handle( new ListExpertRequestsQuery(RequestedById: aliceId), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().RequestedById.Should().Be(aliceId); + result.Success.Should().BeTrue(); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().RequestedById.Should().Be(aliceId); } private static ICceDbContext BuildDb( diff --git a/backend/tests/CCE.Application.Tests/Identity/Queries/ListStateRepAssignmentsQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Identity/Queries/ListStateRepAssignmentsQueryHandlerTests.cs index 02788182..3c1c3e14 100644 --- a/backend/tests/CCE.Application.Tests/Identity/Queries/ListStateRepAssignmentsQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Identity/Queries/ListStateRepAssignmentsQueryHandlerTests.cs @@ -11,12 +11,13 @@ public class ListStateRepAssignmentsQueryHandlerTests public async Task Returns_empty_paged_result_when_no_assignments() { var db = BuildDb(System.Array.Empty(), System.Array.Empty()); - var sut = new ListStateRepAssignmentsQueryHandler(db); + var sut = new ListStateRepAssignmentsQueryHandler(db, IdentityTestHelpers.BuildMsg()); var result = await sut.Handle(new ListStateRepAssignmentsQuery(), CancellationToken.None); - result.Items.Should().BeEmpty(); - result.Total.Should().Be(0); + result.Success.Should().BeTrue(); + result.Data!.Items.Should().BeEmpty(); + result.Data.Total.Should().Be(0); } [Fact] @@ -38,12 +39,13 @@ public async Task Returns_active_assignments_with_user_names() }; var db = BuildDb(new[] { aliceA, bobA }, users); - var sut = new ListStateRepAssignmentsQueryHandler(db); + var sut = new ListStateRepAssignmentsQueryHandler(db, IdentityTestHelpers.BuildMsg()); var result = await sut.Handle(new ListStateRepAssignmentsQuery(), CancellationToken.None); - result.Total.Should().Be(2); - var aliceItem = result.Items.Single(i => i.UserId == aliceId); + result.Success.Should().BeTrue(); + result.Data!.Total.Should().Be(2); + var aliceItem = result.Data.Items.Single(i => i.UserId == aliceId); aliceItem.UserName.Should().Be("alice"); aliceItem.IsActive.Should().BeTrue(); aliceItem.RevokedOn.Should().BeNull(); @@ -66,12 +68,13 @@ public async Task UserId_filter_restricts_to_that_user() BuildUser(bobId, "bob@cce.local", "bob"), }; var db = BuildDb(new[] { aliceA, bobA }, users); - var sut = new ListStateRepAssignmentsQueryHandler(db); + var sut = new ListStateRepAssignmentsQueryHandler(db, IdentityTestHelpers.BuildMsg()); var result = await sut.Handle(new ListStateRepAssignmentsQuery(UserId: aliceId), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().UserId.Should().Be(aliceId); + result.Success.Should().BeTrue(); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().UserId.Should().Be(aliceId); } [Fact] @@ -87,12 +90,13 @@ public async Task CountryId_filter_restricts_to_that_country() var users = new[] { BuildUser(aliceId, "alice@cce.local", "alice") }; var db = BuildDb(new[] { assignment1, assignment2 }, users); - var sut = new ListStateRepAssignmentsQueryHandler(db); + var sut = new ListStateRepAssignmentsQueryHandler(db, IdentityTestHelpers.BuildMsg()); var result = await sut.Handle(new ListStateRepAssignmentsQuery(CountryId: country1), CancellationToken.None); - result.Total.Should().Be(1); - result.Items.Single().CountryId.Should().Be(country1); + result.Success.Should().BeTrue(); + result.Data!.Total.Should().Be(1); + result.Data.Items.Single().CountryId.Should().Be(country1); } [Fact] @@ -109,13 +113,14 @@ public async Task Active_false_with_in_memory_db_returns_all_assignments() var users = new[] { BuildUser(aliceId, "alice@cce.local", "alice") }; var db = BuildDb(new[] { assignment }, users); - var sut = new ListStateRepAssignmentsQueryHandler(db); + var sut = new ListStateRepAssignmentsQueryHandler(db, IdentityTestHelpers.BuildMsg()); var result = await sut.Handle(new ListStateRepAssignmentsQuery(Active: false), CancellationToken.None); - result.Items.Should().HaveCount(1); - result.Items.Single().IsActive.Should().BeFalse(); - result.Items.Single().RevokedOn.Should().NotBeNull(); + result.Success.Should().BeTrue(); + result.Data!.Items.Should().HaveCount(1); + result.Data.Items.Single().IsActive.Should().BeFalse(); + result.Data.Items.Single().RevokedOn.Should().NotBeNull(); } private static ICceDbContext BuildDb( diff --git a/backend/tests/CCE.ArchitectureTests/MessageKeysIntegrityTests.cs b/backend/tests/CCE.ArchitectureTests/MessageKeysIntegrityTests.cs new file mode 100644 index 00000000..79926c37 --- /dev/null +++ b/backend/tests/CCE.ArchitectureTests/MessageKeysIntegrityTests.cs @@ -0,0 +1,83 @@ +using System.Reflection; +using CCE.Application.Messages; + +namespace CCE.ArchitectureTests; + +/// +/// Safety net for the domain-key pipeline. Every constant in +/// must be present in (and vice‑versa) so no key silently +/// falls back to ERR900 at runtime. +/// +public sealed class DomainKeysIntegrityTests +{ + private static readonly Type[] DomainKeyClasses = + typeof(MessageKeys).GetNestedTypes(BindingFlags.Public | BindingFlags.Static) + .Where(t => t.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Any(f => f.FieldType == typeof(string))) + .ToArray(); + + /// All public const string values declared in MessageKeys nested classes. + private static readonly HashSet DomainKeyValues = new( + DomainKeyClasses.SelectMany(t => t + .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(f => f.FieldType == typeof(string)) + .Select(f => (string)f.GetValue(null)!)), + StringComparer.OrdinalIgnoreCase); + + /// All keys in the SystemCodeMap dictionary. + private static readonly HashSet MappedKeys = new( + GetSystemCodeMapKeys(), StringComparer.OrdinalIgnoreCase); + + [Fact] + public void Every_DomainKeys_constant_is_mapped_in_SystemCodeMap() + { + var unmapped = DomainKeyValues + .Where(v => !MappedKeys.Contains(v)) + .OrderBy(k => k) + .ToList(); + + unmapped.Should().BeEmpty( + because: "every MessageKeys constant must have a SystemCodeMap entry; " + + "unmapped: {0}", string.Join(", ", unmapped)); + } + + [Fact] + public void Every_SystemCodeMap_key_has_a_corresponding_DomainKeys_constant() + { + var orphaned = MappedKeys + .Where(k => !DomainKeyValues.Contains(k)) + .OrderBy(k => k) + .ToList(); + + orphaned.Should().BeEmpty( + because: "every SystemCodeMap domain key must have a matching MessageKeys constant; " + + "orphaned: {0}", string.Join(", ", orphaned)); + } + + [Fact] + public void No_DomainKeys_values_are_duplicated() + { + var duplicates = DomainKeyValues + .GroupBy(k => k, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .Select(g => $"'{g.Key}' appears {g.Count()} times") + .OrderBy(s => s) + .ToList(); + + duplicates.Should().BeEmpty( + because: "MessageKeys values must be unique across all categories to prevent " + + "ambiguous SystemCodeMap lookups; duplicates: {0}", + string.Join(" | ", duplicates)); + } + + private static List GetSystemCodeMapKeys() + { + var field = typeof(SystemCodeMap).GetField( + "DomainToCode", + BindingFlags.NonPublic | BindingFlags.Static); + + if (field?.GetValue(null) is Dictionary dict) + return dict.Keys.ToList(); + return []; + } +} From 0f3a4e765a03ee74ff1254c82d6af05169887cb4 Mon Sep 17 00:00:00 2001 From: MOHAMED RASHED Date: Thu, 25 Jun 2026 10:22:29 +0300 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20remove=20MessageFactory=20short?= =?UTF-8?q?cuts=20=E2=80=94=20enforce=20single=20generic=20call=20pattern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deleted all 52 convenience shortcut methods from MessageFactory, leaving exactly 9 core methods (Ok, NotFound, Conflict, Unauthorized, Forbidden, BusinessRule, ValidationError, Field). Updated ~130 handlers across CCE.Application to call the generic API directly with explicit MessageKeys constants. Previously 83% of handlers used generic calls while 17% used shortcuts, forcing every reader to check whether a shortcut exists before writing a handler. Three handlers already mixed both styles. The factory now has one pattern with no exceptions. Rule going forward: new outcomes add a MessageKeys constant, a SystemCodeMap entry, and a Resources.yaml string — nothing else touches MessageFactory. --- .../Localization/Resources.yaml | 12 +++ .../ApproveJoinRequestCommandHandler.cs | 2 +- .../CastPollVoteCommandHandler.cs | 2 +- .../CreateCommunityCommandHandler.cs | 2 +- .../CreatePost/CreatePostCommandHandler.cs | 12 +-- .../CreateReply/CreateReplyCommandHandler.cs | 2 +- .../DeleteDraft/DeleteDraftCommandHandler.cs | 2 +- .../DeleteTopic/DeleteTopicCommandHandler.cs | 4 +- .../JoinCommunityCommandHandler.cs | 2 +- .../LeaveCommunityCommandHandler.cs | 2 +- .../PublishPost/PublishPostCommandHandler.cs | 2 +- .../RejectJoinRequestCommandHandler.cs | 2 +- .../SetCommunityFollowCommandHandler.cs | 2 +- .../SetPostFollowCommandHandler.cs | 2 +- .../SetTopicFollowCommandHandler.cs | 2 +- .../SetUserFollowCommandHandler.cs | 6 +- .../UpdateDraft/UpdateDraftCommandHandler.cs | 2 +- .../UpdateTopic/UpdateTopicCommandHandler.cs | 2 +- .../VotePost/VotePostCommandHandler.cs | 2 +- .../VoteReply/VoteReplyCommandHandler.cs | 2 +- .../GetCommunityUserProfileQueryHandler.cs | 2 +- .../GetMyTopics/GetMyTopicsQueryHandler.cs | 2 +- .../GetPublicTopicBySlugQueryHandler.cs | 2 +- .../ListMyDrafts/ListMyDraftsQueryHandler.cs | 2 +- .../ListMyMentionsQueryHandler.cs | 2 +- .../GetTopicById/GetTopicByIdQueryHandler.cs | 2 +- ...oveCountryResourceRequestCommandHandler.cs | 8 +- .../CreateNews/CreateNewsCommandHandler.cs | 2 +- .../CreateResourceCommandHandler.cs | 8 +- .../DeleteEvent/DeleteEventCommandHandler.cs | 4 +- .../DeleteHomepageSectionCommandHandler.cs | 2 +- .../DeleteNews/DeleteNewsCommandHandler.cs | 4 +- .../DeletePage/DeletePageCommandHandler.cs | 4 +- .../DeleteResourceCommandHandler.cs | 4 +- .../DeleteResourceCategoryCommandHandler.cs | 2 +- .../PublishNews/PublishNewsCommandHandler.cs | 2 +- .../PublishResourceCommandHandler.cs | 6 +- ...ectCountryResourceRequestCommandHandler.cs | 8 +- .../RescheduleEventCommandHandler.cs | 2 +- ...bmitCountryContentRequestCommandHandler.cs | 2 +- .../UpdateEvent/UpdateEventCommandHandler.cs | 2 +- .../UpdateNews/UpdateNewsCommandHandler.cs | 2 +- .../UpdateResourceCommandHandler.cs | 4 +- .../UpdateResourceCategoryCommandHandler.cs | 2 +- .../GetPublicEventByIdQueryHandler.cs | 2 +- .../GetPublicNewsByIdQueryHandler.cs | 2 +- .../GetPublicResourceByIdQueryHandler.cs | 2 +- .../DownloadFile/DownloadFileQueryHandler.cs | 10 +- .../GetAssetById/GetAssetByIdQueryHandler.cs | 2 +- .../GetCountryContentRequestQueryHandler.cs | 2 +- .../GetEventById/GetEventByIdQueryHandler.cs | 2 +- .../GetNewsById/GetNewsByIdQueryHandler.cs | 2 +- .../GetResourceByIdQueryHandler.cs | 2 +- .../GetResourceCategoryByIdQueryHandler.cs | 2 +- .../UpsertCountryProfileCommandHandler.cs | 2 +- .../GetMyCountryProfileQueryHandler.cs | 6 +- .../GetPublicCountryProfileQueryHandler.cs | 2 +- .../GetEvaluationByIdQueryHandler.cs | 2 +- .../Auth/AdLogin/AdLoginCommandHandler.cs | 4 +- .../Auth/Login/LoginCommandHandler.cs | 6 +- .../Register/RegisterUserCommandHandler.cs | 2 +- .../ResetPasswordCommandHandler.cs | 2 +- .../ApproveExpertRequestCommandHandler.cs | 4 +- .../AssignUserRolesCommandHandler.cs | 2 +- .../ChangeUserStatusCommandHandler.cs | 2 +- .../CreateStateRepAssignmentCommandHandler.cs | 4 +- .../CreateUser/CreateUserCommandHandler.cs | 2 +- .../DeleteUser/DeleteUserCommandHandler.cs | 2 +- .../RejectExpertRequestCommandHandler.cs | 4 +- .../RevokeStateRepAssignmentCommandHandler.cs | 2 +- .../ConfirmEmailChangeCommandHandler.cs | 14 +-- .../ConfirmPhoneChangeCommandHandler.cs | 14 +-- .../Commands/ContactChangeOtpService.cs | 4 +- .../RequestEmailChangeCommandHandler.cs | 2 +- .../RequestPhoneChangeCommandHandler.cs | 2 +- .../SubmitExpertRequestCommandHandler.cs | 4 +- .../UpdateMyProfileCommandHandler.cs | 2 +- .../UpsertUserInterestCommandHandler.cs | 6 +- .../GetMyExpertStatusQueryHandler.cs | 2 +- .../GetMyInterestsQueryHandler.cs | 2 +- .../GetMyProfile/GetMyProfileQueryHandler.cs | 2 +- .../GetExpertRequestByIdQueryHandler.cs | 2 +- .../GetUserById/GetUserByIdQueryHandler.cs | 2 +- .../CreateInteractiveMapCommandHandler.cs | 4 +- .../CreateInteractiveMapNodeCommandHandler.cs | 4 +- .../DeleteInteractiveMapCommandHandler.cs | 6 +- .../DeleteInteractiveMapNodeCommandHandler.cs | 6 +- .../UpdateInteractiveMapCommandHandler.cs | 6 +- .../UpdateInteractiveMapNodeCommandHandler.cs | 6 +- .../GetInteractiveMapBySlugQueryHandler.cs | 2 +- ...etInteractiveMapNodeDetailsQueryHandler.cs | 4 +- .../GetInteractiveMapByIdQueryHandler.cs | 2 +- .../RefreshKapsarcSnapshotCommandHandler.cs | 12 +-- .../UpsertCountryCodeCommandHandler.cs | 2 +- .../GetCountryCodeByIdQueryHandler.cs | 2 +- .../DeleteMedia/DeleteMediaCommandHandler.cs | 2 +- .../UpdateMediaMetadataCommandHandler.cs | 2 +- .../UploadMedia/UploadMediaCommandHandler.cs | 6 +- .../GetMediaById/GetMediaByIdQueryHandler.cs | 2 +- .../Messages/MessageFactory.cs | 102 ------------------ .../CCE.Application/Messages/MessageKeys.cs | 3 + .../CCE.Application/Messages/SystemCode.cs | 3 + .../CCE.Application/Messages/SystemCodeMap.cs | 3 + .../CreateGlossaryEntryCommandHandler.cs | 2 +- .../CreateKnowledgePartnerCommandHandler.cs | 2 +- .../CreatePolicySectionCommandHandler.cs | 2 +- .../DeleteGlossaryEntryCommandHandler.cs | 4 +- .../DeleteKnowledgePartnerCommandHandler.cs | 4 +- .../DeletePolicySectionCommandHandler.cs | 4 +- .../ReorderPolicySectionCommandHandler.cs | 4 +- .../UpdateAboutSettingsCommandHandler.cs | 2 +- .../UpdateGlossaryEntryCommandHandler.cs | 4 +- .../UpdateHomepageSettingsCommandHandler.cs | 2 +- .../UpdateKnowledgePartnerCommandHandler.cs | 4 +- .../UpdatePolicySectionCommandHandler.cs | 4 +- .../GetPublicAboutSettingsQueryHandler.cs | 2 +- .../GetPublicHomepageQueryHandler.cs | 2 +- .../GetPublicPoliciesSettingsQueryHandler.cs | 2 +- .../GetAboutSettingsQueryHandler.cs | 2 +- .../GetHomepageSettingsQueryHandler.cs | 2 +- .../GetPoliciesSettingsQueryHandler.cs | 2 +- .../RequestVerificationCommandHandler.cs | 2 +- .../VerifyOtp/VerifyOtpCommandHandler.cs | 10 +- .../CCE.Infrastructure.csproj | 1 + 124 files changed, 221 insertions(+), 301 deletions(-) diff --git a/backend/src/CCE.Api.Common/Localization/Resources.yaml b/backend/src/CCE.Api.Common/Localization/Resources.yaml index 5a0584d7..f5c1af45 100644 --- a/backend/src/CCE.Api.Common/Localization/Resources.yaml +++ b/backend/src/CCE.Api.Common/Localization/Resources.yaml @@ -265,6 +265,18 @@ NOTIFICATION_TEMPLATE_UPDATED: ar: "تم تحديث قالب الإشعار بنجاح" en: "Notification template updated successfully" +DEVICE_TOKEN_REGISTERED: + ar: "تم تسجيل الجهاز بنجاح" + en: "Device registered successfully" + +DEVICE_TOKEN_DELETED: + ar: "تم إلغاء تسجيل الجهاز بنجاح" + en: "Device unregistered successfully" + +DEVICE_TOKEN_NOT_FOUND: + ar: "رمز الجهاز غير موجود" + en: "Device token not found" + EVALUATION_NOT_FOUND: ar: "التقييم غير موجود" en: "Evaluation not found" diff --git a/backend/src/CCE.Application/Community/Commands/ApproveJoinRequest/ApproveJoinRequestCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/ApproveJoinRequest/ApproveJoinRequestCommandHandler.cs index 12a8966f..d9c88f3d 100644 --- a/backend/src/CCE.Application/Community/Commands/ApproveJoinRequest/ApproveJoinRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/ApproveJoinRequest/ApproveJoinRequestCommandHandler.cs @@ -31,7 +31,7 @@ public ApproveJoinRequestCommandHandler( public async Task> Handle(ApproveJoinRequestCommand request, CancellationToken cancellationToken) { var by = _currentUser.GetUserId(); - if (by is null || by == Guid.Empty) return _msg.NotAuthenticated(); + if (by is null || by == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var joinRequest = await _repo.GetRequestAsync(request.RequestId, cancellationToken).ConfigureAwait(false); if (joinRequest is null) return _msg.NotFound(MessageKeys.Community.JOIN_REQUEST_NOT_FOUND); diff --git a/backend/src/CCE.Application/Community/Commands/CastPollVote/CastPollVoteCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/CastPollVote/CastPollVoteCommandHandler.cs index fb4a90cf..e6d07f08 100644 --- a/backend/src/CCE.Application/Community/Commands/CastPollVote/CastPollVoteCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/CastPollVote/CastPollVoteCommandHandler.cs @@ -39,7 +39,7 @@ public CastPollVoteCommandHandler( public async Task> Handle(CastPollVoteCommand request, CancellationToken cancellationToken) { var userId = _currentUser.GetUserId(); - if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); + if (userId is null || userId == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var optionIds = request.OptionIds.Distinct().ToList(); if (optionIds.Count == 0) diff --git a/backend/src/CCE.Application/Community/Commands/CreateCommunity/CreateCommunityCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/CreateCommunity/CreateCommunityCommandHandler.cs index 7ee233b3..20cf3868 100644 --- a/backend/src/CCE.Application/Community/Commands/CreateCommunity/CreateCommunityCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/CreateCommunity/CreateCommunityCommandHandler.cs @@ -31,7 +31,7 @@ public CreateCommunityCommandHandler( public async Task> Handle(CreateCommunityCommand request, CancellationToken cancellationToken) { var userId = _currentUser.GetUserId(); - if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); + if (userId is null || userId == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); if (await _repo.SlugExistsAsync(request.Slug, cancellationToken).ConfigureAwait(false)) return _msg.Conflict(MessageKeys.General.DUPLICATE_VALUE); diff --git a/backend/src/CCE.Application/Community/Commands/CreatePost/CreatePostCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/CreatePost/CreatePostCommandHandler.cs index c1c78022..53eefddf 100644 --- a/backend/src/CCE.Application/Community/Commands/CreatePost/CreatePostCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/CreatePost/CreatePostCommandHandler.cs @@ -57,7 +57,7 @@ public async Task> Handle(CreatePostCommand request, Cancellation { var authorId = _currentUser.GetUserId(); if (authorId is null || authorId == Guid.Empty) - return _msg.NotAuthenticated(); + return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); if (!await _accessGuard.CanPostAsync(request.CommunityId, authorId.Value, cancellationToken).ConfigureAwait(false)) { @@ -78,7 +78,7 @@ public async Task> Handle(CreatePostCommand request, Cancellation } if (!await _repo.TopicExistsAsync(request.TopicId, cancellationToken).ConfigureAwait(false)) - return _msg.TopicNotFound(); + return _msg.NotFound(MessageKeys.Community.TOPIC_NOT_FOUND); var sanitized = request.Content is null ? null : _sanitizer.Sanitize(request.Content); var post = Post.CreateDraft(request.CommunityId, request.TopicId, authorId.Value, request.Type, @@ -102,18 +102,18 @@ public async Task> Handle(CreatePostCommand request, Cancellation foreach (var att in request.Attachments) { if (!assets.TryGetValue(att.AssetFileId, out var asset)) - return _msg.AssetNotFound(); + return _msg.NotFound(MessageKeys.Content.ASSET_NOT_FOUND); if (asset.VirusScanStatus != Domain.Content.VirusScanStatus.Clean) - return _msg.AssetNotClean(); + return _msg.BusinessRule(MessageKeys.Content.ASSET_NOT_CLEAN); var allowed = att.Kind == Domain.Community.AttachmentKind.Media ? PostAttachmentPolicy.MediaMimeTypes : PostAttachmentPolicy.DocumentMimeTypes; if (!allowed.Contains(asset.MimeType)) - return _msg.InvalidFileType(); + return _msg.BusinessRule(MessageKeys.Media.INVALID_FILE_TYPE); if (att.Kind == Domain.Community.AttachmentKind.Document && asset.SizeBytes > PostAttachmentPolicy.MaxDocumentSizeBytes) - return _msg.FileTooLarge(); + return _msg.BusinessRule(MessageKeys.Media.FILE_TOO_LARGE); post.AddAttachment(att.AssetFileId, att.Kind, att.SortOrder, att.MetadataJson); } diff --git a/backend/src/CCE.Application/Community/Commands/CreateReply/CreateReplyCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/CreateReply/CreateReplyCommandHandler.cs index 122a46ba..d5e2f9c9 100644 --- a/backend/src/CCE.Application/Community/Commands/CreateReply/CreateReplyCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/CreateReply/CreateReplyCommandHandler.cs @@ -52,7 +52,7 @@ public CreateReplyCommandHandler( public async Task> Handle(CreateReplyCommand request, CancellationToken cancellationToken) { var authorId = _currentUser.GetUserId(); - if (authorId is null || authorId == Guid.Empty) return _msg.NotAuthenticated(); + if (authorId is null || authorId == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var post = await _repo.GetPostAsync(request.PostId, cancellationToken).ConfigureAwait(false); if (post is null) return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); diff --git a/backend/src/CCE.Application/Community/Commands/DeleteDraft/DeleteDraftCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/DeleteDraft/DeleteDraftCommandHandler.cs index bf7d88a8..e6ea7c21 100644 --- a/backend/src/CCE.Application/Community/Commands/DeleteDraft/DeleteDraftCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/DeleteDraft/DeleteDraftCommandHandler.cs @@ -27,7 +27,7 @@ public DeleteDraftCommandHandler( public async Task> Handle(DeleteDraftCommand request, CancellationToken cancellationToken) { var userId = _currentUser.GetUserId(); - if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); + if (userId is null || userId == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var post = await _repo.GetAsync(request.PostId, cancellationToken).ConfigureAwait(false); if (post is null) return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); diff --git a/backend/src/CCE.Application/Community/Commands/DeleteTopic/DeleteTopicCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/DeleteTopic/DeleteTopicCommandHandler.cs index 4f893e78..f1c0ef45 100644 --- a/backend/src/CCE.Application/Community/Commands/DeleteTopic/DeleteTopicCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/DeleteTopic/DeleteTopicCommandHandler.cs @@ -33,11 +33,11 @@ public async Task> Handle(DeleteTopicCommand request, Cancell { var topic = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (topic is null) - return _messages.TopicNotFound(); + return _messages.NotFound(MessageKeys.Community.TOPIC_NOT_FOUND); var deletedById = _currentUser.GetUserId(); if (deletedById is null) - return _messages.NotAuthenticated(); + return _messages.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); topic.SoftDelete(deletedById.Value, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Community/Commands/JoinCommunity/JoinCommunityCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/JoinCommunity/JoinCommunityCommandHandler.cs index dc4c1c99..a86ba8f4 100644 --- a/backend/src/CCE.Application/Community/Commands/JoinCommunity/JoinCommunityCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/JoinCommunity/JoinCommunityCommandHandler.cs @@ -31,7 +31,7 @@ public JoinCommunityCommandHandler( public async Task> Handle(JoinCommunityCommand request, CancellationToken cancellationToken) { var userId = _currentUser.GetUserId(); - if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); + if (userId is null || userId == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var community = await _repo.GetAsync(request.CommunityId, cancellationToken).ConfigureAwait(false); if (community is null || !community.IsActive) diff --git a/backend/src/CCE.Application/Community/Commands/LeaveCommunity/LeaveCommunityCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/LeaveCommunity/LeaveCommunityCommandHandler.cs index b89de8d1..60858217 100644 --- a/backend/src/CCE.Application/Community/Commands/LeaveCommunity/LeaveCommunityCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/LeaveCommunity/LeaveCommunityCommandHandler.cs @@ -26,7 +26,7 @@ public LeaveCommunityCommandHandler( public async Task> Handle(LeaveCommunityCommand request, CancellationToken cancellationToken) { var userId = _currentUser.GetUserId(); - if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); + if (userId is null || userId == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var membership = await _repo.FindMembershipAsync(request.CommunityId, userId.Value, cancellationToken).ConfigureAwait(false); if (membership is not null) diff --git a/backend/src/CCE.Application/Community/Commands/PublishPost/PublishPostCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/PublishPost/PublishPostCommandHandler.cs index d0e958dc..53f3e5e4 100644 --- a/backend/src/CCE.Application/Community/Commands/PublishPost/PublishPostCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/PublishPost/PublishPostCommandHandler.cs @@ -36,7 +36,7 @@ public PublishPostCommandHandler( public async Task> Handle(PublishPostCommand request, CancellationToken cancellationToken) { var userId = _currentUser.GetUserId(); - if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); + if (userId is null || userId == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var post = await _repo.GetAsync(request.PostId, cancellationToken).ConfigureAwait(false); if (post is null) return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); diff --git a/backend/src/CCE.Application/Community/Commands/RejectJoinRequest/RejectJoinRequestCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/RejectJoinRequest/RejectJoinRequestCommandHandler.cs index ff7a2501..a3625c22 100644 --- a/backend/src/CCE.Application/Community/Commands/RejectJoinRequest/RejectJoinRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/RejectJoinRequest/RejectJoinRequestCommandHandler.cs @@ -30,7 +30,7 @@ public RejectJoinRequestCommandHandler( public async Task> Handle(RejectJoinRequestCommand request, CancellationToken cancellationToken) { var by = _currentUser.GetUserId(); - if (by is null || by == Guid.Empty) return _msg.NotAuthenticated(); + if (by is null || by == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var joinRequest = await _repo.GetRequestAsync(request.RequestId, cancellationToken).ConfigureAwait(false); if (joinRequest is null) return _msg.NotFound(MessageKeys.Community.JOIN_REQUEST_NOT_FOUND); diff --git a/backend/src/CCE.Application/Community/Commands/SetCommunityFollow/SetCommunityFollowCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/SetCommunityFollow/SetCommunityFollowCommandHandler.cs index b432f5b3..a452dd9c 100644 --- a/backend/src/CCE.Application/Community/Commands/SetCommunityFollow/SetCommunityFollowCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/SetCommunityFollow/SetCommunityFollowCommandHandler.cs @@ -31,7 +31,7 @@ public SetCommunityFollowCommandHandler( public async Task> Handle(SetCommunityFollowCommand request, CancellationToken cancellationToken) { var userId = _currentUser.GetUserId(); - if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); + if (userId is null || userId == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); if (request.Status == FollowStatus.Followed) { diff --git a/backend/src/CCE.Application/Community/Commands/SetPostFollow/SetPostFollowCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/SetPostFollow/SetPostFollowCommandHandler.cs index c38e5a2a..de891381 100644 --- a/backend/src/CCE.Application/Community/Commands/SetPostFollow/SetPostFollowCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/SetPostFollow/SetPostFollowCommandHandler.cs @@ -32,7 +32,7 @@ public SetPostFollowCommandHandler( public async Task> Handle(SetPostFollowCommand request, CancellationToken cancellationToken) { var userId = _currentUser.GetUserId(); - if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); + if (userId is null || userId == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); if (request.Status == FollowStatus.Followed) { diff --git a/backend/src/CCE.Application/Community/Commands/SetTopicFollow/SetTopicFollowCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/SetTopicFollow/SetTopicFollowCommandHandler.cs index 41145c4c..775d87c0 100644 --- a/backend/src/CCE.Application/Community/Commands/SetTopicFollow/SetTopicFollowCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/SetTopicFollow/SetTopicFollowCommandHandler.cs @@ -32,7 +32,7 @@ public SetTopicFollowCommandHandler( public async Task> Handle(SetTopicFollowCommand request, CancellationToken cancellationToken) { var userId = _currentUser.GetUserId(); - if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); + if (userId is null || userId == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); if (request.Status == FollowStatus.Followed) { diff --git a/backend/src/CCE.Application/Community/Commands/SetUserFollow/SetUserFollowCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/SetUserFollow/SetUserFollowCommandHandler.cs index f5998451..31285c44 100644 --- a/backend/src/CCE.Application/Community/Commands/SetUserFollow/SetUserFollowCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/SetUserFollow/SetUserFollowCommandHandler.cs @@ -34,14 +34,14 @@ public SetUserFollowCommandHandler( public async Task> Handle(SetUserFollowCommand request, CancellationToken cancellationToken) { var followerId = _currentUser.GetUserId(); - if (followerId is null || followerId == Guid.Empty) return _msg.NotAuthenticated(); + if (followerId is null || followerId == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); if (request.Status == FollowStatus.Followed) { - if (followerId.Value == request.UserId) return _msg.CannotFollowSelf(); + if (followerId.Value == request.UserId) return _msg.ValidationError(MessageKeys.Community.CANNOT_FOLLOW_SELF, new[] { _msg.Field("userId", MessageKeys.Community.CANNOT_FOLLOW_SELF) }); var followed = await _userRepo.FindAsync(request.UserId, cancellationToken).ConfigureAwait(false); - if (followed is null) return _msg.UserNotFound(); + if (followed is null) return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); // Idempotent: only create + bump counts when not already following var existing = await _service.FindUserFollowAsync(followerId.Value, request.UserId, cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Community/Commands/UpdateDraft/UpdateDraftCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/UpdateDraft/UpdateDraftCommandHandler.cs index a0f19f96..128c5636 100644 --- a/backend/src/CCE.Application/Community/Commands/UpdateDraft/UpdateDraftCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/UpdateDraft/UpdateDraftCommandHandler.cs @@ -34,7 +34,7 @@ public UpdateDraftCommandHandler( public async Task> Handle(UpdateDraftCommand request, CancellationToken cancellationToken) { var userId = _currentUser.GetUserId(); - if (userId is null || userId == Guid.Empty) return _msg.NotAuthenticated(); + if (userId is null || userId == Guid.Empty) return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var post = await _repo.GetAsync(request.PostId, cancellationToken).ConfigureAwait(false); if (post is null) return _msg.NotFound(MessageKeys.Community.POST_NOT_FOUND); diff --git a/backend/src/CCE.Application/Community/Commands/UpdateTopic/UpdateTopicCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/UpdateTopic/UpdateTopicCommandHandler.cs index ef0c1e39..39494ae8 100644 --- a/backend/src/CCE.Application/Community/Commands/UpdateTopic/UpdateTopicCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/UpdateTopic/UpdateTopicCommandHandler.cs @@ -28,7 +28,7 @@ public async Task> Handle(UpdateTopicCommand request, Cancell { var topic = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (topic is null) - return _messages.TopicNotFound(); + return _messages.NotFound(MessageKeys.Community.TOPIC_NOT_FOUND); topic.UpdateContent(request.NameAr, request.NameEn, request.DescriptionAr, request.DescriptionEn); topic.Reorder(request.OrderIndex); diff --git a/backend/src/CCE.Application/Community/Commands/VotePost/VotePostCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/VotePost/VotePostCommandHandler.cs index cf712fd2..038033fa 100644 --- a/backend/src/CCE.Application/Community/Commands/VotePost/VotePostCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/VotePost/VotePostCommandHandler.cs @@ -44,7 +44,7 @@ public async Task> Handle(VotePostCommand request, Cancellati { var userId = _currentUser.GetUserId(); if (userId is null || userId == Guid.Empty) - return _msg.NotAuthenticated(); + return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var post = await _repo.GetPostAsync(request.PostId, cancellationToken).ConfigureAwait(false); if (post is null) diff --git a/backend/src/CCE.Application/Community/Commands/VoteReply/VoteReplyCommandHandler.cs b/backend/src/CCE.Application/Community/Commands/VoteReply/VoteReplyCommandHandler.cs index d712072d..9619ca6c 100644 --- a/backend/src/CCE.Application/Community/Commands/VoteReply/VoteReplyCommandHandler.cs +++ b/backend/src/CCE.Application/Community/Commands/VoteReply/VoteReplyCommandHandler.cs @@ -40,7 +40,7 @@ public async Task> Handle(VoteReplyCommand request, Cancellat { var userId = _currentUser.GetUserId(); if (userId is null || userId == Guid.Empty) - return _msg.NotAuthenticated(); + return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var reply = await _repo.GetReplyAsync(request.ReplyId, cancellationToken).ConfigureAwait(false); if (reply is null) diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetCommunityUserProfile/GetCommunityUserProfileQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetCommunityUserProfile/GetCommunityUserProfileQueryHandler.cs index 2f5c5e5b..e354838c 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetCommunityUserProfile/GetCommunityUserProfileQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetCommunityUserProfile/GetCommunityUserProfileQueryHandler.cs @@ -31,7 +31,7 @@ public async Task> Handle( .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); - if (user is null) return _msg.UserNotFound(); + if (user is null) return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); var isExpert = await _db.ExpertProfiles.AnyAsync(e => e.UserId == request.UserId, cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetMyTopics/GetMyTopicsQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetMyTopics/GetMyTopicsQueryHandler.cs index 1e913691..f6937fe2 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetMyTopics/GetMyTopicsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetMyTopics/GetMyTopicsQueryHandler.cs @@ -32,7 +32,7 @@ public async Task>> Handle( { var userId = _currentUser.GetUserId(); if (userId is null || userId == System.Guid.Empty) - return _msg.NotAuthenticated>(); + return _msg.Unauthorized>(MessageKeys.Identity.NOT_AUTHENTICATED); // Step 1: paginate followed topic IDs (with search filter) var query = from f in _db.TopicFollows diff --git a/backend/src/CCE.Application/Community/Public/Queries/GetPublicTopicBySlug/GetPublicTopicBySlugQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/GetPublicTopicBySlug/GetPublicTopicBySlugQueryHandler.cs index 23aaf099..a2d4cbf2 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/GetPublicTopicBySlug/GetPublicTopicBySlugQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/GetPublicTopicBySlug/GetPublicTopicBySlugQueryHandler.cs @@ -31,7 +31,7 @@ public async Task> Handle( .FirstOrDefault(); if (topic is null) - return _messages.TopicNotFound(); + return _messages.NotFound(MessageKeys.Community.TOPIC_NOT_FOUND); return _messages.Ok(ListPublicTopicsQueryHandler.MapToDto(topic), MessageKeys.General.SUCCESS_OPERATION); } diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListMyDrafts/ListMyDraftsQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListMyDrafts/ListMyDraftsQueryHandler.cs index a7ca8906..da9ccfc8 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListMyDrafts/ListMyDraftsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListMyDrafts/ListMyDraftsQueryHandler.cs @@ -28,7 +28,7 @@ public async Task>> Handle( { var userId = _currentUser.GetUserId(); if (userId is null || userId == System.Guid.Empty) - return _msg.NotAuthenticated>(); + return _msg.Unauthorized>(MessageKeys.Identity.NOT_AUTHENTICATED); var paged = await _db.Posts .Where(p => p.AuthorId == userId.Value && p.Status == PostStatus.Draft) diff --git a/backend/src/CCE.Application/Community/Public/Queries/ListMyMentions/ListMyMentionsQueryHandler.cs b/backend/src/CCE.Application/Community/Public/Queries/ListMyMentions/ListMyMentionsQueryHandler.cs index 40839f66..61977a4a 100644 --- a/backend/src/CCE.Application/Community/Public/Queries/ListMyMentions/ListMyMentionsQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Public/Queries/ListMyMentions/ListMyMentionsQueryHandler.cs @@ -26,7 +26,7 @@ public async Task>> Handle( { var userId = _currentUser.GetUserId(); if (userId is null || userId == System.Guid.Empty) - return _msg.NotAuthenticated>(); + return _msg.Unauthorized>(MessageKeys.Identity.NOT_AUTHENTICATED); var paged = await _db.Mentions .Where(m => m.MentionedUserId == userId.Value) diff --git a/backend/src/CCE.Application/Community/Queries/GetTopicById/GetTopicByIdQueryHandler.cs b/backend/src/CCE.Application/Community/Queries/GetTopicById/GetTopicByIdQueryHandler.cs index fa79ad0e..6312360f 100644 --- a/backend/src/CCE.Application/Community/Queries/GetTopicById/GetTopicByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Community/Queries/GetTopicById/GetTopicByIdQueryHandler.cs @@ -27,7 +27,7 @@ public async Task> Handle(GetTopicByIdQuery request, Cancella .ConfigureAwait(false); var topic = list.SingleOrDefault(); if (topic is null) - return _messages.TopicNotFound(); + return _messages.NotFound(MessageKeys.Community.TOPIC_NOT_FOUND); return _messages.Ok(ListTopicsQueryHandler.MapToDto(topic), MessageKeys.General.SUCCESS_OPERATION); } diff --git a/backend/src/CCE.Application/Content/Commands/ApproveCountryResourceRequest/ApproveCountryResourceRequestCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/ApproveCountryResourceRequest/ApproveCountryResourceRequestCommandHandler.cs index 8a99b42a..e1e4e3b4 100644 --- a/backend/src/CCE.Application/Content/Commands/ApproveCountryResourceRequest/ApproveCountryResourceRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/ApproveCountryResourceRequest/ApproveCountryResourceRequestCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content.Dtos; using CCE.Application.Messages; @@ -37,7 +37,7 @@ public async Task> Handle( { var entity = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (entity is null) - return _messages.CountryContentRequestNotFound(); + return _messages.NotFound(MessageKeys.Content.COUNTRY_RESOURCE_REQUEST_NOT_FOUND); var approvedById = _currentUser.GetUserId() ?? throw new DomainException("Cannot approve from a request without a user identity."); @@ -49,13 +49,13 @@ public async Task> Handle( catch (DomainException) { // ERR031 — request is not in Pending state (already approved or rejected) - return _messages.CountryRequestProcessingFailed(); + return _messages.BusinessRule(MessageKeys.Content.COUNTRY_REQUEST_PROCESSING_FAILED); } await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); // CON023 - return _messages.CountryRequestProcessed(MapToDto(entity)); + return _messages.Ok(MapToDto(entity), MessageKeys.Content.COUNTRY_REQUEST_PROCESSED); } internal static CountryContentRequestDto MapToDto(CountryContentRequest e) => new( diff --git a/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandHandler.cs index 71b99c99..580b6ec5 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateNews/CreateNewsCommandHandler.cs @@ -36,7 +36,7 @@ public async Task> Handle(CreateNewsCommand request, Cancellat { var authorId = _currentUser.GetUserId(); if (authorId is null) - return _messages.NotAuthenticated(); + return _messages.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var topicExists = await _db.Topics.Where(t => t.Id == request.TopicId).CountAsyncEither(cancellationToken) > 0; if (!topicExists) diff --git a/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandHandler.cs index 2ae72174..e6ea6cf2 100644 --- a/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/CreateResource/CreateResourceCommandHandler.cs @@ -40,13 +40,13 @@ public CreateResourceCommandHandler( var asset = assets.SingleOrDefault(); if (asset is null) - return _messages.AssetNotFound(); + return _messages.NotFound(MessageKeys.Content.ASSET_NOT_FOUND); if (asset.VirusScanStatus != VirusScanStatus.Clean) - return _messages.AssetNotClean(); + return _messages.BusinessRule(MessageKeys.Content.ASSET_NOT_CLEAN); var categoryExists = await ExistsAsync(_db.ResourceCategories.Where(c => c.Id == request.CategoryId), cancellationToken).ConfigureAwait(false); if (!categoryExists) - return _messages.CategoryNotFound(); + return _messages.NotFound(MessageKeys.Content.CATEGORY_NOT_FOUND); var countryIds = request.CountryIds.Distinct().ToList(); if (countryIds.Count > 0) @@ -61,7 +61,7 @@ public CreateResourceCommandHandler( var uploadedById = _currentUser.GetUserId(); if (uploadedById is null) - return _messages.NotAuthenticated(); + return _messages.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); var resource = Resource.Draft( request.TitleAr, diff --git a/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommandHandler.cs index 99267b5c..1c402eab 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteEvent/DeleteEventCommandHandler.cs @@ -33,11 +33,11 @@ public async Task> Handle(DeleteEventCommand request, Cancell { var ev = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (ev is null) - return _messages.EventNotFound(); + return _messages.NotFound(MessageKeys.Content.EVENT_NOT_FOUND); var userId = _currentUser.GetUserId(); if (userId is null) - return _messages.NotAuthenticated(); + return _messages.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); ev.SoftDelete(userId.Value, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Content/Commands/DeleteHomepageSection/DeleteHomepageSectionCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteHomepageSection/DeleteHomepageSectionCommandHandler.cs index 1bc1c732..4846fe69 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteHomepageSection/DeleteHomepageSectionCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteHomepageSection/DeleteHomepageSectionCommandHandler.cs @@ -32,7 +32,7 @@ public async Task> Handle(DeleteHomepageSectionCommand reques var deletedById = _currentUser.GetUserId(); if (deletedById is null) { - return _msg.NotAuthenticated(); + return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); } section.SoftDelete(deletedById.Value, _clock); diff --git a/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommandHandler.cs index 9b70f3f8..ad839771 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteNews/DeleteNewsCommandHandler.cs @@ -33,11 +33,11 @@ public async Task> Handle(DeleteNewsCommand request, Cancella { var news = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (news is null) - return _messages.NewsNotFound(); + return _messages.NotFound(MessageKeys.Content.NEWS_NOT_FOUND); var userId = _currentUser.GetUserId(); if (userId is null) - return _messages.NotAuthenticated(); + return _messages.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); news.SoftDelete(userId.Value, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Content/Commands/DeletePage/DeletePageCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeletePage/DeletePageCommandHandler.cs index 1c2fa663..fe213d45 100644 --- a/backend/src/CCE.Application/Content/Commands/DeletePage/DeletePageCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeletePage/DeletePageCommandHandler.cs @@ -27,13 +27,13 @@ public async Task> Handle(DeletePageCommand request, Cancella var page = await _service.FindAsync(request.Id, cancellationToken).ConfigureAwait(false); if (page is null) { - return _msg.PageNotFound(); + return _msg.NotFound(MessageKeys.Content.PAGE_NOT_FOUND); } var deletedById = _currentUser.GetUserId(); if (deletedById is null) { - return _msg.NotAuthenticated(); + return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); } page.SoftDelete(deletedById.Value, _clock); diff --git a/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommandHandler.cs index f66b1258..304d7774 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteResource/DeleteResourceCommandHandler.cs @@ -33,11 +33,11 @@ public async Task> Handle(DeleteResourceCommand request, Canc { var resource = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (resource is null) - return _messages.ResourceNotFound(); + return _messages.NotFound(MessageKeys.Content.RESOURCE_NOT_FOUND); var userId = _currentUser.GetUserId(); if (userId is null) - return _messages.NotAuthenticated(); + return _messages.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); resource.SoftDelete(userId.Value, _clock); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Content/Commands/DeleteResourceCategory/DeleteResourceCategoryCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/DeleteResourceCategory/DeleteResourceCategoryCommandHandler.cs index bc531900..2b963219 100644 --- a/backend/src/CCE.Application/Content/Commands/DeleteResourceCategory/DeleteResourceCategoryCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/DeleteResourceCategory/DeleteResourceCategoryCommandHandler.cs @@ -26,7 +26,7 @@ public async Task> Handle(DeleteResourceCategoryCommand reque { var category = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (category is null) - return _messages.CategoryNotFound(); + return _messages.NotFound(MessageKeys.Content.CATEGORY_NOT_FOUND); category.Deactivate(); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommandHandler.cs index ee14eb26..886cc14c 100644 --- a/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/PublishNews/PublishNewsCommandHandler.cs @@ -32,7 +32,7 @@ public async Task> Handle(PublishNewsCommand request, Cancella { var news = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (news is null) - return _messages.NewsNotFound(); + return _messages.NotFound(MessageKeys.Content.NEWS_NOT_FOUND); var expectedRowVersion = news.RowVersion; news.Publish(_clock); diff --git a/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommandHandler.cs index 69254361..deb11d90 100644 --- a/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/PublishResource/PublishResourceCommandHandler.cs @@ -31,7 +31,7 @@ public PublishResourceCommandHandler( { var resource = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (resource is null) - return _messages.ResourceNotFound(); + return _messages.NotFound(MessageKeys.Content.RESOURCE_NOT_FOUND); var assets = await _db.AssetFiles .Where(a => a.Id == resource.AssetFileId) @@ -39,9 +39,9 @@ public PublishResourceCommandHandler( .ConfigureAwait(false); var asset = assets.SingleOrDefault(); if (asset is null) - return _messages.AssetNotFound(); + return _messages.NotFound(MessageKeys.Content.ASSET_NOT_FOUND); if (asset.VirusScanStatus != VirusScanStatus.Clean) - return _messages.AssetNotClean(); + return _messages.BusinessRule(MessageKeys.Content.ASSET_NOT_CLEAN); var expectedRowVersion = resource.RowVersion; resource.Publish(_clock); diff --git a/backend/src/CCE.Application/Content/Commands/RejectCountryResourceRequest/RejectCountryResourceRequestCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/RejectCountryResourceRequest/RejectCountryResourceRequestCommandHandler.cs index 04df24aa..597fb41c 100644 --- a/backend/src/CCE.Application/Content/Commands/RejectCountryResourceRequest/RejectCountryResourceRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/RejectCountryResourceRequest/RejectCountryResourceRequestCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Content.Commands.ApproveCountryResourceRequest; using CCE.Application.Content.Dtos; @@ -38,7 +38,7 @@ public async Task> Handle( { var entity = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (entity is null) - return _messages.CountryContentRequestNotFound(); + return _messages.NotFound(MessageKeys.Content.COUNTRY_RESOURCE_REQUEST_NOT_FOUND); var rejectedById = _currentUser.GetUserId() ?? throw new DomainException("Cannot reject from a request without a user identity."); @@ -50,12 +50,12 @@ public async Task> Handle( catch (DomainException) { // ERR031 — request is not in Pending state (already approved or rejected) - return _messages.CountryRequestProcessingFailed(); + return _messages.BusinessRule(MessageKeys.Content.COUNTRY_REQUEST_PROCESSING_FAILED); } await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); // CON023 - return _messages.CountryRequestProcessed(ApproveCountryResourceRequestCommandHandler.MapToDto(entity)); + return _messages.Ok(ApproveCountryResourceRequestCommandHandler.MapToDto(entity), MessageKeys.Content.COUNTRY_REQUEST_PROCESSED); } } diff --git a/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandHandler.cs index bc7daff0..294efbfd 100644 --- a/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/RescheduleEvent/RescheduleEventCommandHandler.cs @@ -29,7 +29,7 @@ public async Task> Handle(RescheduleEventCommand request, Can { var ev = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (ev is null) - return _messages.EventNotFound(); + return _messages.NotFound(MessageKeys.Content.EVENT_NOT_FOUND); var expectedRowVersion = ev.RowVersion; ev.Reschedule(request.StartsOn, request.EndsOn); diff --git a/backend/src/CCE.Application/Content/Commands/SubmitCountryContentRequest/SubmitCountryContentRequestCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/SubmitCountryContentRequest/SubmitCountryContentRequestCommandHandler.cs index 25401d95..f5a36ca9 100644 --- a/backend/src/CCE.Application/Content/Commands/SubmitCountryContentRequest/SubmitCountryContentRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/SubmitCountryContentRequest/SubmitCountryContentRequestCommandHandler.cs @@ -48,7 +48,7 @@ public SubmitCountryContentRequestCommandHandler( var countryId = await ResolveCountryIdAsync(request.CountryId, cancellationToken).ConfigureAwait(false); if (countryId is null) - return _messages.CountryScopeForbidden(); + return _messages.Forbidden(MessageKeys.Country.COUNTRY_SCOPE_FORBIDDEN); CountryContentRequest contentRequest = request.Content switch { diff --git a/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandHandler.cs index be5cd520..80783b56 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateEvent/UpdateEventCommandHandler.cs @@ -34,7 +34,7 @@ public async Task> Handle(UpdateEventCommand request, Cancell q => q.Include(e => e.Tags), cancellationToken).ConfigureAwait(false); if (ev is null) - return _messages.EventNotFound(); + return _messages.NotFound(MessageKeys.Content.EVENT_NOT_FOUND); var topicExists = await _db.Topics.Where(t => t.Id == request.TopicId).CountAsyncEither(cancellationToken) > 0; if (!topicExists) diff --git a/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandHandler.cs index 04bad0a4..95402ba2 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateNews/UpdateNewsCommandHandler.cs @@ -34,7 +34,7 @@ public async Task> Handle(UpdateNewsCommand request, Cancellat q => q.Include(n => n.Tags), cancellationToken).ConfigureAwait(false); if (news is null) - return _messages.NewsNotFound(); + return _messages.NotFound(MessageKeys.Content.NEWS_NOT_FOUND); var topicExists = await _db.Topics.Where(t => t.Id == request.TopicId).CountAsyncEither(cancellationToken) > 0; if (!topicExists) diff --git a/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandHandler.cs index a8db42fd..bcb6799c 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateResource/UpdateResourceCommandHandler.cs @@ -32,11 +32,11 @@ public UpdateResourceCommandHandler( q => q.Include(r => r.Countries), cancellationToken).ConfigureAwait(false); if (resource is null) - return _messages.ResourceNotFound(); + return _messages.NotFound(MessageKeys.Content.RESOURCE_NOT_FOUND); var categoryExists = await ExistsAsync(_db.ResourceCategories.Where(c => c.Id == request.CategoryId), cancellationToken).ConfigureAwait(false); if (!categoryExists) - return _messages.CategoryNotFound(); + return _messages.NotFound(MessageKeys.Content.CATEGORY_NOT_FOUND); var countryIds = request.CountryIds.Distinct().ToList(); if (countryIds.Count > 0) diff --git a/backend/src/CCE.Application/Content/Commands/UpdateResourceCategory/UpdateResourceCategoryCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UpdateResourceCategory/UpdateResourceCategoryCommandHandler.cs index 056021c7..a9e3d494 100644 --- a/backend/src/CCE.Application/Content/Commands/UpdateResourceCategory/UpdateResourceCategoryCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UpdateResourceCategory/UpdateResourceCategoryCommandHandler.cs @@ -28,7 +28,7 @@ public async Task> Handle(UpdateResourceCategoryCo { var category = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (category is null) - return _messages.CategoryNotFound(); + return _messages.NotFound(MessageKeys.Content.CATEGORY_NOT_FOUND); category.UpdateNames(request.NameAr, request.NameEn); category.Reorder(request.OrderIndex); 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 db6f031a..39661574 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicEventById/GetPublicEventByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicEventById/GetPublicEventByIdQueryHandler.cs @@ -28,7 +28,7 @@ public async Task> Handle(GetPublicEventByIdQuery reque .ConfigureAwait(false); var ev = list.SingleOrDefault(); if (ev is null) - return _messages.EventNotFound(); + return _messages.NotFound(MessageKeys.Content.EVENT_NOT_FOUND); var topics = await _db.Topics.Where(t => t.Id == ev.TopicId) .ToListAsyncEither(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Content/Public/Queries/GetPublicNewsById/GetPublicNewsByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Public/Queries/GetPublicNewsById/GetPublicNewsByIdQueryHandler.cs index 47910484..8ce7bf95 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicNewsById/GetPublicNewsByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicNewsById/GetPublicNewsByIdQueryHandler.cs @@ -28,7 +28,7 @@ public async Task> Handle(GetPublicNewsByIdQuery request .ConfigureAwait(false); var news = list.SingleOrDefault(); if (news is null) - return _messages.NewsNotFound(); + return _messages.NotFound(MessageKeys.Content.NEWS_NOT_FOUND); var topics = await _db.Topics.Where(t => t.Id == news.TopicId) .ToListAsyncEither(cancellationToken).ConfigureAwait(false); 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 bdabb005..bf25f3e8 100644 --- a/backend/src/CCE.Application/Content/Public/Queries/GetPublicResourceById/GetPublicResourceByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Public/Queries/GetPublicResourceById/GetPublicResourceByIdQueryHandler.cs @@ -30,7 +30,7 @@ public async Task> Handle(GetPublicResourceByIdQuery .ConfigureAwait(false); var resource = list.SingleOrDefault(); if (resource is null || resource.PublishedOn is null) - return _messages.ResourceNotFound(); + return _messages.NotFound(MessageKeys.Content.RESOURCE_NOT_FOUND); return _messages.Ok(await MapToDtoAsync(resource, cancellationToken).ConfigureAwait(false), MessageKeys.General.SUCCESS_OPERATION); } diff --git a/backend/src/CCE.Application/Content/Queries/DownloadFile/DownloadFileQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/DownloadFile/DownloadFileQueryHandler.cs index 0ed5d42f..5f55a149 100644 --- a/backend/src/CCE.Application/Content/Queries/DownloadFile/DownloadFileQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/DownloadFile/DownloadFileQueryHandler.cs @@ -35,7 +35,7 @@ public async Task> Handle(DownloadFileQuery reques .ConfigureAwait(false); if (media is null) - return _msg.MediaFileNotFound(); + return _msg.NotFound(MessageKeys.Media.MEDIA_FILE_NOT_FOUND); var storage = _storageFactory.GetStorage(DownloadFileType.Media); Stream stream; @@ -47,7 +47,7 @@ public async Task> Handle(DownloadFileQuery reques } catch (FileNotFoundException) { - return _msg.MediaFileNotFound(); + return _msg.NotFound(MessageKeys.Media.MEDIA_FILE_NOT_FOUND); } var payload = new DownloadFilePayload(stream, media.MimeType, media.OriginalFileName); @@ -59,10 +59,10 @@ public async Task> Handle(DownloadFileQuery reques .ConfigureAwait(false); if (asset is null) - return _msg.AssetNotFound(); + return _msg.NotFound(MessageKeys.Content.ASSET_NOT_FOUND); if (asset.VirusScanStatus != VirusScanStatus.Clean) - return _msg.AssetNotClean(); + return _msg.BusinessRule(MessageKeys.Content.ASSET_NOT_CLEAN); var assetStorage = _storageFactory.GetStorage(DownloadFileType.Asset); Stream assetStream; @@ -74,7 +74,7 @@ public async Task> Handle(DownloadFileQuery reques } catch (FileNotFoundException) { - return _msg.AssetNotFound(); + return _msg.NotFound(MessageKeys.Content.ASSET_NOT_FOUND); } var assetPayload = new DownloadFilePayload(assetStream, asset.MimeType, asset.OriginalFileName); diff --git a/backend/src/CCE.Application/Content/Queries/GetAssetById/GetAssetByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetAssetById/GetAssetByIdQueryHandler.cs index 9a662f1d..6f5982c0 100644 --- a/backend/src/CCE.Application/Content/Queries/GetAssetById/GetAssetByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetAssetById/GetAssetByIdQueryHandler.cs @@ -29,7 +29,7 @@ public async Task> Handle(GetAssetByIdQuery request, Canc var asset = list.SingleOrDefault(); if (asset is null) - return _msg.AssetNotFound(); + return _msg.NotFound(MessageKeys.Content.ASSET_NOT_FOUND); var publicUrl = asset.Url.StartsWith("http", System.StringComparison.OrdinalIgnoreCase) ? asset.Url diff --git a/backend/src/CCE.Application/Content/Queries/GetCountryContentRequest/GetCountryContentRequestQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetCountryContentRequest/GetCountryContentRequestQueryHandler.cs index 39c37b1c..5f82a5c2 100644 --- a/backend/src/CCE.Application/Content/Queries/GetCountryContentRequest/GetCountryContentRequestQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetCountryContentRequest/GetCountryContentRequestQueryHandler.cs @@ -39,7 +39,7 @@ public async Task> Handle( var entity = items.FirstOrDefault(); if (entity is null) - return _messages.CountryContentRequestNotFound(); + return _messages.NotFound(MessageKeys.Content.COUNTRY_RESOURCE_REQUEST_NOT_FOUND); var dto = new CountryContentRequestDto( entity.Id, entity.CountryId, entity.RequestedById, entity.Type, entity.Status, diff --git a/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQueryHandler.cs index 1ef2ccba..17bb983d 100644 --- a/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetEventById/GetEventByIdQueryHandler.cs @@ -25,7 +25,7 @@ public async Task> Handle(GetEventByIdQuery request, Cancella var list = await _db.Events.Where(e => e.Id == request.Id).ToListAsyncEither(cancellationToken).ConfigureAwait(false); var ev = list.SingleOrDefault(); if (ev is null) - return _messages.EventNotFound(); + return _messages.NotFound(MessageKeys.Content.EVENT_NOT_FOUND); var topics = await _db.Topics.Where(t => t.Id == ev.TopicId) .ToListAsyncEither(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQueryHandler.cs index d7e47cba..bf2507ea 100644 --- a/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetNewsById/GetNewsByIdQueryHandler.cs @@ -25,7 +25,7 @@ public async Task> Handle(GetNewsByIdQuery request, Cancellati var list = await _db.News.Where(n => n.Id == request.Id).ToListAsyncEither(cancellationToken).ConfigureAwait(false); var news = list.SingleOrDefault(); if (news is null) - return _messages.NewsNotFound(); + return _messages.NotFound(MessageKeys.Content.NEWS_NOT_FOUND); var topics = await _db.Topics.Where(t => t.Id == news.TopicId) .ToListAsyncEither(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQueryHandler.cs index 17503356..fe256e65 100644 --- a/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetResourceById/GetResourceByIdQueryHandler.cs @@ -30,7 +30,7 @@ public async Task> Handle(GetResourceByIdQuery request, Ca .ConfigureAwait(false); var resource = list.SingleOrDefault(); return resource is null - ? _messages.ResourceNotFound() + ? _messages.NotFound(MessageKeys.Content.RESOURCE_NOT_FOUND) : _messages.Ok(await MapToDtoAsync(resource, cancellationToken).ConfigureAwait(false), MessageKeys.General.SUCCESS_OPERATION); } diff --git a/backend/src/CCE.Application/Content/Queries/GetResourceCategoryById/GetResourceCategoryByIdQueryHandler.cs b/backend/src/CCE.Application/Content/Queries/GetResourceCategoryById/GetResourceCategoryByIdQueryHandler.cs index 088ac1f4..d53526f6 100644 --- a/backend/src/CCE.Application/Content/Queries/GetResourceCategoryById/GetResourceCategoryByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Content/Queries/GetResourceCategoryById/GetResourceCategoryByIdQueryHandler.cs @@ -27,7 +27,7 @@ public async Task> Handle(GetResourceCategoryByIdQ .ConfigureAwait(false); var category = list.SingleOrDefault(); if (category is null) - return _messages.CategoryNotFound(); + return _messages.NotFound(MessageKeys.Content.CATEGORY_NOT_FOUND); return _messages.Ok(MapToDto(category), MessageKeys.General.SUCCESS_OPERATION); } diff --git a/backend/src/CCE.Application/Country/Commands/UpsertCountryProfile/UpsertCountryProfileCommandHandler.cs b/backend/src/CCE.Application/Country/Commands/UpsertCountryProfile/UpsertCountryProfileCommandHandler.cs index 1805fe72..6c0b3f98 100644 --- a/backend/src/CCE.Application/Country/Commands/UpsertCountryProfile/UpsertCountryProfileCommandHandler.cs +++ b/backend/src/CCE.Application/Country/Commands/UpsertCountryProfile/UpsertCountryProfileCommandHandler.cs @@ -38,7 +38,7 @@ public async Task> Handle(UpsertCountryProfileComman // State reps may only edit their own assigned country; null scope = admin bypass var authorizedIds = await _scope.GetAuthorizedCountryIdsAsync(cancellationToken).ConfigureAwait(false); if (authorizedIds is not null && !authorizedIds.Contains(request.CountryId)) - return _messages.CountryScopeForbidden(); + return _messages.Forbidden(MessageKeys.Country.COUNTRY_SCOPE_FORBIDDEN); var userId = _currentUser.GetUserId() ?? throw new DomainException("Cannot upsert country profile from a request without a user identity."); diff --git a/backend/src/CCE.Application/Country/Queries/GetMyCountryProfile/GetMyCountryProfileQueryHandler.cs b/backend/src/CCE.Application/Country/Queries/GetMyCountryProfile/GetMyCountryProfileQueryHandler.cs index 35ca7f91..238c3962 100644 --- a/backend/src/CCE.Application/Country/Queries/GetMyCountryProfile/GetMyCountryProfileQueryHandler.cs +++ b/backend/src/CCE.Application/Country/Queries/GetMyCountryProfile/GetMyCountryProfileQueryHandler.cs @@ -33,19 +33,19 @@ public async Task> Handle( var authorizedIds = await _scope.GetAuthorizedCountryIdsAsync(cancellationToken).ConfigureAwait(false); // null = admin (no scope restriction) — this endpoint is state-rep only; empty = no assignment if (authorizedIds is not null && authorizedIds.Count == 0) - return _messages.NoCountryAssigned(); + return _messages.NotFound(MessageKeys.Country.NO_COUNTRY_ASSIGNED); // Use first assigned country (state reps typically have one) var countryId = authorizedIds is { Count: > 0 } ? authorizedIds[0] : System.Guid.Empty; if (countryId == System.Guid.Empty) - return _messages.CountryProfileNotFound(); + return _messages.NotFound(MessageKeys.Country.COUNTRY_PROFILE_NOT_FOUND); var profiles = await _db.CountryProfiles .Where(p => p.CountryId == countryId) .ToListAsyncEither(cancellationToken).ConfigureAwait(false); var profile = profiles.FirstOrDefault(); if (profile is null) - return _messages.CountryProfileNotFound(); + return _messages.NotFound(MessageKeys.Country.COUNTRY_PROFILE_NOT_FOUND); CountryKapsarcSnapshot? snapshot = null; var countries = await _db.Countries diff --git a/backend/src/CCE.Application/CountryPublic/Queries/GetPublicCountryProfile/GetPublicCountryProfileQueryHandler.cs b/backend/src/CCE.Application/CountryPublic/Queries/GetPublicCountryProfile/GetPublicCountryProfileQueryHandler.cs index 745f316c..905178fe 100644 --- a/backend/src/CCE.Application/CountryPublic/Queries/GetPublicCountryProfile/GetPublicCountryProfileQueryHandler.cs +++ b/backend/src/CCE.Application/CountryPublic/Queries/GetPublicCountryProfile/GetPublicCountryProfileQueryHandler.cs @@ -32,7 +32,7 @@ public async Task> Handle( .ToListAsyncEither(cancellationToken).ConfigureAwait(false); var country = countries.FirstOrDefault(); if (country is null) - return _messages.CountryNotFound(); + return _messages.NotFound(MessageKeys.Country.COUNTRY_NOT_FOUND); // Editorial profile is optional — return country + KAPSARC data even when absent var profiles = await _db.CountryProfiles diff --git a/backend/src/CCE.Application/Evaluation/Queries/GetEvaluationById/GetEvaluationByIdQueryHandler.cs b/backend/src/CCE.Application/Evaluation/Queries/GetEvaluationById/GetEvaluationByIdQueryHandler.cs index 30d7343e..d8692055 100644 --- a/backend/src/CCE.Application/Evaluation/Queries/GetEvaluationById/GetEvaluationByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Evaluation/Queries/GetEvaluationById/GetEvaluationByIdQueryHandler.cs @@ -28,7 +28,7 @@ public async Task> Handle( .ConfigureAwait(false); if (evaluation is null) - return _msg.EvaluationNotFound(); + return _msg.NotFound(MessageKeys.Evaluation.EVALUATION_NOT_FOUND); var dto = new ServiceEvaluationDto( evaluation.Id, diff --git a/backend/src/CCE.Application/Identity/Auth/AdLogin/AdLoginCommandHandler.cs b/backend/src/CCE.Application/Identity/Auth/AdLogin/AdLoginCommandHandler.cs index a9ed49b1..d1401b61 100644 --- a/backend/src/CCE.Application/Identity/Auth/AdLogin/AdLoginCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Auth/AdLogin/AdLoginCommandHandler.cs @@ -28,9 +28,9 @@ public async Task> Handle(AdLoginCommand request, Cancell return result.Failure switch { - LoginFailureReason.Deactivated => _msg.AccountDeactivated(), + LoginFailureReason.Deactivated => _msg.Forbidden(MessageKeys.Identity.ACCOUNT_DEACTIVATED), LoginFailureReason.None => _msg.Ok(result.Token!, MessageKeys.Identity.AD_LOGIN_SUCCESS), - _ => _msg.InvalidCredentials(), + _ => _msg.Unauthorized(MessageKeys.Identity.INVALID_CREDENTIALS), }; } } diff --git a/backend/src/CCE.Application/Identity/Auth/Login/LoginCommandHandler.cs b/backend/src/CCE.Application/Identity/Auth/Login/LoginCommandHandler.cs index 9ebc9296..3e6f3f5e 100644 --- a/backend/src/CCE.Application/Identity/Auth/Login/LoginCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Auth/Login/LoginCommandHandler.cs @@ -23,10 +23,10 @@ public async Task> Handle(LoginCommand request, Cancellat request.IpAddress, request.UserAgent, ct).ConfigureAwait(false); return result.Failure switch { - LoginFailureReason.Deactivated => _msg.AccountDeactivated(), - LoginFailureReason.ContactNotVerified => _msg.ContactNotVerified(), + LoginFailureReason.Deactivated => _msg.Forbidden(MessageKeys.Identity.ACCOUNT_DEACTIVATED), + LoginFailureReason.ContactNotVerified => _msg.Forbidden(MessageKeys.Identity.CONTACT_NOT_VERIFIED), LoginFailureReason.None => _msg.Ok(result.Token!, MessageKeys.Identity.LOGIN_SUCCESS), - _ => _msg.InvalidCredentials(), + _ => _msg.Unauthorized(MessageKeys.Identity.INVALID_CREDENTIALS), }; } } diff --git a/backend/src/CCE.Application/Identity/Auth/Register/RegisterUserCommandHandler.cs b/backend/src/CCE.Application/Identity/Auth/Register/RegisterUserCommandHandler.cs index 25325042..0ff0bcd2 100644 --- a/backend/src/CCE.Application/Identity/Auth/Register/RegisterUserCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Auth/Register/RegisterUserCommandHandler.cs @@ -23,7 +23,7 @@ public async Task> Handle(RegisterUserCommand request, Can request.EmailAddress, request.Password, request.JobTitle, request.OrganizationName, request.PhoneNumber, request.CountryId, ct).ConfigureAwait(false); - if (result.EmailTaken) return _msg.EmailExists(); + if (result.EmailTaken) return _msg.Conflict(MessageKeys.Identity.EMAIL_EXISTS); if (result.User is null) return _msg.BusinessRule(MessageKeys.Identity.REGISTRATION_FAILED); return _msg.Ok(new AuthUserDto( diff --git a/backend/src/CCE.Application/Identity/Auth/ResetPassword/ResetPasswordCommandHandler.cs b/backend/src/CCE.Application/Identity/Auth/ResetPassword/ResetPasswordCommandHandler.cs index f5b99786..cfaab2b7 100644 --- a/backend/src/CCE.Application/Identity/Auth/ResetPassword/ResetPasswordCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Auth/ResetPassword/ResetPasswordCommandHandler.cs @@ -26,7 +26,7 @@ public async Task> Handle(ResetPasswordCommand request, { return errorKey switch { - MessageKeys.Identity.USER_NOT_FOUND => _msg.UserNotFound(), + MessageKeys.Identity.USER_NOT_FOUND => _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND), MessageKeys.Identity.INVALID_RESET_TOKEN => _msg.Unauthorized(MessageKeys.Identity.INVALID_RESET_TOKEN), _ => _msg.BusinessRule(errorKey), }; diff --git a/backend/src/CCE.Application/Identity/Commands/ApproveExpertRequest/ApproveExpertRequestCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/ApproveExpertRequest/ApproveExpertRequestCommandHandler.cs index 6cb42650..72a6bf6f 100644 --- a/backend/src/CCE.Application/Identity/Commands/ApproveExpertRequest/ApproveExpertRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/ApproveExpertRequest/ApproveExpertRequestCommandHandler.cs @@ -38,12 +38,12 @@ public async Task> Handle( { var registration = await _service.FindIncludingDeletedAsync(request.Id, cancellationToken).ConfigureAwait(false); if (registration is null) - return _msg.ExpertRequestNotFound(); + return _msg.NotFound(MessageKeys.Identity.EXPERT_REQUEST_NOT_FOUND); var approvedById = _currentUser.GetUserId(); if (approvedById is null) { - return _msg.NotAuthenticated(); + return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); } registration.Approve(approvedById.Value, _clock); diff --git a/backend/src/CCE.Application/Identity/Commands/AssignUserRoles/AssignUserRolesCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/AssignUserRoles/AssignUserRolesCommandHandler.cs index 8084832b..42eef739 100644 --- a/backend/src/CCE.Application/Identity/Commands/AssignUserRoles/AssignUserRolesCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/AssignUserRoles/AssignUserRolesCommandHandler.cs @@ -27,7 +27,7 @@ public async Task> Handle(AssignUserRolesCommand request var ok = await _service.ReplaceRolesAsync(request.Id, request.Roles, cancellationToken).ConfigureAwait(false); if (!ok) { - return _msg.UserNotFound(); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); } var result = await _mediator.Send(new GetUserByIdQuery(request.Id), cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Identity/Commands/ChangeUserStatus/ChangeUserStatusCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/ChangeUserStatus/ChangeUserStatusCommandHandler.cs index 96135acc..e824cf44 100644 --- a/backend/src/CCE.Application/Identity/Commands/ChangeUserStatus/ChangeUserStatusCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/ChangeUserStatus/ChangeUserStatusCommandHandler.cs @@ -33,7 +33,7 @@ public async Task> Handle(ChangeUserStatusCommand reques var user = await _service.FindAsync(request.UserId, cancellationToken).ConfigureAwait(false); if (user is null) { - return _msg.UserNotFound(); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); } var newStatus = request.IsActive ? UserStatus.Active : UserStatus.Inactive; diff --git a/backend/src/CCE.Application/Identity/Commands/CreateStateRepAssignment/CreateStateRepAssignmentCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/CreateStateRepAssignment/CreateStateRepAssignmentCommandHandler.cs index 941a2efc..eb51c87f 100644 --- a/backend/src/CCE.Application/Identity/Commands/CreateStateRepAssignment/CreateStateRepAssignmentCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/CreateStateRepAssignment/CreateStateRepAssignmentCommandHandler.cs @@ -43,7 +43,7 @@ public async Task> Handle( var userExists = await ExistsAsync(_db.Users.Where(u => u.Id == request.UserId), cancellationToken).ConfigureAwait(false); if (!userExists) { - return _msg.UserNotFound(); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); } var countryExists = await ExistsAsync(_db.Countries.Where(c => c.Id == request.CountryId), cancellationToken).ConfigureAwait(false); @@ -55,7 +55,7 @@ public async Task> Handle( var assignedById = _currentUser.GetUserId(); if (assignedById is null) { - return _msg.NotAuthenticated(); + return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); } var assignment = StateRepresentativeAssignment.Assign(request.UserId, request.CountryId, assignedById.Value, _clock); diff --git a/backend/src/CCE.Application/Identity/Commands/CreateUser/CreateUserCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/CreateUser/CreateUserCommandHandler.cs index 41ab85c9..de7a9b1a 100644 --- a/backend/src/CCE.Application/Identity/Commands/CreateUser/CreateUserCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/CreateUser/CreateUserCommandHandler.cs @@ -30,7 +30,7 @@ public async Task> Handle(CreateUserCommand request, Can request.FirstName, request.LastName, request.Email, request.PhoneNumber, request.CountryId, request.Role, createdBy, cancellationToken).ConfigureAwait(false); - if (result.EmailTaken) return _msg.EmailExists(); + if (result.EmailTaken) return _msg.Conflict(MessageKeys.Identity.EMAIL_EXISTS); if (result.Failed || result.User is null) return _msg.BusinessRule(MessageKeys.Identity.REGISTRATION_FAILED); var detail = await _mediator.Send(new GetUserByIdQuery(result.User.Id), cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Identity/Commands/DeleteUser/DeleteUserCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/DeleteUser/DeleteUserCommandHandler.cs index 30b73770..1b0f1825 100644 --- a/backend/src/CCE.Application/Identity/Commands/DeleteUser/DeleteUserCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/DeleteUser/DeleteUserCommandHandler.cs @@ -32,7 +32,7 @@ public async Task> Handle(DeleteUserCommand request, Can var user = await _service.FindAsync(request.UserId, cancellationToken).ConfigureAwait(false); if (user is null || user.IsDeleted) { - return _msg.UserNotFound(); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); } var deletedById = _currentUser.GetUserId() diff --git a/backend/src/CCE.Application/Identity/Commands/RejectExpertRequest/RejectExpertRequestCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/RejectExpertRequest/RejectExpertRequestCommandHandler.cs index a26e5281..a80225e1 100644 --- a/backend/src/CCE.Application/Identity/Commands/RejectExpertRequest/RejectExpertRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/RejectExpertRequest/RejectExpertRequestCommandHandler.cs @@ -38,12 +38,12 @@ public async Task> Handle( { var registration = await _service.FindIncludingDeletedAsync(request.Id, cancellationToken).ConfigureAwait(false); if (registration is null) - return _msg.ExpertRequestNotFound(); + return _msg.NotFound(MessageKeys.Identity.EXPERT_REQUEST_NOT_FOUND); var rejectedById = _currentUser.GetUserId(); if (rejectedById is null) { - return _msg.NotAuthenticated(); + return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); } registration.Reject(rejectedById.Value, request.RejectionReasonAr, request.RejectionReasonEn, _clock); diff --git a/backend/src/CCE.Application/Identity/Commands/RevokeStateRepAssignment/RevokeStateRepAssignmentCommandHandler.cs b/backend/src/CCE.Application/Identity/Commands/RevokeStateRepAssignment/RevokeStateRepAssignmentCommandHandler.cs index fa8db205..1de52636 100644 --- a/backend/src/CCE.Application/Identity/Commands/RevokeStateRepAssignment/RevokeStateRepAssignmentCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Commands/RevokeStateRepAssignment/RevokeStateRepAssignmentCommandHandler.cs @@ -39,7 +39,7 @@ public async Task> Handle(RevokeStateRepAssignmentCommand req var revokedById = _currentUser.GetUserId(); if (revokedById is null) { - return _msg.NotAuthenticated(); + return _msg.Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); } assignment.Revoke(revokedById.Value, _clock); diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandHandler.cs index 1ea5f652..f633b732 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmEmailChange/ConfirmEmailChangeCommandHandler.cs @@ -45,16 +45,16 @@ public async Task> Handle( .ConfigureAwait(false); if (otp is null) - return _msg.OtpNotFound(); + return _msg.NotFound(MessageKeys.Verification.OTP_NOT_FOUND); if (otp.IsInvalidated) - return _msg.OtpInvalidated(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_INVALIDATED); if (otp.IsExpired(now)) - return _msg.OtpExpired(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_EXPIRED); if (otp.HasExceededMaxAttempts()) - return _msg.OtpMaxAttempts(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_MAX_ATTEMPTS); // Ownership validation — OTP must belong to the authenticated user if (otp.UserId.HasValue && otp.UserId.Value != request.UserId) @@ -66,7 +66,7 @@ public async Task> Handle( { _otpRepo.Update(otp); await _db.SaveChangesAsync(ct).ConfigureAwait(false); - return _msg.OtpInvalidCode(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_INVALID_CODE); } // WRITE — fetch user via repository @@ -75,7 +75,7 @@ public async Task> Handle( .ConfigureAwait(false); if (user is null) - return _msg.UserNotFound(); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); // Use UserManager to ensure NormalizedEmail and SecurityStamp are properly updated var setEmailResult = await _userManager.SetEmailAsync(user, otp.Contact).ConfigureAwait(false); @@ -96,6 +96,6 @@ public async Task> Handle( // ICceDbContext as unit of work await _db.SaveChangesAsync(ct).ConfigureAwait(false); - return _msg.EmailUpdated(); + return _msg.Ok(MessageKeys.Verification.EMAIL_UPDATED); } } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandHandler.cs index 857d7d38..1efaec07 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/ConfirmPhoneChange/ConfirmPhoneChangeCommandHandler.cs @@ -41,16 +41,16 @@ public async Task> Handle( .ConfigureAwait(false); if (otp is null) - return _msg.OtpNotFound(); + return _msg.NotFound(MessageKeys.Verification.OTP_NOT_FOUND); if (otp.IsInvalidated) - return _msg.OtpInvalidated(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_INVALIDATED); if (otp.IsExpired(now)) - return _msg.OtpExpired(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_EXPIRED); if (otp.HasExceededMaxAttempts()) - return _msg.OtpMaxAttempts(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_MAX_ATTEMPTS); // Ownership validation — OTP must belong to the authenticated user if (otp.UserId.HasValue && otp.UserId.Value != request.UserId) @@ -62,7 +62,7 @@ public async Task> Handle( { _otpRepo.Update(otp); await _db.SaveChangesAsync(ct).ConfigureAwait(false); - return _msg.OtpInvalidCode(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_INVALID_CODE); } // WRITE — fetch user via repository @@ -71,7 +71,7 @@ public async Task> Handle( .ConfigureAwait(false); if (user is null) - return _msg.UserNotFound(); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); // Read CountryId stored at request-time — client does not need to re-send it System.Guid? countryId = null; @@ -93,6 +93,6 @@ public async Task> Handle( // ICceDbContext as unit of work await _db.SaveChangesAsync(ct).ConfigureAwait(false); - return _msg.PhoneUpdated(); + return _msg.Ok(MessageKeys.Verification.PHONE_UPDATED); } } diff --git a/backend/src/CCE.Application/Identity/Public/Commands/ContactChangeOtpService.cs b/backend/src/CCE.Application/Identity/Public/Commands/ContactChangeOtpService.cs index a6d3633c..8394f220 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/ContactChangeOtpService.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/ContactChangeOtpService.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Messages; using CCE.Application.Notifications; using CCE.Application.Verification; @@ -52,7 +52,7 @@ public ContactChangeOtpService( .ConfigureAwait(false); if (existing is not null && !existing.CanResend(now)) - return (null, _msg.OtpCooldownActive()); + return (null, _msg.BusinessRule(MessageKeys.Verification.OTP_COOLDOWN_ACTIVE)); var (plainCode, codeHash) = _codeGenerator.Generate(); diff --git a/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandHandler.cs index 9de14e2e..0978cc1b 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/RequestEmailChange/RequestEmailChangeCommandHandler.cs @@ -40,7 +40,7 @@ public async Task> Handle( .ConfigureAwait(false); if (taken) - return _msg.ContactAlreadyTaken(); + return _msg.Conflict(MessageKeys.Verification.CONTACT_ALREADY_TAKEN); var (entity, fail) = await _otpService.PrepareAsync( request.NewEmail, diff --git a/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandHandler.cs index d9c424fa..499e931d 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/RequestPhoneChange/RequestPhoneChangeCommandHandler.cs @@ -44,7 +44,7 @@ public async Task> Handle( .ConfigureAwait(false); if (taken) - return _msg.ContactAlreadyTaken(); + return _msg.Conflict(MessageKeys.Verification.CONTACT_ALREADY_TAKEN); // Serialize CountryId into ExtraData so it survives to confirm-time without client round-trip var extraData = request.CountryId.HasValue diff --git a/backend/src/CCE.Application/Identity/Public/Commands/SubmitExpertRequest/SubmitExpertRequestCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/SubmitExpertRequest/SubmitExpertRequestCommandHandler.cs index c0270cc8..5b54cc01 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/SubmitExpertRequest/SubmitExpertRequestCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/SubmitExpertRequest/SubmitExpertRequestCommandHandler.cs @@ -40,9 +40,9 @@ public async Task> Handle(SubmitExpertRequestCo var asset = assets.FirstOrDefault(); if (asset is null) - return _msg.AssetNotFound(); + return _msg.NotFound(MessageKeys.Content.ASSET_NOT_FOUND); if (asset.VirusScanStatus != VirusScanStatus.Clean) - return _msg.AssetNotClean(); + return _msg.BusinessRule(MessageKeys.Content.ASSET_NOT_CLEAN); // WRITE: create aggregate via domain factory var entity = ExpertRegistrationRequest.Submit( diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandHandler.cs index 44b2a5b7..8fcaa3fa 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UpdateMyProfile/UpdateMyProfileCommandHandler.cs @@ -25,7 +25,7 @@ public async Task> Handle(UpdateMyProfileCommand reques // fetch via repository var user = await _service.FindAsync(request.UserId, cancellationToken).ConfigureAwait(false); if (user is null) - return _msg.UserNotFound(); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); // domain methods user.UpdateProfile(request.FirstName, request.LastName, request.JobTitle, request.OrganizationName); diff --git a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs index 17560752..3edd89ab 100644 --- a/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Commands/UserInterest/UpsertUserInterestCommandHandler.cs @@ -31,7 +31,7 @@ public async Task> Handle( { var user = await _service.FindAsync(request.UserId, cancellationToken).ConfigureAwait(false); if (user is null) - return _msg.UserNotFound(); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); var errors = new List(); @@ -117,11 +117,11 @@ public async Task> Handle( var jobSectorTopic = currentTopics .FirstOrDefault(t => t.Category == "job_sector" && user.UserInterestTopics.Any(uit => uit.InterestTopicId == t.Id)); - return _msg.InterestUpserted(new UpsertUserInterestResult( + return _msg.Ok(new UpsertUserInterestResult( carbonAreaTopics, knowledgeAssessmentTopic is not null ? new InterestTopicDto(knowledgeAssessmentTopic.Id, knowledgeAssessmentTopic.NameAr, knowledgeAssessmentTopic.NameEn, knowledgeAssessmentTopic.Category, knowledgeAssessmentTopic.IsActive) : null, jobSectorTopic is not null ? new InterestTopicDto(jobSectorTopic.Id, jobSectorTopic.NameAr, jobSectorTopic.NameEn, jobSectorTopic.Category, jobSectorTopic.IsActive) : null, - user.CountryId)); + user.CountryId), MessageKeys.Identity.INTEREST_UPSERTED); } private static void UpsertCategory( diff --git a/backend/src/CCE.Application/Identity/Public/Queries/GetMyExpertStatus/GetMyExpertStatusQueryHandler.cs b/backend/src/CCE.Application/Identity/Public/Queries/GetMyExpertStatus/GetMyExpertStatusQueryHandler.cs index 10f59c44..cc48b7e7 100644 --- a/backend/src/CCE.Application/Identity/Public/Queries/GetMyExpertStatus/GetMyExpertStatusQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Queries/GetMyExpertStatus/GetMyExpertStatusQueryHandler.cs @@ -29,7 +29,7 @@ public async Task> Handle(GetMyExpertStatusQuer var entity = rows.FirstOrDefault(); if (entity is null) - return _msg.ExpertRequestNotFound(); + return _msg.NotFound(MessageKeys.Identity.EXPERT_REQUEST_NOT_FOUND); var attachments = await _db.ExpertRequestAttachments .Where(a => a.ExpertRequestId == entity.Id) diff --git a/backend/src/CCE.Application/Identity/Public/Queries/GetMyInterests/GetMyInterestsQueryHandler.cs b/backend/src/CCE.Application/Identity/Public/Queries/GetMyInterests/GetMyInterestsQueryHandler.cs index 50ed26a3..3d1fac83 100644 --- a/backend/src/CCE.Application/Identity/Public/Queries/GetMyInterests/GetMyInterestsQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Queries/GetMyInterests/GetMyInterestsQueryHandler.cs @@ -31,7 +31,7 @@ public async Task> Handle( { var user = await _service.FindAsync(request.UserId, cancellationToken).ConfigureAwait(false); if (user is null) - return _msg.UserNotFound(); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); var currentTopics = await _db.InterestTopics .Where(t => t.IsActive) diff --git a/backend/src/CCE.Application/Identity/Public/Queries/GetMyProfile/GetMyProfileQueryHandler.cs b/backend/src/CCE.Application/Identity/Public/Queries/GetMyProfile/GetMyProfileQueryHandler.cs index 1d909a48..d05496bf 100644 --- a/backend/src/CCE.Application/Identity/Public/Queries/GetMyProfile/GetMyProfileQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Public/Queries/GetMyProfile/GetMyProfileQueryHandler.cs @@ -31,7 +31,7 @@ public async Task> Handle(GetMyProfileQuery request, Ca var user = users.FirstOrDefault(); if (user is null) - return _msg.UserNotFound(); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); var interestTopics = user.UserInterestTopics .Select(uit => new InterestTopicDto( diff --git a/backend/src/CCE.Application/Identity/Queries/GetExpertRequestById/GetExpertRequestByIdQueryHandler.cs b/backend/src/CCE.Application/Identity/Queries/GetExpertRequestById/GetExpertRequestByIdQueryHandler.cs index 0e5fa092..fc140be0 100644 --- a/backend/src/CCE.Application/Identity/Queries/GetExpertRequestById/GetExpertRequestByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Queries/GetExpertRequestById/GetExpertRequestByIdQueryHandler.cs @@ -31,7 +31,7 @@ public async Task> Handle( var row = rows.FirstOrDefault(); if (row is null) - return _msg.ExpertRequestNotFound(); + return _msg.NotFound(MessageKeys.Identity.EXPERT_REQUEST_NOT_FOUND); var userNames = await _db.Users .Where(u => u.Id == row.RequestedById) diff --git a/backend/src/CCE.Application/Identity/Queries/GetUserById/GetUserByIdQueryHandler.cs b/backend/src/CCE.Application/Identity/Queries/GetUserById/GetUserByIdQueryHandler.cs index efb303ba..39efce5b 100644 --- a/backend/src/CCE.Application/Identity/Queries/GetUserById/GetUserByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Identity/Queries/GetUserById/GetUserByIdQueryHandler.cs @@ -33,7 +33,7 @@ public async Task> Handle( var user = users.SingleOrDefault(); if (user is null) { - return _msg.UserNotFound(); + return _msg.NotFound(MessageKeys.Identity.USER_NOT_FOUND); } var roleNames = diff --git a/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMap/CreateInteractiveMapCommandHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMap/CreateInteractiveMapCommandHandler.cs index 0133f12f..7a065b17 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMap/CreateInteractiveMapCommandHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMap/CreateInteractiveMapCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.InteractiveMaps; @@ -36,6 +36,6 @@ public async Task> Handle( await _repo.AddAsync(entity, cancellationToken).ConfigureAwait(false); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.MapCreated(); + return _msg.Ok(MessageKeys.InteractiveMaps.MAP_CREATED); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMapNode/CreateInteractiveMapNodeCommandHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMapNode/CreateInteractiveMapNodeCommandHandler.cs index 449d8788..e2e59410 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMapNode/CreateInteractiveMapNodeCommandHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Commands/CreateInteractiveMapNode/CreateInteractiveMapNodeCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.InteractiveMaps; @@ -42,6 +42,6 @@ public async Task> Handle( await _repo.AddAsync(entity, cancellationToken).ConfigureAwait(false); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.NodeCreated(); + return _msg.Ok(MessageKeys.InteractiveMaps.NODE_CREATED); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Commands/DeleteInteractiveMap/DeleteInteractiveMapCommandHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Commands/DeleteInteractiveMap/DeleteInteractiveMapCommandHandler.cs index 7f4e0fd9..8676a33e 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Commands/DeleteInteractiveMap/DeleteInteractiveMapCommandHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Commands/DeleteInteractiveMap/DeleteInteractiveMapCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.InteractiveMaps; @@ -29,11 +29,11 @@ public async Task> Handle( { var entity = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (entity is null) - return _msg.MapNotFound(); + return _msg.NotFound(MessageKeys.InteractiveMaps.MAP_NOT_FOUND); entity.Deactivate(); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.MapDeleted(); + return _msg.Ok(MessageKeys.InteractiveMaps.MAP_DELETED); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Commands/DeleteInteractiveMapNode/DeleteInteractiveMapNodeCommandHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Commands/DeleteInteractiveMapNode/DeleteInteractiveMapNodeCommandHandler.cs index 4170b250..610cfb92 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Commands/DeleteInteractiveMapNode/DeleteInteractiveMapNodeCommandHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Commands/DeleteInteractiveMapNode/DeleteInteractiveMapNodeCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.InteractiveMaps; @@ -29,11 +29,11 @@ public async Task> Handle( { var entity = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (entity is null || entity.InteractiveMapId != request.MapId) - return _msg.NodeNotFound(); + return _msg.NotFound(MessageKeys.InteractiveMaps.NODE_NOT_FOUND); entity.Deactivate(); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.NodeDeleted(); + return _msg.Ok(MessageKeys.InteractiveMaps.NODE_DELETED); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMap/UpdateInteractiveMapCommandHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMap/UpdateInteractiveMapCommandHandler.cs index 9ef951a3..b7f2eba7 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMap/UpdateInteractiveMapCommandHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMap/UpdateInteractiveMapCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.InteractiveMaps; @@ -29,7 +29,7 @@ public async Task> Handle( { var entity = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (entity is null) - return _msg.MapNotFound(); + return _msg.NotFound(MessageKeys.InteractiveMaps.MAP_NOT_FOUND); entity.UpdateDetails( request.NameAr, @@ -44,6 +44,6 @@ public async Task> Handle( await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.MapUpdated(); + return _msg.Ok(MessageKeys.InteractiveMaps.MAP_UPDATED); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMapNode/UpdateInteractiveMapNodeCommandHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMapNode/UpdateInteractiveMapNodeCommandHandler.cs index 8db4583c..194b129f 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMapNode/UpdateInteractiveMapNodeCommandHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Commands/UpdateInteractiveMapNode/UpdateInteractiveMapNodeCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Messages; using CCE.Domain.InteractiveMaps; @@ -29,7 +29,7 @@ public async Task> Handle( { var entity = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (entity is null || entity.InteractiveMapId != request.MapId) - return _msg.NodeNotFound(); + return _msg.NotFound(MessageKeys.InteractiveMaps.NODE_NOT_FOUND); entity.UpdateDetails( request.NameAr, @@ -49,6 +49,6 @@ public async Task> Handle( await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _msg.NodeUpdated(); + return _msg.Ok(MessageKeys.InteractiveMaps.NODE_UPDATED); } } diff --git a/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapById/GetInteractiveMapBySlugQueryHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapById/GetInteractiveMapBySlugQueryHandler.cs index 57df621d..3bd5e19f 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapById/GetInteractiveMapBySlugQueryHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapById/GetInteractiveMapBySlugQueryHandler.cs @@ -29,7 +29,7 @@ public async Task> Handle( .ConfigureAwait(false); if (map is null) - return _msg.MapNotFound(); + return _msg.NotFound(MessageKeys.InteractiveMaps.MAP_NOT_FOUND); var nodes = await _db.InteractiveMapNodes .Include(n => n.Tags) diff --git a/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapNodeDetails/GetInteractiveMapNodeDetailsQueryHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapNodeDetails/GetInteractiveMapNodeDetailsQueryHandler.cs index dd206399..2af7100b 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapNodeDetails/GetInteractiveMapNodeDetailsQueryHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Public/Queries/GetInteractiveMapNodeDetails/GetInteractiveMapNodeDetailsQueryHandler.cs @@ -35,7 +35,7 @@ public async Task> Handle( .ConfigureAwait(false); if (node is null) - return _msg.NodeNotFound(); + return _msg.NotFound(MessageKeys.InteractiveMaps.NODE_NOT_FOUND); // ─── 1b. Resolve node tag IDs for tag-based matching ─── var nodeTagIds = await _db.InteractiveMapNodes @@ -56,7 +56,7 @@ public async Task> Handle( .ConfigureAwait(false); if (topic is null) - return _msg.MapNotFound(); + return _msg.NotFound(MessageKeys.InteractiveMaps.MAP_NOT_FOUND); // ─── 3. News — top N by topic or tags, newest first ─── var news = await _db.News diff --git a/backend/src/CCE.Application/InteractiveMaps/Queries/GetInteractiveMapById/GetInteractiveMapByIdQueryHandler.cs b/backend/src/CCE.Application/InteractiveMaps/Queries/GetInteractiveMapById/GetInteractiveMapByIdQueryHandler.cs index 4bf81e82..1770328f 100644 --- a/backend/src/CCE.Application/InteractiveMaps/Queries/GetInteractiveMapById/GetInteractiveMapByIdQueryHandler.cs +++ b/backend/src/CCE.Application/InteractiveMaps/Queries/GetInteractiveMapById/GetInteractiveMapByIdQueryHandler.cs @@ -36,7 +36,7 @@ public async Task> Handle( .ConfigureAwait(false); if (dto is null) - return _msg.MapNotFound(); + return _msg.NotFound(MessageKeys.InteractiveMaps.MAP_NOT_FOUND); return _msg.Ok(dto, MessageKeys.General.ITEMS_LISTED); } diff --git a/backend/src/CCE.Application/Kapsarc/Commands/RefreshKapsarcSnapshot/RefreshKapsarcSnapshotCommandHandler.cs b/backend/src/CCE.Application/Kapsarc/Commands/RefreshKapsarcSnapshot/RefreshKapsarcSnapshotCommandHandler.cs index 8ccf9912..f7ad67bf 100644 --- a/backend/src/CCE.Application/Kapsarc/Commands/RefreshKapsarcSnapshot/RefreshKapsarcSnapshotCommandHandler.cs +++ b/backend/src/CCE.Application/Kapsarc/Commands/RefreshKapsarcSnapshot/RefreshKapsarcSnapshotCommandHandler.cs @@ -1,4 +1,4 @@ -using CCE.Application.Common; +using CCE.Application.Common; using CCE.Application.Common.Interfaces; using CCE.Application.Kapsarc.Dtos; using CCE.Application.Kapsarc.Queries.GetLatestKapsarcSnapshot; @@ -41,7 +41,7 @@ public async Task> Handle( { var country = await _countries.GetByIdAsync(request.CountryId, cancellationToken).ConfigureAwait(false); if (country is null) - return _messages.CountryNotFound(); + return _messages.NotFound(MessageKeys.Country.COUNTRY_NOT_FOUND); // Live retrieval from KAPSARC (inputs per BRD §6.5.1: ISO code + country name) var result = await _kapsarc @@ -52,7 +52,7 @@ public async Task> Handle( || result.PerformanceScore is null || result.TotalIndex is null) { // BRD ER001 — no KAPSARC output / data unavailable - return _messages.KapsarcDataUnavailable(); + return _messages.BusinessRule(MessageKeys.Country.KAPSARC_DATA_UNAVAILABLE); } CountryKapsarcSnapshot snapshot; @@ -69,7 +69,7 @@ public async Task> Handle( catch (DomainException) { // Out-of-range / invalid payload from the gateway → treat as unavailable - return _messages.KapsarcDataUnavailable(); + return _messages.BusinessRule(MessageKeys.Country.KAPSARC_DATA_UNAVAILABLE); } await _snapshots.AddAsync(snapshot, cancellationToken).ConfigureAwait(false); @@ -77,7 +77,7 @@ public async Task> Handle( await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return _messages.KapsarcSnapshotRefreshed( - GetLatestKapsarcSnapshotQueryHandler.MapToDto(snapshot)); + return _messages.Ok( + GetLatestKapsarcSnapshotQueryHandler.MapToDto(snapshot), MessageKeys.Country.KAPSARC_SNAPSHOT_REFRESHED); } } diff --git a/backend/src/CCE.Application/Lookups/Commands/UpsertCountryCode/UpsertCountryCodeCommandHandler.cs b/backend/src/CCE.Application/Lookups/Commands/UpsertCountryCode/UpsertCountryCodeCommandHandler.cs index f5dee64f..db5a266e 100644 --- a/backend/src/CCE.Application/Lookups/Commands/UpsertCountryCode/UpsertCountryCodeCommandHandler.cs +++ b/backend/src/CCE.Application/Lookups/Commands/UpsertCountryCode/UpsertCountryCodeCommandHandler.cs @@ -47,7 +47,7 @@ public async Task> Handle( { var entity = await _repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false); if (entity is null) - return _msg.CountryCodeNotFound(); + return _msg.NotFound(MessageKeys.Lookups.COUNTRY_CODE_NOT_FOUND); entity.UpdateLookup(request.NameAr, request.NameEn, request.DialCode, request.FlagUrl, request.IsActive); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Lookups/Queries/GetCountryCodeById/GetCountryCodeByIdQueryHandler.cs b/backend/src/CCE.Application/Lookups/Queries/GetCountryCodeById/GetCountryCodeByIdQueryHandler.cs index cbd63cb2..a2d070ba 100644 --- a/backend/src/CCE.Application/Lookups/Queries/GetCountryCodeById/GetCountryCodeByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Lookups/Queries/GetCountryCodeById/GetCountryCodeByIdQueryHandler.cs @@ -28,7 +28,7 @@ public async Task> Handle( .ConfigureAwait(false); var entity = list.SingleOrDefault(); return entity is null - ? _msg.CountryCodeNotFound() + ? _msg.NotFound(MessageKeys.Lookups.COUNTRY_CODE_NOT_FOUND) : _msg.Ok(CCE.Application.Lookups.Queries.ListCountryCodes.ListCountryCodesQueryHandler.MapToDto(entity), MessageKeys.General.ITEMS_LISTED); } } diff --git a/backend/src/CCE.Application/Media/Commands/DeleteMedia/DeleteMediaCommandHandler.cs b/backend/src/CCE.Application/Media/Commands/DeleteMedia/DeleteMediaCommandHandler.cs index 2a431d72..74ff3d69 100644 --- a/backend/src/CCE.Application/Media/Commands/DeleteMedia/DeleteMediaCommandHandler.cs +++ b/backend/src/CCE.Application/Media/Commands/DeleteMedia/DeleteMediaCommandHandler.cs @@ -33,7 +33,7 @@ public async Task> Handle( { var mediaFile = await _repo.FindAsync(request.Id, ct).ConfigureAwait(false); if (mediaFile is null) - return _msg.MediaFileNotFound(); + return _msg.NotFound(MessageKeys.Media.MEDIA_FILE_NOT_FOUND); await _fileStorage.DeleteAsync(mediaFile.StorageKey, ct).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/Media/Commands/UpdateMediaMetadata/UpdateMediaMetadataCommandHandler.cs b/backend/src/CCE.Application/Media/Commands/UpdateMediaMetadata/UpdateMediaMetadataCommandHandler.cs index ee51bb5b..7733913a 100644 --- a/backend/src/CCE.Application/Media/Commands/UpdateMediaMetadata/UpdateMediaMetadataCommandHandler.cs +++ b/backend/src/CCE.Application/Media/Commands/UpdateMediaMetadata/UpdateMediaMetadataCommandHandler.cs @@ -28,7 +28,7 @@ public async Task> Handle( { var mediaFile = await _repo.FindAsync(request.Id, ct).ConfigureAwait(false); if (mediaFile is null) - return _msg.MediaFileNotFound(); + return _msg.NotFound(MessageKeys.Media.MEDIA_FILE_NOT_FOUND); mediaFile.UpdateMetadata( request.TitleAr, diff --git a/backend/src/CCE.Application/Media/Commands/UploadMedia/UploadMediaCommandHandler.cs b/backend/src/CCE.Application/Media/Commands/UploadMedia/UploadMediaCommandHandler.cs index ad79bf47..a598719e 100644 --- a/backend/src/CCE.Application/Media/Commands/UploadMedia/UploadMediaCommandHandler.cs +++ b/backend/src/CCE.Application/Media/Commands/UploadMedia/UploadMediaCommandHandler.cs @@ -41,13 +41,13 @@ public async Task> Handle( UploadMediaCommand request, CancellationToken ct) { if (request.FileSize == 0) - return _msg.EmptyFile(); + return _msg.BusinessRule(MessageKeys.Media.EMPTY_FILE); if (request.FileSize > _opts.MaxSizeBytes) - return _msg.FileTooLarge(); + return _msg.BusinessRule(MessageKeys.Media.FILE_TOO_LARGE); if (!_opts.AllowedMimeTypes.Contains(request.ContentType)) - return _msg.InvalidFileType(); + return _msg.BusinessRule(MessageKeys.Media.INVALID_FILE_TYPE); var userId = _currentUser.GetUserId() ?? throw new DomainException("Authenticated user required."); diff --git a/backend/src/CCE.Application/Media/Queries/GetMediaById/GetMediaByIdQueryHandler.cs b/backend/src/CCE.Application/Media/Queries/GetMediaById/GetMediaByIdQueryHandler.cs index 0b8c8c43..3216837f 100644 --- a/backend/src/CCE.Application/Media/Queries/GetMediaById/GetMediaByIdQueryHandler.cs +++ b/backend/src/CCE.Application/Media/Queries/GetMediaById/GetMediaByIdQueryHandler.cs @@ -24,7 +24,7 @@ public async Task> Handle( { var mediaFile = await _repo.FindAsync(request.Id, ct).ConfigureAwait(false); if (mediaFile is null) - return _msg.MediaFileNotFound(); + return _msg.NotFound(MessageKeys.Media.MEDIA_FILE_NOT_FOUND); return _msg.Ok(MediaFileDto.FromEntity(mediaFile), MessageKeys.General.ITEMS_LISTED); } diff --git a/backend/src/CCE.Application/Messages/MessageFactory.cs b/backend/src/CCE.Application/Messages/MessageFactory.cs index de63fc6f..6c7b01a0 100644 --- a/backend/src/CCE.Application/Messages/MessageFactory.cs +++ b/backend/src/CCE.Application/Messages/MessageFactory.cs @@ -74,108 +74,6 @@ public FieldError Field(string fieldName, string domainKey) return new FieldError(fieldName, code, msg); } - // ─── Convenience shortcuts (Identity domain) ─── - - public Response UserNotFound() => NotFound(MessageKeys.Identity.USER_NOT_FOUND); - public Response InterestUpserted(T data) => Ok(data, MessageKeys.Identity.INTEREST_UPSERTED); - public Response EmailExists() => Conflict(MessageKeys.Identity.EMAIL_EXISTS); - public Response InvalidCredentials() => Unauthorized(MessageKeys.Identity.INVALID_CREDENTIALS); - public Response NotAuthenticated() => Unauthorized(MessageKeys.Identity.NOT_AUTHENTICATED); - public Response AccountDeactivated() => Forbidden(MessageKeys.Identity.ACCOUNT_DEACTIVATED); - public Response ContactNotVerified() => Forbidden(MessageKeys.Identity.CONTACT_NOT_VERIFIED); - - // ─── Convenience shortcuts (Content domain) ─── - - public Response NewsNotFound() => NotFound(MessageKeys.Content.NEWS_NOT_FOUND); - public Response EventNotFound() => NotFound(MessageKeys.Content.EVENT_NOT_FOUND); - public Response ResourceNotFound() => NotFound(MessageKeys.Content.RESOURCE_NOT_FOUND); - public Response PageNotFound() => NotFound(MessageKeys.Content.PAGE_NOT_FOUND); - public Response TopicNotFound() => NotFound(MessageKeys.Community.TOPIC_NOT_FOUND); - public Response CannotFollowSelf() => ValidationError( - MessageKeys.Community.CANNOT_FOLLOW_SELF, - new[] { Field("userId", MessageKeys.Community.CANNOT_FOLLOW_SELF) }); - public Response CategoryNotFound() => NotFound(MessageKeys.Content.CATEGORY_NOT_FOUND); - public Response AssetNotFound() => NotFound(MessageKeys.Content.ASSET_NOT_FOUND); - public Response AssetNotClean() => BusinessRule(MessageKeys.Content.ASSET_NOT_CLEAN); - - // ─── Convenience shortcuts (Identity / Expert domain) ─── - - public Response ExpertRequestNotFound() => NotFound(MessageKeys.Identity.EXPERT_REQUEST_NOT_FOUND); - - // ─── Convenience shortcuts (Platform Settings domain) ─── - - public Response HomepageSettingsNotFound() => NotFound(MessageKeys.PlatformSettings.HOMEPAGE_SETTINGS_NOT_FOUND); - public Response AboutSettingsNotFound() => NotFound(MessageKeys.PlatformSettings.ABOUT_SETTINGS_NOT_FOUND); - public Response PoliciesSettingsNotFound() => NotFound(MessageKeys.PlatformSettings.POLICIES_SETTINGS_NOT_FOUND); - public Response GlossaryEntryNotFound() => NotFound(MessageKeys.PlatformSettings.GLOSSARY_ENTRY_NOT_FOUND); - public Response KnowledgePartnerNotFound() => NotFound(MessageKeys.PlatformSettings.KNOWLEDGE_PARTNER_NOT_FOUND); - public Response PolicySectionNotFound() => NotFound(MessageKeys.PlatformSettings.POLICY_SECTION_NOT_FOUND); - public Response ContentUpdateFailed() => BusinessRule(MessageKeys.PlatformSettings.CONTENT_UPDATE_FAILED); - - // ─── Convenience shortcuts (Media domain) ─── - - public Response MediaFileNotFound() => NotFound(MessageKeys.Media.MEDIA_FILE_NOT_FOUND); - public Response InvalidFileType() => BusinessRule(MessageKeys.Media.INVALID_FILE_TYPE); - public Response FileTooLarge() => BusinessRule(MessageKeys.Media.FILE_TOO_LARGE); - public Response EmptyFile() => BusinessRule(MessageKeys.Media.EMPTY_FILE); - - // ─── Convenience shortcuts (Verification domain) ─── - - public Response OtpNotFound() => NotFound(MessageKeys.Verification.OTP_NOT_FOUND); - public Response OtpExpired() => BusinessRule(MessageKeys.Verification.OTP_EXPIRED); - public Response OtpInvalidCode() => BusinessRule(MessageKeys.Verification.OTP_INVALID_CODE); - public Response OtpMaxAttempts() => BusinessRule(MessageKeys.Verification.OTP_MAX_ATTEMPTS); - public Response OtpCooldownActive() => BusinessRule(MessageKeys.Verification.OTP_COOLDOWN_ACTIVE); - public Response OtpInvalidated() => BusinessRule(MessageKeys.Verification.OTP_INVALIDATED); - public Response ContactAlreadyTaken() => Conflict(MessageKeys.Verification.CONTACT_ALREADY_TAKEN); - public Response EmailUpdated() => Ok(MessageKeys.Verification.EMAIL_UPDATED); - public Response PhoneUpdated() => Ok(MessageKeys.Verification.PHONE_UPDATED); - - // ─── Convenience shortcuts (Country domain) ─── - - public Response CountryNotFound() => NotFound(MessageKeys.Country.COUNTRY_NOT_FOUND); - public Response CountryProfileNotFound() => NotFound(MessageKeys.Country.COUNTRY_PROFILE_NOT_FOUND); - public Response NoCountryAssigned() => NotFound(MessageKeys.Country.NO_COUNTRY_ASSIGNED); - public Response CountryScopeForbidden() => Forbidden(MessageKeys.Country.COUNTRY_SCOPE_FORBIDDEN); - public Response CountryContentRequestNotFound() => NotFound(MessageKeys.Content.COUNTRY_RESOURCE_REQUEST_NOT_FOUND); - public Response CountryRequestProcessed(T data) => Ok(data, MessageKeys.Content.COUNTRY_REQUEST_PROCESSED); - public Response CountryRequestProcessingFailed() => BusinessRule(MessageKeys.Content.COUNTRY_REQUEST_PROCESSING_FAILED); - public Response KapsarcDataUnavailable() => BusinessRule(MessageKeys.Country.KAPSARC_DATA_UNAVAILABLE); - public Response KapsarcSnapshotRefreshed(T data) => Ok(data, MessageKeys.Country.KAPSARC_SNAPSHOT_REFRESHED); - - // ─── Convenience shortcuts (InteractiveMaps domain) ─── - - public Response MapNotFound() => NotFound(MessageKeys.InteractiveMaps.MAP_NOT_FOUND); - public Response MapCreated() => Ok(MessageKeys.InteractiveMaps.MAP_CREATED); - public Response MapUpdated() => Ok(MessageKeys.InteractiveMaps.MAP_UPDATED); - public Response MapDeleted() => Ok(MessageKeys.InteractiveMaps.MAP_DELETED); - public Response NodeNotFound() => NotFound(MessageKeys.InteractiveMaps.NODE_NOT_FOUND); - public Response NodeCreated() => Ok(MessageKeys.InteractiveMaps.NODE_CREATED); - public Response NodeUpdated() => Ok(MessageKeys.InteractiveMaps.NODE_UPDATED); - public Response NodeDeleted() => Ok(MessageKeys.InteractiveMaps.NODE_DELETED); - - // ─── Convenience shortcuts (Evaluation domain) ─── - - public Response EvaluationSubmitted() => Ok(MessageKeys.Evaluation.EVALUATION_SUBMITTED); - public Response EvaluationNotFound() => NotFound(MessageKeys.Evaluation.EVALUATION_NOT_FOUND); - - // ─── Convenience shortcuts (Notification domain) ─── - - public Response NotificationTemplateNotFound() => NotFound(MessageKeys.Notifications.TEMPLATE_NOT_FOUND); - public Response NotificationLogNotFound() => NotFound(MessageKeys.Notifications.NOTIFICATION_NOT_FOUND); - public Response NotificationSettingsUpdated() => Ok(MessageKeys.Notifications.NOTIFICATION_SETTINGS_UPDATED); - public Response NotificationMarkedRead() => Ok(MessageKeys.Notifications.NOTIFICATION_MARKED_READ); - public Response NotificationsMarkedRead(int count) => Ok(count, MessageKeys.Notifications.NOTIFICATIONS_MARKED_READ); - public Response NotificationRetried(T data) => Ok(data, MessageKeys.Notifications.NOTIFICATION_RETRIED); - public Response NotificationTemplateCreated(T data) => Ok(data, MessageKeys.Notifications.NOTIFICATION_TEMPLATE_CREATED); - public Response NotificationTemplateUpdated(T data) => Ok(data, MessageKeys.Notifications.NOTIFICATION_TEMPLATE_UPDATED); - - // ─── Convenience shortcuts (Lookups domain) ─── - - public Response CountryCodeNotFound() => NotFound(MessageKeys.Lookups.COUNTRY_CODE_NOT_FOUND); - public Response LookupCreated(T data) => Ok(data, MessageKeys.Lookups.LOOKUP_CREATED); - public Response LookupUpdated(T data) => Ok(data, MessageKeys.Lookups.LOOKUP_UPDATED); - // ─── Private ─── private Response Fail(string domainKey, MessageType type) diff --git a/backend/src/CCE.Application/Messages/MessageKeys.cs b/backend/src/CCE.Application/Messages/MessageKeys.cs index 28439ed1..80bc6dea 100644 --- a/backend/src/CCE.Application/Messages/MessageKeys.cs +++ b/backend/src/CCE.Application/Messages/MessageKeys.cs @@ -161,6 +161,9 @@ public static class Notifications public const string NOTIFICATIONS_MARKED_READ = "NOTIFICATIONS_MARKED_READ"; public const string NOTIFICATION_TEMPLATE_CREATED = "NOTIFICATION_TEMPLATE_CREATED"; public const string NOTIFICATION_TEMPLATE_UPDATED = "NOTIFICATION_TEMPLATE_UPDATED"; + public const string DEVICE_TOKEN_NOT_FOUND = "DEVICE_TOKEN_NOT_FOUND"; + public const string DEVICE_TOKEN_REGISTERED = "DEVICE_TOKEN_REGISTERED"; + public const string DEVICE_TOKEN_DELETED = "DEVICE_TOKEN_DELETED"; } public static class KnowledgeMap diff --git a/backend/src/CCE.Application/Messages/SystemCode.cs b/backend/src/CCE.Application/Messages/SystemCode.cs index bcb6fd40..4289073f 100644 --- a/backend/src/CCE.Application/Messages/SystemCode.cs +++ b/backend/src/CCE.Application/Messages/SystemCode.cs @@ -97,6 +97,7 @@ public static class SystemCode public const string ERR080 = "ERR080"; // Template not found public const string ERR081 = "ERR081"; // Template duplicate public const string ERR082 = "ERR082"; // Notification not found + public const string ERR083 = "ERR083"; // Device token not found // ─── KnowledgeMap Errors ─── public const string ERR090 = "ERR090"; // Map not found @@ -249,6 +250,8 @@ public static class SystemCode public const string CON045 = "CON045"; // Notifications marked read public const string CON046 = "CON046"; // Notification template created public const string CON047 = "CON047"; // Notification template updated + public const string CON087 = "CON087"; // Device token registered + public const string CON088 = "CON088"; // Device token deleted // ─── Lookups Success ─── public const string CON070 = "CON070"; // Lookup created diff --git a/backend/src/CCE.Application/Messages/SystemCodeMap.cs b/backend/src/CCE.Application/Messages/SystemCodeMap.cs index ec118d85..add417b5 100644 --- a/backend/src/CCE.Application/Messages/SystemCodeMap.cs +++ b/backend/src/CCE.Application/Messages/SystemCodeMap.cs @@ -94,6 +94,7 @@ public static class SystemCodeMap ["TEMPLATE_NOT_FOUND"] = SystemCode.ERR080, ["TEMPLATE_DUPLICATE"] = SystemCode.ERR081, ["NOTIFICATION_NOT_FOUND"] = SystemCode.ERR082, + ["DEVICE_TOKEN_NOT_FOUND"] = SystemCode.ERR083, // ─── KnowledgeMap Errors ─── ["MAP_NOT_FOUND"] = SystemCode.ERR090, @@ -230,6 +231,8 @@ public static class SystemCodeMap ["NOTIFICATIONS_MARKED_READ"] = SystemCode.CON045, ["NOTIFICATION_TEMPLATE_CREATED"] = SystemCode.CON046, ["NOTIFICATION_TEMPLATE_UPDATED"] = SystemCode.CON047, + ["DEVICE_TOKEN_REGISTERED"] = SystemCode.CON087, + ["DEVICE_TOKEN_DELETED"] = SystemCode.CON088, // ─── Verification Success ─── ["OTP_SENT"] = SystemCode.CON060, diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/CreateGlossaryEntry/CreateGlossaryEntryCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/CreateGlossaryEntry/CreateGlossaryEntryCommandHandler.cs index eea3c841..4bb5493c 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/CreateGlossaryEntry/CreateGlossaryEntryCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/CreateGlossaryEntry/CreateGlossaryEntryCommandHandler.cs @@ -36,7 +36,7 @@ public CreateGlossaryEntryCommandHandler( { var about = await _repo.GetAsync(cancellationToken).ConfigureAwait(false); if (about is null) - return _msg.AboutSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.ABOUT_SETTINGS_NOT_FOUND); var userId = _currentUser.GetUserId() ?? throw new DomainException("User identity required."); diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/CreateKnowledgePartner/CreateKnowledgePartnerCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/CreateKnowledgePartner/CreateKnowledgePartnerCommandHandler.cs index 89424097..d0b2c76b 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/CreateKnowledgePartner/CreateKnowledgePartnerCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/CreateKnowledgePartner/CreateKnowledgePartnerCommandHandler.cs @@ -36,7 +36,7 @@ public CreateKnowledgePartnerCommandHandler( { var about = await _repo.GetAsync(cancellationToken).ConfigureAwait(false); if (about is null) - return _msg.AboutSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.ABOUT_SETTINGS_NOT_FOUND); var userId = _currentUser.GetUserId() ?? throw new DomainException("User identity required."); diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/CreatePolicySection/CreatePolicySectionCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/CreatePolicySection/CreatePolicySectionCommandHandler.cs index 6ee4a72b..4f19128e 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/CreatePolicySection/CreatePolicySectionCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/CreatePolicySection/CreatePolicySectionCommandHandler.cs @@ -36,7 +36,7 @@ public CreatePolicySectionCommandHandler( { var settings = await _repo.GetAsync(cancellationToken).ConfigureAwait(false); if (settings is null) - return _msg.PoliciesSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.POLICIES_SETTINGS_NOT_FOUND); var userId = _currentUser.GetUserId() ?? throw new DomainException("User identity required."); diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/DeleteGlossaryEntry/DeleteGlossaryEntryCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/DeleteGlossaryEntry/DeleteGlossaryEntryCommandHandler.cs index 00b4bdd2..5c9e141b 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/DeleteGlossaryEntry/DeleteGlossaryEntryCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/DeleteGlossaryEntry/DeleteGlossaryEntryCommandHandler.cs @@ -28,11 +28,11 @@ public async Task> Handle( { var about = await _repo.GetAsync(cancellationToken).ConfigureAwait(false); if (about is null) - return _msg.AboutSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.ABOUT_SETTINGS_NOT_FOUND); var entry = about.GlossaryEntries.FirstOrDefault(e => e.Id == request.Id); if (entry is null) - return _msg.GlossaryEntryNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.GLOSSARY_ENTRY_NOT_FOUND); about.RemoveGlossaryEntry(entry); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/DeleteKnowledgePartner/DeleteKnowledgePartnerCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/DeleteKnowledgePartner/DeleteKnowledgePartnerCommandHandler.cs index abff7262..98a5f53b 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/DeleteKnowledgePartner/DeleteKnowledgePartnerCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/DeleteKnowledgePartner/DeleteKnowledgePartnerCommandHandler.cs @@ -28,11 +28,11 @@ public async Task> Handle( { var about = await _repo.GetAsync(cancellationToken).ConfigureAwait(false); if (about is null) - return _msg.AboutSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.ABOUT_SETTINGS_NOT_FOUND); var partner = about.KnowledgePartners.FirstOrDefault(p => p.Id == request.Id); if (partner is null) - return _msg.KnowledgePartnerNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.KNOWLEDGE_PARTNER_NOT_FOUND); about.RemoveKnowledgePartner(partner); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/DeletePolicySection/DeletePolicySectionCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/DeletePolicySection/DeletePolicySectionCommandHandler.cs index 0740c52f..2330fdcd 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/DeletePolicySection/DeletePolicySectionCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/DeletePolicySection/DeletePolicySectionCommandHandler.cs @@ -28,11 +28,11 @@ public async Task> Handle( { var settings = await _repo.GetAsync(cancellationToken).ConfigureAwait(false); if (settings is null) - return _msg.PoliciesSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.POLICIES_SETTINGS_NOT_FOUND); var section = settings.Sections.FirstOrDefault(s => s.Id == request.Id); if (section is null) - return _msg.PolicySectionNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.POLICY_SECTION_NOT_FOUND); settings.RemoveSection(section); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/ReorderPolicySection/ReorderPolicySectionCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/ReorderPolicySection/ReorderPolicySectionCommandHandler.cs index 7334b3c6..0371d78a 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/ReorderPolicySection/ReorderPolicySectionCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/ReorderPolicySection/ReorderPolicySectionCommandHandler.cs @@ -28,11 +28,11 @@ public ReorderPolicySectionCommandHandler( { var settings = await _repo.GetAsync(cancellationToken).ConfigureAwait(false); if (settings is null) - return _msg.PoliciesSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.POLICIES_SETTINGS_NOT_FOUND); var section = settings.Sections.FirstOrDefault(s => s.Id == request.Id); if (section is null) - return _msg.PolicySectionNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.POLICY_SECTION_NOT_FOUND); settings.ReorderSection(section, request.OrderIndex); await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateAboutSettings/UpdateAboutSettingsCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateAboutSettings/UpdateAboutSettingsCommandHandler.cs index 7056ddd3..2fdd0481 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateAboutSettings/UpdateAboutSettingsCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateAboutSettings/UpdateAboutSettingsCommandHandler.cs @@ -36,7 +36,7 @@ public UpdateAboutSettingsCommandHandler( { var settings = await _repo.GetAsync(cancellationToken).ConfigureAwait(false); if (settings is null) - return _msg.AboutSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.ABOUT_SETTINGS_NOT_FOUND); var userId = _currentUser.GetUserId() ?? throw new DomainException("User identity required."); diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateGlossaryEntry/UpdateGlossaryEntryCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateGlossaryEntry/UpdateGlossaryEntryCommandHandler.cs index 7a63f7b6..63cc0f0b 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateGlossaryEntry/UpdateGlossaryEntryCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateGlossaryEntry/UpdateGlossaryEntryCommandHandler.cs @@ -36,11 +36,11 @@ public UpdateGlossaryEntryCommandHandler( { var about = await _repo.GetAsync(cancellationToken).ConfigureAwait(false); if (about is null) - return _msg.AboutSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.ABOUT_SETTINGS_NOT_FOUND); var entry = about.GlossaryEntries.FirstOrDefault(e => e.Id == request.Id); if (entry is null) - return _msg.GlossaryEntryNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.GLOSSARY_ENTRY_NOT_FOUND); var userId = _currentUser.GetUserId() ?? throw new DomainException("User identity required."); diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateHomepageSettings/UpdateHomepageSettingsCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateHomepageSettings/UpdateHomepageSettingsCommandHandler.cs index 5916462b..8754cbbc 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateHomepageSettings/UpdateHomepageSettingsCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateHomepageSettings/UpdateHomepageSettingsCommandHandler.cs @@ -36,7 +36,7 @@ public UpdateHomepageSettingsCommandHandler( { var settings = await _repo.GetAsync(cancellationToken).ConfigureAwait(false); if (settings is null) - return _msg.HomepageSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.HOMEPAGE_SETTINGS_NOT_FOUND); var userId = _currentUser.GetUserId() ?? throw new DomainException("User identity required."); diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateKnowledgePartner/UpdateKnowledgePartnerCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateKnowledgePartner/UpdateKnowledgePartnerCommandHandler.cs index 8ddfb9c0..2d870e87 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/UpdateKnowledgePartner/UpdateKnowledgePartnerCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/UpdateKnowledgePartner/UpdateKnowledgePartnerCommandHandler.cs @@ -36,11 +36,11 @@ public UpdateKnowledgePartnerCommandHandler( { var about = await _repo.GetAsync(cancellationToken).ConfigureAwait(false); if (about is null) - return _msg.AboutSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.ABOUT_SETTINGS_NOT_FOUND); var partner = about.KnowledgePartners.FirstOrDefault(p => p.Id == request.Id); if (partner is null) - return _msg.KnowledgePartnerNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.KNOWLEDGE_PARTNER_NOT_FOUND); var userId = _currentUser.GetUserId() ?? throw new DomainException("User identity required."); diff --git a/backend/src/CCE.Application/PlatformSettings/Commands/UpdatePolicySection/UpdatePolicySectionCommandHandler.cs b/backend/src/CCE.Application/PlatformSettings/Commands/UpdatePolicySection/UpdatePolicySectionCommandHandler.cs index 373e6898..65bd7e88 100644 --- a/backend/src/CCE.Application/PlatformSettings/Commands/UpdatePolicySection/UpdatePolicySectionCommandHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Commands/UpdatePolicySection/UpdatePolicySectionCommandHandler.cs @@ -36,11 +36,11 @@ public UpdatePolicySectionCommandHandler( { var settings = await _repo.GetAsync(cancellationToken).ConfigureAwait(false); if (settings is null) - return _msg.PoliciesSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.POLICIES_SETTINGS_NOT_FOUND); var section = settings.Sections.FirstOrDefault(s => s.Id == request.Id); if (section is null) - return _msg.PolicySectionNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.POLICY_SECTION_NOT_FOUND); var userId = _currentUser.GetUserId() ?? throw new DomainException("User identity required."); diff --git a/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicAboutSettings/GetPublicAboutSettingsQueryHandler.cs b/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicAboutSettings/GetPublicAboutSettingsQueryHandler.cs index ce1caecc..8dd26ca6 100644 --- a/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicAboutSettings/GetPublicAboutSettingsQueryHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicAboutSettings/GetPublicAboutSettingsQueryHandler.cs @@ -27,7 +27,7 @@ public async Task> Handle( var list = await _db.AboutSettings.ToListAsyncEither(cancellationToken).ConfigureAwait(false); var settings = list.FirstOrDefault(); if (settings is null) - return _msg.AboutSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.ABOUT_SETTINGS_NOT_FOUND); var glossary = await _db.GlossaryEntries .Where(e => e.AboutSettingsId == settings.Id) diff --git a/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicHomepage/GetPublicHomepageQueryHandler.cs b/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicHomepage/GetPublicHomepageQueryHandler.cs index ca9a65bc..cb625fcb 100644 --- a/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicHomepage/GetPublicHomepageQueryHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicHomepage/GetPublicHomepageQueryHandler.cs @@ -29,7 +29,7 @@ public async Task> Handle( var settingsList = await _db.HomepageSettings.ToListAsyncEither(cancellationToken).ConfigureAwait(false); var settings = settingsList.FirstOrDefault(); if (settings is null) - return _msg.HomepageSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.HOMEPAGE_SETTINGS_NOT_FOUND); var countries = await ( from hc in _db.HomepageCountries diff --git a/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicPoliciesSettings/GetPublicPoliciesSettingsQueryHandler.cs b/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicPoliciesSettings/GetPublicPoliciesSettingsQueryHandler.cs index bb2fb44e..fe489d12 100644 --- a/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicPoliciesSettings/GetPublicPoliciesSettingsQueryHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Public/Queries/GetPublicPoliciesSettings/GetPublicPoliciesSettingsQueryHandler.cs @@ -27,7 +27,7 @@ public async Task> Handle( var list = await _db.PoliciesSettings.ToListAsyncEither(cancellationToken).ConfigureAwait(false); var settings = list.FirstOrDefault(); if (settings is null) - return _msg.PoliciesSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.POLICIES_SETTINGS_NOT_FOUND); var sections = await _db.PolicySections .Where(s => s.PoliciesSettingsId == settings.Id) diff --git a/backend/src/CCE.Application/PlatformSettings/Queries/GetAboutSettings/GetAboutSettingsQueryHandler.cs b/backend/src/CCE.Application/PlatformSettings/Queries/GetAboutSettings/GetAboutSettingsQueryHandler.cs index bcfa5644..169d00d7 100644 --- a/backend/src/CCE.Application/PlatformSettings/Queries/GetAboutSettings/GetAboutSettingsQueryHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Queries/GetAboutSettings/GetAboutSettingsQueryHandler.cs @@ -26,7 +26,7 @@ public async Task> Handle( var list = await _db.AboutSettings.ToListAsyncEither(cancellationToken).ConfigureAwait(false); var settings = list.FirstOrDefault(); if (settings is null) - return _msg.AboutSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.ABOUT_SETTINGS_NOT_FOUND); var glossary = await _db.GlossaryEntries .Where(e => e.AboutSettingsId == settings.Id) diff --git a/backend/src/CCE.Application/PlatformSettings/Queries/GetHomepageSettings/GetHomepageSettingsQueryHandler.cs b/backend/src/CCE.Application/PlatformSettings/Queries/GetHomepageSettings/GetHomepageSettingsQueryHandler.cs index 6628a2a3..136ce80f 100644 --- a/backend/src/CCE.Application/PlatformSettings/Queries/GetHomepageSettings/GetHomepageSettingsQueryHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Queries/GetHomepageSettings/GetHomepageSettingsQueryHandler.cs @@ -26,7 +26,7 @@ public async Task> Handle( var list = await _db.HomepageSettings.ToListAsyncEither(cancellationToken).ConfigureAwait(false); var settings = list.FirstOrDefault(); if (settings is null) - return _msg.HomepageSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.HOMEPAGE_SETTINGS_NOT_FOUND); var countries = await _db.HomepageCountries .Where(hc => hc.HomepageSettingsId == settings.Id) diff --git a/backend/src/CCE.Application/PlatformSettings/Queries/GetPoliciesSettings/GetPoliciesSettingsQueryHandler.cs b/backend/src/CCE.Application/PlatformSettings/Queries/GetPoliciesSettings/GetPoliciesSettingsQueryHandler.cs index b74215f1..39013e2a 100644 --- a/backend/src/CCE.Application/PlatformSettings/Queries/GetPoliciesSettings/GetPoliciesSettingsQueryHandler.cs +++ b/backend/src/CCE.Application/PlatformSettings/Queries/GetPoliciesSettings/GetPoliciesSettingsQueryHandler.cs @@ -26,7 +26,7 @@ public async Task> Handle( var list = await _db.PoliciesSettings.ToListAsyncEither(cancellationToken).ConfigureAwait(false); var settings = list.FirstOrDefault(); if (settings is null) - return _msg.PoliciesSettingsNotFound(); + return _msg.NotFound(MessageKeys.PlatformSettings.POLICIES_SETTINGS_NOT_FOUND); var sections = await _db.PolicySections .Where(s => s.PoliciesSettingsId == settings.Id) diff --git a/backend/src/CCE.Application/Verification/Commands/RequestVerification/RequestVerificationCommandHandler.cs b/backend/src/CCE.Application/Verification/Commands/RequestVerification/RequestVerificationCommandHandler.cs index 9beea48f..8bd2de4c 100644 --- a/backend/src/CCE.Application/Verification/Commands/RequestVerification/RequestVerificationCommandHandler.cs +++ b/backend/src/CCE.Application/Verification/Commands/RequestVerification/RequestVerificationCommandHandler.cs @@ -41,7 +41,7 @@ public async Task> Handle( .ConfigureAwait(false); if (existing is not null && !existing.CanResend(now)) - return _msg.OtpCooldownActive(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_COOLDOWN_ACTIVE); var (plainCode, codeHash) = _codeGenerator.Generate(); diff --git a/backend/src/CCE.Application/Verification/Commands/VerifyOtp/VerifyOtpCommandHandler.cs b/backend/src/CCE.Application/Verification/Commands/VerifyOtp/VerifyOtpCommandHandler.cs index f5519366..92c81ee4 100644 --- a/backend/src/CCE.Application/Verification/Commands/VerifyOtp/VerifyOtpCommandHandler.cs +++ b/backend/src/CCE.Application/Verification/Commands/VerifyOtp/VerifyOtpCommandHandler.cs @@ -45,16 +45,16 @@ public async Task> Handle( .ConfigureAwait(false); if (entity is null) - return _msg.OtpNotFound(); + return _msg.NotFound(MessageKeys.Verification.OTP_NOT_FOUND); if (entity.IsExpired(now)) - return _msg.OtpExpired(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_EXPIRED); if (entity.IsInvalidated) - return _msg.OtpInvalidated(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_INVALIDATED); if (entity.HasExceededMaxAttempts()) - return _msg.OtpMaxAttempts(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_MAX_ATTEMPTS); entity.IncrementAttempt(); @@ -62,7 +62,7 @@ public async Task> Handle( { _otpRepo.Update(entity); await _db.SaveChangesAsync(ct).ConfigureAwait(false); - return _msg.OtpInvalidCode(); + return _msg.BusinessRule(MessageKeys.Verification.OTP_INVALID_CODE); } entity.MarkVerified(); diff --git a/backend/src/CCE.Infrastructure/CCE.Infrastructure.csproj b/backend/src/CCE.Infrastructure/CCE.Infrastructure.csproj index a6ddbac1..dbe2147a 100644 --- a/backend/src/CCE.Infrastructure/CCE.Infrastructure.csproj +++ b/backend/src/CCE.Infrastructure/CCE.Infrastructure.csproj @@ -28,6 +28,7 @@ +