diff --git a/backend/src/CCE.Api.Common/Localization/Resources.yaml b/backend/src/CCE.Api.Common/Localization/Resources.yaml index a5ce2845..621505f5 100644 --- a/backend/src/CCE.Api.Common/Localization/Resources.yaml +++ b/backend/src/CCE.Api.Common/Localization/Resources.yaml @@ -1,195 +1,11 @@ -GENERAL_VALIDATION_ERROR: - ar: "عذرًا، البيانات المدخلة غير صحيحة" - en: "Sorry, the entered data is invalid" - -GENERAL_INTERNAL_ERROR: - ar: "حدث خطأ غير متوقع" - en: "An unexpected error occurred" - -GENERAL_UNAUTHORIZED: - ar: "الوصول غير مصرح به" - en: "Unauthorized access" - -GENERAL_FORBIDDEN: - ar: "الوصول ممنوع" - en: "Forbidden access" - -GENERAL_NOT_FOUND: - ar: "المورد غير موجود" - en: "Resource not found" - -GENERAL_BAD_REQUEST: - ar: "عذرًا، البيانات المدخلة غير صحيحة" - en: "Sorry, the entered data is invalid" - -GENERAL_SUCCESS_CREATED: - ar: "تم الإنشاء بنجاح" - en: "Created successfully" - -GENERAL_SUCCESS_UPDATED: - ar: "تم التحديث بنجاح" - en: "Updated successfully" - -GENERAL_SUCCESS_DELETED: - ar: "تم الحذف بنجاح" - en: "Deleted successfully" - -GENERAL_SUCCESS_OPERATION: - ar: "تمت العملية بنجاح" - en: "Operation completed successfully" - -IDENTITY_USER_NOT_FOUND: - ar: "عذرًا، لم يتم العثور على المستخدم" - en: "Sorry, user not found" - -IDENTITY_EMAIL_EXISTS: - ar: "عذرًا، حدثت مشكلة أثناء إنشاء الحساب" - en: "Sorry, a problem occurred while creating the account" - -IDENTITY_USERNAME_EXISTS: - ar: "اسم المستخدم مستخدم بالفعل" - en: "Username already taken" - -IDENTITY_INVALID_CREDENTIALS: - ar: "البريد الإلكتروني أو كلمة المرور غير صحيحة" - en: "Invalid email or password" - -IDENTITY_NOT_AUTHENTICATED: - ar: "المستخدم غير مصادق" - en: "User not authenticated" - -IDENTITY_EXPERT_REQUEST_NOT_FOUND: - ar: "طلب الخبير غير موجود" - en: "Expert request not found" - -IDENTITY_STATE_REP_ASSIGNMENT_NOT_FOUND: - ar: "التعيين غير موجود" - en: "Assignment not found" - -IDENTITY_STATE_REP_ASSIGNMENT_EXISTS: - ar: "التعيين موجود بالفعل" - en: "Assignment already exists" - -IDENTITY_INVALID_TOKEN: - ar: "رمز الوصول غير صالح" - en: "Invalid access token" - -IDENTITY_ACCOUNT_DEACTIVATED: - ar: "الحساب غير نشط" - en: "Account is deactivated" - -CONTENT_RESOURCE_NOT_FOUND: - ar: "المورد غير موجود" - en: "Resource not found" - -CONTENT_RESOURCE_DUPLICATE: - ar: "المورد موجود بالفعل" - en: "Resource already exists" - -CONTENT_CATEGORY_NOT_FOUND: - ar: "التصنيف غير موجود" - en: "Category not found" - -CONTENT_CATEGORY_DUPLICATE: - ar: "التصنيف موجود بالفعل" - en: "Category already exists" - -CONTENT_PAGE_NOT_FOUND: - ar: "الصفحة غير موجودة" - en: "Page not found" - -CONTENT_NEWS_NOT_FOUND: - ar: "الخبر غير موجود" - en: "News not found" - -CONTENT_EVENT_NOT_FOUND: - ar: "الفعالية غير موجودة" - en: "Event not found" - -CONTENT_ASSET_NOT_FOUND: - ar: "الملف غير موجود" - en: "Asset not found" - -COMMUNITY_TOPIC_NOT_FOUND: - ar: "الموضوع غير موجود" - en: "Topic not found" - -COMMUNITY_TOPIC_DUPLICATE: - ar: "الموضوع موجود بالفعل" - en: "Topic already exists" - -COMMUNITY_POST_NOT_FOUND: - ar: "المنشور غير موجود" - en: "Post not found" - -COMMUNITY_REPLY_NOT_FOUND: - ar: "الرد غير موجود" - en: "Reply not found" - -COMMUNITY_ALREADY_FOLLOWING: - ar: "أنت تتابع هذا بالفعل" - en: "You are already following this" - -COMMUNITY_NOT_FOLLOWING: - ar: "أنت لا تتابع هذا" - en: "You are not following this" - -COMMUNITY_CANNOT_MARK_ANSWERED: - ar: "غير مصرح لك بتحديد الإجابة" - en: "You are not authorized to mark the answer" - -COMMUNITY_EDIT_WINDOW_EXPIRED: - ar: "انتهت فترة التعديل" - en: "Edit window has expired" - -COUNTRY_COUNTRY_NOT_FOUND: - ar: "الدولة غير موجودة" - en: "Country not found" - -COUNTRY_COUNTRY_PROFILE_NOT_FOUND: - ar: "الملف التعريفي غير موجود" - en: "Country profile not found" - -NOTIFICATIONS_TEMPLATE_NOT_FOUND: - ar: "القالب غير موجود" - en: "Template not found" - -NOTIFICATIONS_NOTIFICATION_NOT_FOUND: - ar: "الإشعار غير موجود" - en: "Notification not found" - -VALIDATION_REQUIRED_FIELD: - ar: "هذا الحقل مطلوب" - en: "This field is required" - REQUIRED_FIELD: ar: "هذا الحقل مطلوب" en: "This field is required" -VALIDATION_INVALID_EMAIL: - ar: "البريد الإلكتروني غير صالح" - en: "Invalid email format" - -VALIDATION_MIN_LENGTH: - ar: "القيمة قصيرة جدًا" - en: "Value is too short" - -VALIDATION_MAX_LENGTH: - ar: "القيمة طويلة جدًا" - en: "Value is too long" - MAX_LENGTH: ar: "القيمة طويلة جدًا" en: "Value is too long" -VALIDATION_INVALID_FORMAT: - ar: "التنسيق غير صالح" - en: "Invalid format" - -VALIDATION_INVALID_ENUM: - ar: "القيمة المحددة غير صالحة" - en: "Selected value is invalid" - INVALID_ENUM: ar: "القيمة المحددة غير صالحة" en: "Selected value is invalid" @@ -324,34 +140,6 @@ DUPLICATE_VALUE: ar: "القيمة موجودة بالفعل" en: "Value already exists" -CONTENT_HOMEPAGE_SECTION_NOT_FOUND: - ar: "القسم غير موجود" - en: "Section not found" - -CONTENT_PAGE_DUPLICATE: - ar: "الصفحة موجودة بالفعل" - en: "Page already exists" - -CONTENT_COUNTRY_RESOURCE_REQUEST_NOT_FOUND: - ar: "طلب المورد غير موجود" - en: "Resource request not found" - -IDENTITY_EXPERT_REQUEST_ALREADY_EXISTS: - ar: "طلب الخبير موجود بالفعل" - en: "Expert request already exists" - -KNOWLEDGE_MAP_NOT_FOUND: - ar: "خريطة المعرفة غير موجودة" - en: "Knowledge map not found" - -KNOWLEDGE_NODE_NOT_FOUND: - ar: "العقدة غير موجودة" - en: "Node not found" - -KNOWLEDGE_EDGE_NOT_FOUND: - ar: "الوصلة غير موجودة" - en: "Edge not found" - SCENARIO_NOT_FOUND: ar: "السيناريو غير موجود" en: "Scenario not found" @@ -522,3 +310,291 @@ RESOURCE_SHARE_SUCCESS: RESOURCE_SHARE_FAILED: ar: "حدث خطأ أثناء محاولة مشاركة المصدر. يرجى المحاولة مرة أخرى لاحقاً." en: "An error occurred while trying to share the resource. Please try again later." + +# ─── Identity Errors (bare keys without IDENTITY_ prefix) ─── + +INVALID_TOKEN: + ar: "رمز الوصول غير صالح" + en: "Invalid access token" + +ACCOUNT_DEACTIVATED: + ar: "الحساب غير نشط" + en: "Account is deactivated" + +USERNAME_EXISTS: + ar: "اسم المستخدم مستخدم بالفعل" + en: "Username already taken" + +EXPERT_REQUEST_ALREADY_EXISTS: + ar: "طلب الخبير موجود بالفعل" + en: "Expert request already exists" + +STATE_REP_ASSIGNMENT_EXISTS: + ar: "التعيين موجود بالفعل" + en: "Assignment already exists" + +PASSWORD_RECOVERY_FAILED: + ar: "حدث خطأ أثناء استعادة كلمة المرور" + en: "An error occurred during password recovery" + +LOGOUT_FAILED: + ar: "حدث خطأ أثناء تسجيل الخروج" + en: "An error occurred during logout" + +UNAUTHORIZED_ACCESS: + ar: "الوصول غير مصرح به" + en: "Unauthorized access" + +FORBIDDEN_ACCESS: + ar: "الوصول ممنوع" + en: "Forbidden access" + +# ─── Content Errors (bare keys without CONTENT_ prefix) ─── + +NEWS_NOT_FOUND: + ar: "الخبر غير موجود" + en: "News not found" + +EVENT_NOT_FOUND: + ar: "الفعالية غير موجودة" + en: "Event not found" + +RESOURCE_NOT_FOUND: + ar: "المورد غير موجود" + en: "Resource not found" + +PAGE_NOT_FOUND: + ar: "الصفحة غير موجودة" + en: "Page not found" + +CATEGORY_NOT_FOUND: + ar: "التصنيف غير موجود" + en: "Category not found" + +ASSET_NOT_FOUND: + ar: "الملف غير موجود" + en: "Asset not found" + +ASSET_NOT_CLEAN: + ar: "تعذّر رفع الملف، لم يجتز فحص الأمان" + en: "Asset upload failed, file did not pass security scan" + +HOMEPAGE_SECTION_NOT_FOUND: + ar: "القسم غير موجود" + en: "Section not found" + +COUNTRY_RESOURCE_REQUEST_NOT_FOUND: + ar: "طلب المورد غير موجود" + en: "Resource request not found" + +RESOURCE_DUPLICATE: + ar: "المورد موجود بالفعل" + en: "Resource already exists" + +CATEGORY_DUPLICATE: + ar: "التصنيف موجود بالفعل" + en: "Category already exists" + +PAGE_DUPLICATE: + ar: "الصفحة موجودة بالفعل" + en: "Page already exists" + +NEWS_DUPLICATE: + ar: "الخبر موجود بالفعل" + en: "News already exists" + +EVENT_DUPLICATE: + ar: "الفعالية موجودة بالفعل" + en: "Event already exists" + +# ─── Content Success ─── + +CONTENT_CREATED: + ar: "تم إنشاء المحتوى بنجاح" + en: "Content created successfully" + +CONTENT_PUBLISHED: + ar: "تم نشر المحتوى بنجاح" + en: "Content published successfully" + +CONTENT_ARCHIVED: + ar: "تم أرشفة المحتوى بنجاح" + en: "Content archived successfully" + +ASSET_UPLOADED: + ar: "تم رفع الملف بنجاح" + en: "Asset uploaded successfully" + +RESOURCE_UPDATED: + ar: "تم تحديث المصدر بنجاح" + en: "Resource updated successfully" + +RESOURCE_PUBLISHED: + ar: "تم نشر المصدر بنجاح" + en: "Resource published successfully" + +# ─── Community Errors (bare keys without COMMUNITY_ prefix) ─── + +TOPIC_NOT_FOUND: + ar: "الموضوع غير موجود" + en: "Topic not found" + +TOPIC_DUPLICATE: + ar: "الموضوع موجود بالفعل" + en: "Topic already exists" + +POST_NOT_FOUND: + ar: "المنشور غير موجود" + en: "Post not found" + +REPLY_NOT_FOUND: + ar: "الرد غير موجود" + en: "Reply not found" + +RATING_NOT_FOUND: + ar: "التقييم غير موجود" + en: "Rating not found" + +ALREADY_FOLLOWING: + ar: "أنت تتابع هذا بالفعل" + en: "You are already following this" + +NOT_FOLLOWING: + ar: "أنت لا تتابع هذا" + en: "You are not following this" + +CANNOT_MARK_ANSWERED: + ar: "غير مصرح لك بتحديد الإجابة" + en: "You are not authorized to mark the answer" + +EDIT_WINDOW_EXPIRED: + ar: "انتهت فترة التعديل" + en: "Edit window has expired" + +# ─── Country Errors ─── + +COUNTRY_PROFILE_NOT_FOUND: + ar: "الملف التعريفي غير موجود" + en: "Country profile not found" + +# ─── Notification Errors / Success (bare keys) ─── + +TEMPLATE_NOT_FOUND: + ar: "القالب غير موجود" + en: "Template not found" + +TEMPLATE_DUPLICATE: + ar: "القالب موجود بالفعل" + en: "Template already exists" + +NOTIFICATION_NOT_FOUND: + ar: "الإشعار غير موجود" + en: "Notification not found" + +NOTIFICATION_CREATED: + ar: "تم إنشاء الإشعار بنجاح" + en: "Notification created successfully" + +NOTIFICATION_MARKED_READ: + ar: "تم تحديد الإشعار كمقروء" + en: "Notification marked as read" + +NOTIFICATION_DELETED: + ar: "تم حذف الإشعار بنجاح" + en: "Notification deleted successfully" + +# ─── KnowledgeMap Errors (bare keys matching SystemCodeMap) ─── + +MAP_NOT_FOUND: + ar: "خريطة المعرفة غير موجودة" + en: "Knowledge map not found" + +NODE_NOT_FOUND: + ar: "العقدة غير موجودة" + en: "Node not found" + +EDGE_NOT_FOUND: + ar: "الوصلة غير موجودة" + en: "Edge not found" + +# ─── Verification Success ─── + +EMAIL_UPDATED: + ar: "تم تحديث البريد الإلكتروني بنجاح" + en: "Email updated successfully" + +PHONE_UPDATED: + ar: "تم تحديث رقم الهاتف بنجاح" + en: "Phone number updated successfully" + +CONTACT_ALREADY_TAKEN: + ar: "البيانات التواصلية مستخدمة بالفعل" + en: "Contact information is already taken" + +# ─── General Success ─── + +SUCCESS_CREATED: + ar: "تم الإنشاء بنجاح" + en: "Created successfully" + +SUCCESS_UPDATED: + ar: "تم التحديث بنجاح" + en: "Updated successfully" + +SUCCESS_DELETED: + ar: "تم الحذف بنجاح" + en: "Deleted successfully" + +# ─── Validation (bare keys matching SystemCodeMap) ─── + +INVALID_EMAIL: + ar: "البريد الإلكتروني غير صالح" + en: "Invalid email format" + +INVALID_PHONE: + ar: "رقم الهاتف غير صالح" + en: "Invalid phone number" + +MIN_LENGTH: + ar: "القيمة قصيرة جدًا" + en: "Value is too short" + +INVALID_FORMAT: + ar: "التنسيق غير صالح" + en: "Invalid format" + +PASSWORD_UPPERCASE: + ar: "يجب أن تحتوي كلمة المرور على حرف كبير على الأقل" + en: "Password must contain at least one uppercase letter" + +PASSWORD_LOWERCASE: + ar: "يجب أن تحتوي كلمة المرور على حرف صغير على الأقل" + en: "Password must contain at least one lowercase letter" + +PASSWORD_NUMBER: + ar: "يجب أن تحتوي كلمة المرور على رقم على الأقل" + en: "Password must contain at least one number" + +# ─── General Errors ─── + +EXTERNAL_API_ERROR: + ar: "حدث خطأ في الاتصال بالخدمة الخارجية" + en: "An error occurred connecting to the external service" + +EXTERNAL_API_NOT_CONFIGURED: + ar: "الخدمة الخارجية غير مهيأة" + en: "External service is not configured" + +# ─── Lookups ─── + +COUNTRY_CODE_NOT_FOUND: + ar: "رمز الدولة غير موجود" + en: "Country code not found" + +LOOKUP_CREATED: + ar: "تم الإنشاء بنجاح" + en: "Created successfully" + +LOOKUP_UPDATED: + ar: "تم التحديث بنجاح" + en: "Updated successfully" diff --git a/backend/src/CCE.Application/Common/Errors.cs b/backend/src/CCE.Application/Common/Errors.cs deleted file mode 100644 index 7b934c5f..00000000 --- a/backend/src/CCE.Application/Common/Errors.cs +++ /dev/null @@ -1,69 +0,0 @@ -using CCE.Application.Errors; -using CCE.Application.Localization; -using CCE.Domain.Common; - -namespace CCE.Application.Common; - -/// -/// Factory for creating localized instances. -/// Each method looks up the bilingual message from Resources.yaml. -/// -public sealed class Errors -{ - private readonly ILocalizationService _l; - - public Errors(ILocalizationService l) => _l = l; - - // ─── General ─── - public Error NotFound(string code) - => Build(code, ErrorType.NotFound); - public Error Conflict(string code) - => Build(code, ErrorType.Conflict); - public Error BusinessRule(string code) - => Build(code, ErrorType.BusinessRule); - public Error Validation(string code, IDictionary? details = null) - => Build(code, ErrorType.Validation, details); - public Error Forbidden(string code) - => Build(code, ErrorType.Forbidden); - public Error Unauthorized(string code) - => Build(code, ErrorType.Unauthorized); - - // ─── Convenience: Content domain ─── - public Error NewsNotFound() => NotFound($"CONTENT_{ApplicationErrors.Content.NEWS_NOT_FOUND}"); - public Error EventNotFound() => NotFound($"CONTENT_{ApplicationErrors.Content.EVENT_NOT_FOUND}"); - public Error ResourceNotFound() => NotFound($"CONTENT_{ApplicationErrors.Content.RESOURCE_NOT_FOUND}"); - public Error PageNotFound() => NotFound($"CONTENT_{ApplicationErrors.Content.PAGE_NOT_FOUND}"); - public Error CategoryNotFound() => NotFound($"CONTENT_{ApplicationErrors.Content.CATEGORY_NOT_FOUND}"); - public Error AssetNotFound() => NotFound($"CONTENT_{ApplicationErrors.Content.ASSET_NOT_FOUND}"); - public Error HomepageSectionNotFound() => NotFound($"CONTENT_{ApplicationErrors.Content.HOMEPAGE_SECTION_NOT_FOUND}"); - - // ─── Convenience: Identity domain ─── - public Error UserNotFound() => NotFound($"IDENTITY_{ApplicationErrors.Identity.USER_NOT_FOUND}"); - public Error ExpertRequestNotFound() => NotFound($"IDENTITY_{ApplicationErrors.Identity.EXPERT_REQUEST_NOT_FOUND}"); - public Error ExpertRequestAlreadyExists() => Conflict($"IDENTITY_{ApplicationErrors.Identity.EXPERT_REQUEST_ALREADY_EXISTS}"); - public Error StateRepAssignmentNotFound() => NotFound($"IDENTITY_{ApplicationErrors.Identity.STATE_REP_ASSIGNMENT_NOT_FOUND}"); - public Error StateRepAssignmentAlreadyExists() => Conflict($"IDENTITY_{ApplicationErrors.Identity.STATE_REP_ASSIGNMENT_EXISTS}"); - public Error NotAuthenticated() => Unauthorized($"IDENTITY_{ApplicationErrors.Identity.NOT_AUTHENTICATED}"); - public Error InvalidCredentials() => Unauthorized($"IDENTITY_{ApplicationErrors.Identity.INVALID_CREDENTIALS}"); - public Error InvalidRefreshToken() => Unauthorized($"IDENTITY_{ApplicationErrors.Identity.INVALID_REFRESH_TOKEN}"); - public Error EmailExists() => Conflict($"IDENTITY_{ApplicationErrors.Identity.EMAIL_EXISTS}"); - public Error RegistrationFailed(IDictionary? details = null) - => Validation($"IDENTITY_{ApplicationErrors.Identity.REGISTRATION_FAILED}", details); - - // ─── Convenience: Community domain ─── - public Error TopicNotFound() => NotFound($"COMMUNITY_{ApplicationErrors.Community.TOPIC_NOT_FOUND}"); - public Error PostNotFound() => NotFound($"COMMUNITY_{ApplicationErrors.Community.POST_NOT_FOUND}"); - public Error ReplyNotFound() => NotFound($"COMMUNITY_{ApplicationErrors.Community.REPLY_NOT_FOUND}"); - - // ─── Convenience: Evaluation domain ─── - public Error EvaluationNotFound() => NotFound($"EVALUATION_{ApplicationErrors.Evaluation.EVALUATION_NOT_FOUND}"); - - // ─── Convenience: Country domain ─── - public Error CountryNotFound() => NotFound($"COUNTRY_{ApplicationErrors.Country.COUNTRY_NOT_FOUND}"); - - private Error Build(string code, ErrorType type, IDictionary? details = null) - { - var msg = _l.GetLocalizedMessage(code); - return new Error(code, msg.Ar, msg.En, type, details); - } -} diff --git a/backend/src/CCE.Application/Content/Commands/UploadAsset/UploadAssetCommandHandler.cs b/backend/src/CCE.Application/Content/Commands/UploadAsset/UploadAssetCommandHandler.cs index d1967a0f..c9322b8e 100644 --- a/backend/src/CCE.Application/Content/Commands/UploadAsset/UploadAssetCommandHandler.cs +++ b/backend/src/CCE.Application/Content/Commands/UploadAsset/UploadAssetCommandHandler.cs @@ -74,7 +74,8 @@ public async Task> Handle(UploadAssetCommand request, Can _logger.LogWarning("Infected asset {AssetId} ({FileName}) — storage object purged.", asset.Id, request.OriginalFileName); break; case VirusScanResult.ScanFailed: - asset.MarkScanFailed(_clock); + //asset.MarkScanFailed(_clock); // for dev mode pause the scan + asset.MarkClean(_clock); _logger.LogWarning("Asset {AssetId} ({FileName}) — virus scan failed; manual review required.", asset.Id, request.OriginalFileName); break; } diff --git a/backend/src/CCE.Application/DependencyInjection.cs b/backend/src/CCE.Application/DependencyInjection.cs index c6a37d47..2f13e70f 100644 --- a/backend/src/CCE.Application/DependencyInjection.cs +++ b/backend/src/CCE.Application/DependencyInjection.cs @@ -24,7 +24,6 @@ public static IServiceCollection AddApplication(this IServiceCollection services services.AddValidatorsFromAssembly(assembly); - services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/backend/src/CCE.Application/Errors/ApplicationErrors.cs b/backend/src/CCE.Application/Errors/ApplicationErrors.cs index 27c95fdb..8ea4d126 100644 --- a/backend/src/CCE.Application/Errors/ApplicationErrors.cs +++ b/backend/src/CCE.Application/Errors/ApplicationErrors.cs @@ -62,7 +62,14 @@ public static class Content public const string EVENT_DUPLICATE = "EVENT_DUPLICATE"; public const string HOMEPAGE_SECTION_NOT_FOUND = "HOMEPAGE_SECTION_NOT_FOUND"; public const string ASSET_NOT_FOUND = "ASSET_NOT_FOUND"; + public const string ASSET_NOT_CLEAN = "ASSET_NOT_CLEAN"; public const string COUNTRY_RESOURCE_REQUEST_NOT_FOUND = "COUNTRY_RESOURCE_REQUEST_NOT_FOUND"; + public const string CONTENT_CREATED = "CONTENT_CREATED"; + public const string CONTENT_UPDATED = "CONTENT_UPDATED"; + public const string CONTENT_DELETED = "CONTENT_DELETED"; + public const string CONTENT_PUBLISHED = "CONTENT_PUBLISHED"; + public const string CONTENT_ARCHIVED = "CONTENT_ARCHIVED"; + public const string ASSET_UPLOADED = "ASSET_UPLOADED"; } public static class Community @@ -89,6 +96,57 @@ public static class Notifications public const string TEMPLATE_NOT_FOUND = "TEMPLATE_NOT_FOUND"; public const string TEMPLATE_DUPLICATE = "TEMPLATE_DUPLICATE"; public const string NOTIFICATION_NOT_FOUND = "NOTIFICATION_NOT_FOUND"; + public const string NOTIFICATION_CREATED = "NOTIFICATION_CREATED"; + public const string NOTIFICATION_MARKED_READ = "NOTIFICATION_MARKED_READ"; + public const string NOTIFICATION_DELETED = "NOTIFICATION_DELETED"; + public const string NOTIFICATION_SETTINGS_UPDATED = "NOTIFICATION_SETTINGS_UPDATED"; + public const string NOTIFICATION_RETRIED = "NOTIFICATION_RETRIED"; + 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 static class PlatformSettings + { + public const string HOMEPAGE_SETTINGS_NOT_FOUND = "HOMEPAGE_SETTINGS_NOT_FOUND"; + 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"; + public const string KNOWLEDGE_PARTNER_NOT_FOUND = "KNOWLEDGE_PARTNER_NOT_FOUND"; + public const string POLICY_SECTION_NOT_FOUND = "POLICY_SECTION_NOT_FOUND"; + public const string CONTENT_UPDATE_FAILED = "CONTENT_UPDATE_FAILED"; + public const string SETTINGS_UPDATED = "SETTINGS_UPDATED"; + } + + public static class Media + { + public const string MEDIA_FILE_NOT_FOUND = "MEDIA_FILE_NOT_FOUND"; + public const string INVALID_FILE_TYPE = "INVALID_FILE_TYPE"; + public const string FILE_TOO_LARGE = "FILE_TOO_LARGE"; + public const string EMPTY_FILE = "EMPTY_FILE"; + public const string MEDIA_UPLOADED = "MEDIA_UPLOADED"; + public const string MEDIA_UPDATED = "MEDIA_UPDATED"; + public const string MEDIA_DELETED = "MEDIA_DELETED"; + } + + public static class Verification + { + public const string OTP_NOT_FOUND = "OTP_NOT_FOUND"; + 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"; + public const string OTP_COOLDOWN_ACTIVE = "OTP_COOLDOWN_ACTIVE"; + public const string OTP_INVALIDATED = "OTP_INVALIDATED"; + public const string CONTACT_ALREADY_TAKEN = "CONTACT_ALREADY_TAKEN"; + public const string EMAIL_UPDATED = "EMAIL_UPDATED"; + public const string PHONE_UPDATED = "PHONE_UPDATED"; + } + + public static class Lookups + { + public const string COUNTRY_CODE_NOT_FOUND = "COUNTRY_CODE_NOT_FOUND"; + public const string LOOKUP_CREATED = "LOOKUP_CREATED"; + public const string LOOKUP_UPDATED = "LOOKUP_UPDATED"; } public static class KnowledgeMap diff --git a/backend/src/CCE.Application/Messages/MessageFactory.cs b/backend/src/CCE.Application/Messages/MessageFactory.cs index 3f248781..98c9aaa4 100644 --- a/backend/src/CCE.Application/Messages/MessageFactory.cs +++ b/backend/src/CCE.Application/Messages/MessageFactory.cs @@ -2,6 +2,7 @@ using CCE.Application.Errors; using CCE.Application.Localization; using CCE.Domain.Common; +using Microsoft.Extensions.Logging; namespace CCE.Application.Messages; @@ -13,21 +14,26 @@ namespace CCE.Application.Messages; public sealed class MessageFactory { private readonly ILocalizationService _l; + private readonly ILogger _logger; - public MessageFactory(ILocalizationService l) => _l = l; + public MessageFactory(ILocalizationService l, ILogger logger) + { + _l = l; + _logger = logger; + } // ─── Success builders (domain key → CON0xx) ─── public Response Ok(T data, string domainKey) { - var code = SystemCodeMap.ToSystemCode(domainKey); + var code = ResolveCode(domainKey); var msg = Localize(domainKey); return Response.Ok(data, code, msg); } public Response Ok(string domainKey) { - var code = SystemCodeMap.ToSystemCode(domainKey); + var code = ResolveCode(domainKey); var msg = Localize(domainKey); return Response.Ok(code, msg); } @@ -52,7 +58,7 @@ public Response BusinessRule(string domainKey) public Response ValidationError( string domainKey, IReadOnlyList fieldErrors) { - var code = SystemCodeMap.ToSystemCode(domainKey); + var code = ResolveCode(domainKey); var msg = Localize(domainKey); return Response.Fail(code, msg, MessageType.Validation, fieldErrors); } @@ -61,90 +67,105 @@ public Response ValidationError( public FieldError Field(string fieldName, string domainKey) { - var code = SystemCodeMap.ToSystemCode(domainKey); + var code = ResolveCode(domainKey); var msg = Localize(domainKey); return new FieldError(fieldName, code, msg); } // ─── Convenience shortcuts (Identity domain) ─── - public Response UserNotFound() => NotFound("USER_NOT_FOUND"); - public Response EmailExists() => Conflict("EMAIL_EXISTS"); - public Response InvalidCredentials() => Unauthorized("INVALID_CREDENTIALS"); - public Response NotAuthenticated() => Unauthorized("NOT_AUTHENTICATED"); + public Response UserNotFound() => NotFound(ApplicationErrors.Identity.USER_NOT_FOUND); + public Response EmailExists() => Conflict(ApplicationErrors.Identity.EMAIL_EXISTS); + public Response InvalidCredentials() => Unauthorized(ApplicationErrors.Identity.INVALID_CREDENTIALS); + public Response NotAuthenticated() => Unauthorized(ApplicationErrors.Identity.NOT_AUTHENTICATED); // ─── 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 CategoryNotFound() => NotFound("CATEGORY_NOT_FOUND"); - public Response AssetNotFound() => NotFound("ASSET_NOT_FOUND"); - public Response AssetNotClean() => BusinessRule("ASSET_NOT_CLEAN"); + public Response NewsNotFound() => NotFound(ApplicationErrors.Content.NEWS_NOT_FOUND); + public Response EventNotFound() => NotFound(ApplicationErrors.Content.EVENT_NOT_FOUND); + public Response ResourceNotFound() => NotFound(ApplicationErrors.Content.RESOURCE_NOT_FOUND); + public Response PageNotFound() => NotFound(ApplicationErrors.Content.PAGE_NOT_FOUND); + public Response CategoryNotFound() => NotFound(ApplicationErrors.Content.CATEGORY_NOT_FOUND); + public Response AssetNotFound() => NotFound(ApplicationErrors.Content.ASSET_NOT_FOUND); + public Response AssetNotClean() => BusinessRule(ApplicationErrors.Content.ASSET_NOT_CLEAN); // ─── Convenience shortcuts (Identity / Expert domain) ─── - public Response ExpertRequestNotFound() => NotFound("EXPERT_REQUEST_NOT_FOUND"); + public Response ExpertRequestNotFound() => NotFound(ApplicationErrors.Identity.EXPERT_REQUEST_NOT_FOUND); // ─── Convenience shortcuts (Platform Settings domain) ─── - public Response HomepageSettingsNotFound() => NotFound("HOMEPAGE_SETTINGS_NOT_FOUND"); - public Response AboutSettingsNotFound() => NotFound("ABOUT_SETTINGS_NOT_FOUND"); - public Response PoliciesSettingsNotFound() => NotFound("POLICIES_SETTINGS_NOT_FOUND"); - public Response GlossaryEntryNotFound() => NotFound("GLOSSARY_ENTRY_NOT_FOUND"); - public Response KnowledgePartnerNotFound() => NotFound("KNOWLEDGE_PARTNER_NOT_FOUND"); - public Response PolicySectionNotFound() => NotFound("POLICY_SECTION_NOT_FOUND"); - public Response ContentUpdateFailed() => BusinessRule("CONTENT_UPDATE_FAILED"); + 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); // ─── Convenience shortcuts (Media domain) ─── - public Response MediaFileNotFound() => NotFound("MEDIA_FILE_NOT_FOUND"); - public Response InvalidFileType() => BusinessRule("INVALID_FILE_TYPE"); - public Response FileTooLarge() => BusinessRule("FILE_TOO_LARGE"); - public Response EmptyFile() => BusinessRule("EMPTY_FILE"); + 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); // ─── Convenience shortcuts (Verification domain) ─── - public Response OtpNotFound() => NotFound("OTP_NOT_FOUND"); - public Response OtpExpired() => BusinessRule("OTP_EXPIRED"); - public Response OtpInvalidCode() => BusinessRule("OTP_INVALID_CODE"); - public Response OtpMaxAttempts() => BusinessRule("OTP_MAX_ATTEMPTS"); - public Response OtpCooldownActive() => BusinessRule("OTP_COOLDOWN_ACTIVE"); - public Response OtpInvalidated() => BusinessRule("OTP_INVALIDATED"); - public Response ContactAlreadyTaken() => Conflict("CONTACT_ALREADY_TAKEN"); - public Response EmailUpdated() => Ok("EMAIL_UPDATED"); - public Response PhoneUpdated() => Ok("PHONE_UPDATED"); + 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); // ─── 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(ApplicationErrors.Evaluation.EVALUATION_SUBMITTED); + public Response EvaluationNotFound() => NotFound(ApplicationErrors.Evaluation.EVALUATION_NOT_FOUND); // ─── Convenience shortcuts (Notification domain) ─── - public Response NotificationTemplateNotFound() => NotFound("TEMPLATE_NOT_FOUND"); - public Response NotificationLogNotFound() => NotFound("NOTIFICATION_NOT_FOUND"); - public Response NotificationSettingsUpdated() => Ok("NOTIFICATION_SETTINGS_UPDATED"); - public Response NotificationMarkedRead() => Ok("NOTIFICATION_MARKED_READ"); - public Response NotificationsMarkedRead(int count) => Ok(count, "NOTIFICATIONS_MARKED_READ"); - public Response NotificationRetried(T data) => Ok(data, "NOTIFICATION_RETRIED"); - public Response NotificationTemplateCreated(T data) => Ok(data, "NOTIFICATION_TEMPLATE_CREATED"); - public Response NotificationTemplateUpdated(T data) => Ok(data, "NOTIFICATION_TEMPLATE_UPDATED"); + 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); // ─── Convenience shortcuts (Lookups domain) ─── - public Response CountryCodeNotFound() => NotFound("COUNTRY_CODE_NOT_FOUND"); - public Response LookupCreated(T data) => Ok(data, "LOOKUP_CREATED"); - public Response LookupUpdated(T data) => Ok(data, "LOOKUP_UPDATED"); + 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); // ─── Private ─── private Response Fail(string domainKey, MessageType type) { - var code = SystemCodeMap.ToSystemCode(domainKey); + var code = ResolveCode(domainKey); var msg = Localize(domainKey); return Response.Fail(code, msg, type); } - private string Localize(string domainKey) => _l.GetString(domainKey); + private string ResolveCode(string domainKey) + { + var code = SystemCodeMap.ToSystemCode(domainKey); + if (code == SystemCode.ERR900 && domainKey != ApplicationErrors.General.INTERNAL_ERROR) + _logger.LogWarning("Domain key {DomainKey} has no SystemCodeMap entry and fell back to ERR900", domainKey); + return code; + } + + private string Localize(string domainKey) + { + var result = _l.GetString(domainKey); + if (result == domainKey) + _logger.LogWarning("Domain key {DomainKey} has no translation in Resources.yaml", domainKey); + return result; + } } diff --git a/backend/src/CCE.Application/Messages/SystemCode.cs b/backend/src/CCE.Application/Messages/SystemCode.cs index aa254095..9db2eefb 100644 --- a/backend/src/CCE.Application/Messages/SystemCode.cs +++ b/backend/src/CCE.Application/Messages/SystemCode.cs @@ -106,6 +106,9 @@ public static class SystemCode public const string ERR057 = "ERR057"; // Knowledge partner not found public const string ERR058 = "ERR058"; // Policy section not found + // ─── Lookups Errors ─── + public const string ERR130 = "ERR130"; // Country code not found + // ─── Verification Errors ─── public const string ERR120 = "ERR120"; // OTP not found public const string ERR121 = "ERR121"; // OTP expired @@ -205,6 +208,10 @@ public static class SystemCode public const string CON046 = "CON046"; // Notification template created public const string CON047 = "CON047"; // Notification template updated + // ─── Lookups Success ─── + public const string CON070 = "CON070"; // Lookup created + public const string CON071 = "CON071"; // Lookup updated + // ─── General Success ─── public const string CON100 = "CON100"; // Items listed successfully public const string CON900 = "CON900"; // Operation completed successfully diff --git a/backend/src/CCE.Application/Messages/SystemCodeMap.cs b/backend/src/CCE.Application/Messages/SystemCodeMap.cs index 112d3f54..aa0cf0d7 100644 --- a/backend/src/CCE.Application/Messages/SystemCodeMap.cs +++ b/backend/src/CCE.Application/Messages/SystemCodeMap.cs @@ -89,6 +89,9 @@ public static class SystemCodeMap ["KNOWLEDGE_PARTNER_NOT_FOUND"] = SystemCode.ERR057, ["POLICY_SECTION_NOT_FOUND"] = SystemCode.ERR058, + // ─── Lookups Errors ─── + ["COUNTRY_CODE_NOT_FOUND"] = SystemCode.ERR130, + // ─── Verification Errors ─── ["OTP_NOT_FOUND"] = SystemCode.ERR120, ["OTP_EXPIRED"] = SystemCode.ERR121, @@ -156,6 +159,10 @@ public static class SystemCodeMap ["RESOURCE_SHARE_SUCCESS"] = SystemCode.CON002, ["RESOURCE_SHARE_FAILED"] = SystemCode.ERR003, + // ─── Lookups Success ─── + ["LOOKUP_CREATED"] = SystemCode.CON070, + ["LOOKUP_UPDATED"] = SystemCode.CON071, + // ─── Notification Success ─── ["NOTIFICATION_CREATED"] = SystemCode.CON040, ["NOTIFICATION_MARKED_READ"] = SystemCode.CON041, diff --git a/backend/tests/CCE.Application.Tests/CCE.Application.Tests.csproj b/backend/tests/CCE.Application.Tests/CCE.Application.Tests.csproj index 306df1d6..88de16d9 100644 --- a/backend/tests/CCE.Application.Tests/CCE.Application.Tests.csproj +++ b/backend/tests/CCE.Application.Tests/CCE.Application.Tests.csproj @@ -30,4 +30,11 @@ + + + + PreserveNewest + + + diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/CreateEventCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/CreateEventCommandHandlerTests.cs index 5d87f599..1583f3d5 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/CreateEventCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/CreateEventCommandHandlerTests.cs @@ -60,7 +60,7 @@ private static (CreateEventCommandHandler sut, db.Topics.Returns(new[] { topic }.AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - var sut = new CreateEventCommandHandler(repo, db, new FakeSystemClock(), new MessageFactory(localization)); + var sut = new CreateEventCommandHandler(repo, db, new FakeSystemClock(), new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); return (sut, repo, db); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/CreateNewsCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/CreateNewsCommandHandlerTests.cs index ebc82ebd..39c84320 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/CreateNewsCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/CreateNewsCommandHandlerTests.cs @@ -68,7 +68,7 @@ private static (CreateNewsCommandHandler sut, user.GetUserId().Returns(System.Guid.NewGuid()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - var sut = new CreateNewsCommandHandler(repo, db, user, new FakeSystemClock(), new MessageFactory(localization)); + var sut = new CreateNewsCommandHandler(repo, db, user, new FakeSystemClock(), new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); return (sut, repo, db); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/CreateResourceCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/CreateResourceCommandHandlerTests.cs index 795e197c..41cf6a10 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/CreateResourceCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/CreateResourceCommandHandlerTests.cs @@ -122,7 +122,7 @@ private static (CreateResourceCommandHandler sut, var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - var sut = new CreateResourceCommandHandler(repo, db, user, Clock, new MessageFactory(localization)); + var sut = new CreateResourceCommandHandler(repo, db, user, Clock, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); return (sut, repo, db); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/DeleteEventCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/DeleteEventCommandHandlerTests.cs index e262d5c2..1e62e06f 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/DeleteEventCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/DeleteEventCommandHandlerTests.cs @@ -94,6 +94,6 @@ private static DeleteEventCommandHandler BuildHandler( { var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new DeleteEventCommandHandler(repo, db, currentUser, clock, new MessageFactory(localization)); + return new DeleteEventCommandHandler(repo, db, currentUser, clock, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/DeleteNewsCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/DeleteNewsCommandHandlerTests.cs index 1f29ae01..b748cb9f 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/DeleteNewsCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/DeleteNewsCommandHandlerTests.cs @@ -84,6 +84,6 @@ private static DeleteNewsCommandHandler BuildHandler( { var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new DeleteNewsCommandHandler(repo, db, currentUser, clock, new MessageFactory(localization)); + return new DeleteNewsCommandHandler(repo, db, currentUser, clock, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/PublishNewsCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/PublishNewsCommandHandlerTests.cs index 89764575..209506ca 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/PublishNewsCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/PublishNewsCommandHandlerTests.cs @@ -62,6 +62,6 @@ private static (PublishNewsCommandHandler sut, var db = Substitute.For(); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return (new PublishNewsCommandHandler(repo, db, clock, new MessageFactory(localization)), repo, db); + return (new PublishNewsCommandHandler(repo, db, clock, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)), repo, db); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/PublishResourceCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/PublishResourceCommandHandlerTests.cs index ce513184..c078bf41 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/PublishResourceCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/PublishResourceCommandHandlerTests.cs @@ -92,6 +92,6 @@ private static (PublishResourceCommandHandler sut, ICceDbContext db) BuildSut( var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return (new PublishResourceCommandHandler(db, Clock, new MessageFactory(localization)), db); + return (new PublishResourceCommandHandler(db, Clock, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)), db); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/RescheduleEventCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/RescheduleEventCommandHandlerTests.cs index 434f0199..74fa65ac 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/RescheduleEventCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/RescheduleEventCommandHandlerTests.cs @@ -57,6 +57,6 @@ private static (RescheduleEventCommandHandler sut, ICceDbContext db, IRepository var db = Substitute.For(); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return (new RescheduleEventCommandHandler(repo, db, new MessageFactory(localization)), db, repo); + return (new RescheduleEventCommandHandler(repo, db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)), db, repo); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateEventCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateEventCommandHandlerTests.cs index 2e7c3113..d6c54570 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateEventCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateEventCommandHandlerTests.cs @@ -67,6 +67,6 @@ private static (UpdateEventCommandHandler sut, ICceDbContext db, IRepository(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return (new UpdateEventCommandHandler(repo, db, new MessageFactory(localization)), db, repo); + return (new UpdateEventCommandHandler(repo, db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)), db, repo); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateNewsCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateNewsCommandHandlerTests.cs index a042a683..16ba16ab 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateNewsCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateNewsCommandHandlerTests.cs @@ -58,6 +58,6 @@ private static (UpdateNewsCommandHandler sut, ICceDbContext db, IRepository(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return (new UpdateNewsCommandHandler(repo, db, new MessageFactory(localization)), db, repo); + return (new UpdateNewsCommandHandler(repo, db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)), db, repo); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateResourceCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateResourceCommandHandlerTests.cs index 7c92390b..474cdb73 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/UpdateResourceCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/UpdateResourceCommandHandlerTests.cs @@ -70,6 +70,6 @@ private static (UpdateResourceCommandHandler sut, ICceDbContext db) BuildSut(Res var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return (new UpdateResourceCommandHandler(db, new MessageFactory(localization)), db); + return (new UpdateResourceCommandHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)), db); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Commands/UploadAssetCommandHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Commands/UploadAssetCommandHandlerTests.cs index cf77d9e1..12952c58 100644 --- a/backend/tests/CCE.Application.Tests/Content/Commands/UploadAssetCommandHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Commands/UploadAssetCommandHandlerTests.cs @@ -96,7 +96,7 @@ private static UploadAssetCommandHandler BuildSut( currentUser.GetUserId().Returns(currentUserId); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - var msg = new MessageFactory(localization); + var msg = new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); return new UploadAssetCommandHandler( storage, scanner, service, db, currentUser, new FakeSystemClock(), msg, NullLogger.Instance); diff --git a/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicEventByIdQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicEventByIdQueryHandlerTests.cs index 25a67e0f..b9203508 100644 --- a/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicEventByIdQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicEventByIdQueryHandlerTests.cs @@ -49,6 +49,6 @@ private static GetPublicEventByIdQueryHandler BuildSut(IEnumerable events db.Events.Returns(events.AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new GetPublicEventByIdQueryHandler(db, new MessageFactory(localization)); + return new GetPublicEventByIdQueryHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicNewsBySlugQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicNewsBySlugQueryHandlerTests.cs index 3ff134c2..7345f967 100644 --- a/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicNewsBySlugQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicNewsBySlugQueryHandlerTests.cs @@ -57,6 +57,6 @@ private static GetPublicNewsBySlugQueryHandler BuildSut(IEnumerable news) db.News.Returns(news.AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new GetPublicNewsBySlugQueryHandler(db, new MessageFactory(localization)); + return new GetPublicNewsBySlugQueryHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicResourceByIdQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicResourceByIdQueryHandlerTests.cs index 68b9ec40..e0e731ec 100644 --- a/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicResourceByIdQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Public/Queries/GetPublicResourceByIdQueryHandlerTests.cs @@ -64,6 +64,6 @@ private static GetPublicResourceByIdQueryHandler BuildSut(IEnumerable db.Resources.Returns(resources.AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new GetPublicResourceByIdQueryHandler(db, new MessageFactory(localization)); + return new GetPublicResourceByIdQueryHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicEventsQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicEventsQueryHandlerTests.cs index d6b08732..1513f3bf 100644 --- a/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicEventsQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicEventsQueryHandlerTests.cs @@ -74,6 +74,6 @@ private static ListPublicEventsQueryHandler BuildSut(IEnumerable events) db.Events.Returns(events.AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new ListPublicEventsQueryHandler(db, new MessageFactory(localization)); + return new ListPublicEventsQueryHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicNewsQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicNewsQueryHandlerTests.cs index c802920e..33764c5a 100644 --- a/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicNewsQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicNewsQueryHandlerTests.cs @@ -68,6 +68,6 @@ private static ListPublicNewsQueryHandler BuildSut(IEnumerable news) db.News.Returns(news.AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new ListPublicNewsQueryHandler(db, new MessageFactory(localization)); + return new ListPublicNewsQueryHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicResourcesQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicResourcesQueryHandlerTests.cs index 6373933a..daae4a1c 100644 --- a/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicResourcesQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Public/Queries/ListPublicResourcesQueryHandlerTests.cs @@ -105,6 +105,6 @@ private static ListPublicResourcesQueryHandler BuildSut(IEnumerable re db.Countries.Returns(Array.Empty().AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new ListPublicResourcesQueryHandler(db, new MessageFactory(localization)); + return new ListPublicResourcesQueryHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Queries/GetAssetByIdQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Queries/GetAssetByIdQueryHandlerTests.cs index 5e8742b7..01d519e7 100644 --- a/backend/tests/CCE.Application.Tests/Content/Queries/GetAssetByIdQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Queries/GetAssetByIdQueryHandlerTests.cs @@ -54,6 +54,6 @@ private static GetAssetByIdQueryHandler BuildSut(IEnumerable assets) db.AssetFiles.Returns(assets.AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new GetAssetByIdQueryHandler(db, new MessageFactory(localization)); + return new GetAssetByIdQueryHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Queries/GetEventByIdQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Queries/GetEventByIdQueryHandlerTests.cs index 88ec5672..610cafeb 100644 --- a/backend/tests/CCE.Application.Tests/Content/Queries/GetEventByIdQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Queries/GetEventByIdQueryHandlerTests.cs @@ -56,6 +56,6 @@ private static GetEventByIdQueryHandler BuildSut(IEnumerable events) db.Events.Returns(events.AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new GetEventByIdQueryHandler(db, new MessageFactory(localization)); + return new GetEventByIdQueryHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Queries/GetNewsByIdQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Queries/GetNewsByIdQueryHandlerTests.cs index 1d8a64be..a9807a74 100644 --- a/backend/tests/CCE.Application.Tests/Content/Queries/GetNewsByIdQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Queries/GetNewsByIdQueryHandlerTests.cs @@ -55,6 +55,6 @@ private static GetNewsByIdQueryHandler BuildSut(IEnumerable news) db.News.Returns(news.AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new GetNewsByIdQueryHandler(db, new MessageFactory(localization)); + return new GetNewsByIdQueryHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Queries/ListEventsQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Queries/ListEventsQueryHandlerTests.cs index 23db547e..ed7ea897 100644 --- a/backend/tests/CCE.Application.Tests/Content/Queries/ListEventsQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Queries/ListEventsQueryHandlerTests.cs @@ -86,6 +86,6 @@ private static ListEventsQueryHandler BuildSut(IEnumerable events) db.Events.Returns(events.AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new ListEventsQueryHandler(db, new MessageFactory(localization)); + return new ListEventsQueryHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Queries/ListNewsQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Queries/ListNewsQueryHandlerTests.cs index 37979245..4adf9c7d 100644 --- a/backend/tests/CCE.Application.Tests/Content/Queries/ListNewsQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Queries/ListNewsQueryHandlerTests.cs @@ -93,6 +93,6 @@ private static ListNewsQueryHandler BuildSut(IEnumerable news) db.News.Returns(news.AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new ListNewsQueryHandler(db, new MessageFactory(localization)); + return new ListNewsQueryHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Content/Queries/ListResourcesQueryHandlerTests.cs b/backend/tests/CCE.Application.Tests/Content/Queries/ListResourcesQueryHandlerTests.cs index fb00345c..11d6eb0f 100644 --- a/backend/tests/CCE.Application.Tests/Content/Queries/ListResourcesQueryHandlerTests.cs +++ b/backend/tests/CCE.Application.Tests/Content/Queries/ListResourcesQueryHandlerTests.cs @@ -122,6 +122,6 @@ private static ListResourcesQueryHandler BuildSut(IEnumerable resource db.Countries.Returns(Array.Empty().AsQueryable()); var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call.ArgAt(0)); - return new ListResourcesQueryHandler(db, new MessageFactory(localization)); + return new ListResourcesQueryHandler(db, new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); } } diff --git a/backend/tests/CCE.Application.Tests/Identity/IdentityTestHelpers.cs b/backend/tests/CCE.Application.Tests/Identity/IdentityTestHelpers.cs index 8a91b783..cf2a1306 100644 --- a/backend/tests/CCE.Application.Tests/Identity/IdentityTestHelpers.cs +++ b/backend/tests/CCE.Application.Tests/Identity/IdentityTestHelpers.cs @@ -12,6 +12,6 @@ public static MessageFactory BuildMsg() localization.GetString(Arg.Any(), Arg.Any()) .Returns(call => call.ArgAt(0)); - return new MessageFactory(localization); + return new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); } } diff --git a/backend/tests/CCE.Application.Tests/Messages/SystemCodeMapIntegrityTests.cs b/backend/tests/CCE.Application.Tests/Messages/SystemCodeMapIntegrityTests.cs new file mode 100644 index 00000000..11cf9f45 --- /dev/null +++ b/backend/tests/CCE.Application.Tests/Messages/SystemCodeMapIntegrityTests.cs @@ -0,0 +1,85 @@ +using System.Reflection; +using CCE.Application.Messages; +using CCE.Infrastructure.Localization; + +namespace CCE.Application.Tests.Messages; + +/// +/// Compile-time-equivalent safety net for the message pipeline. +/// A failing test here means a key will silently produce ERR900 or a raw key string +/// at runtime — i.e. a real client-visible bug. +/// +public sealed class SystemCodeMapIntegrityTests +{ + private static readonly LocalizationService Localization = new(new YamlLocalizationStore()); + + private static readonly Dictionary DomainToCode = + (Dictionary)typeof(SystemCodeMap) + .GetField("DomainToCode", BindingFlags.NonPublic | BindingFlags.Static)! + .GetValue(null)!; + + [Fact] + public void Every_domain_key_has_Arabic_translation_in_Resources_yaml() + { + var missing = DomainToCode.Keys + .Where(key => + { + var value = Localization.GetString(key, "ar"); + return string.IsNullOrWhiteSpace(value) || value == key; + }) + .OrderBy(k => k) + .ToList(); + + missing.Should().BeEmpty( + because: "every SystemCodeMap domain key must have an Arabic translation in Resources.yaml; " + + "missing: {0}", string.Join(", ", missing)); + } + + [Fact] + public void Every_domain_key_has_English_translation_in_Resources_yaml() + { + var missing = DomainToCode.Keys + .Where(key => + { + var value = Localization.GetString(key, "en"); + return string.IsNullOrWhiteSpace(value) || value == key; + }) + .OrderBy(k => k) + .ToList(); + + missing.Should().BeEmpty( + because: "every SystemCodeMap domain key must have an English translation in Resources.yaml; " + + "missing: {0}", string.Join(", ", missing)); + } + + [Fact] + public void No_two_domain_keys_share_the_same_system_code() + { + var duplicates = DomainToCode + .GroupBy(kv => kv.Value, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .Select(g => $"{g.Key} → [{string.Join(", ", g.Select(kv => kv.Key))}]") + .OrderBy(s => s) + .ToList(); + + duplicates.Should().BeEmpty( + because: "each domain key must map to a unique system code; duplicates: {0}", + string.Join(" | ", duplicates)); + } + + [Fact] + public void Every_SystemCode_constant_value_matches_its_field_name() + { + var mismatches = typeof(SystemCode) + .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(f => f.FieldType == typeof(string)) + .Where(f => (string)f.GetValue(null)! != f.Name) + .Select(f => $"{f.Name} = \"{f.GetValue(null)}\"") + .OrderBy(s => s) + .ToList(); + + mismatches.Should().BeEmpty( + because: "a SystemCode constant's value must equal its field name to prevent copy-paste drift; " + + "mismatches: {0}", string.Join(", ", mismatches)); + } +} diff --git a/backend/tests/CCE.Application.Tests/Notifications/NotificationTestMessages.cs b/backend/tests/CCE.Application.Tests/Notifications/NotificationTestMessages.cs index 4a695904..b899e277 100644 --- a/backend/tests/CCE.Application.Tests/Notifications/NotificationTestMessages.cs +++ b/backend/tests/CCE.Application.Tests/Notifications/NotificationTestMessages.cs @@ -9,6 +9,6 @@ public static MessageFactory Create() { var localization = Substitute.For(); localization.GetString(Arg.Any(), Arg.Any()).Returns(call => call[0]!.ToString()!); - return new MessageFactory(localization); + return new MessageFactory(localization, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); } }