# Club Feature Management Services - Implementation Guide ## Overview Admin services for managing user club features (enable/disable features per user). ## Created Files ### 1. CQRS Layer (Application) #### Query: GetUserClubFeatures **Location:** `/CMS/src/CMSMicroservice.Application/ClubFeatureCQ/Queries/GetUserClubFeatures/` **Files:** - `GetUserClubFeaturesQuery.cs` - Query definition - `GetUserClubFeaturesQueryHandler.cs` - Query handler - `UserClubFeatureDto.cs` - Response DTO **Purpose:** Get list of all club features for a specific user with their active status. **Input:** ```csharp public record GetUserClubFeaturesQuery : IRequest> { public long UserId { get; init; } } ``` **Output:** ```csharp public class UserClubFeatureDto { public long Id { get; set; } public long UserId { get; set; } public long ClubMembershipId { get; set; } public long ClubFeatureId { get; set; } public string FeatureTitle { get; set; } public string? FeatureDescription { get; set; } public bool IsActive { get; set; } public DateTime GrantedAt { get; set; } public string? Notes { get; set; } } ``` **Logic:** - Joins `UserClubFeatures` with `ClubFeature` table - Filters by `UserId` and `!IsDeleted` - Returns list of features with their active status --- #### Command: ToggleUserClubFeature **Location:** `/CMS/src/CMSMicroservice.Application/ClubFeatureCQ/Commands/ToggleUserClubFeature/` **Files:** - `ToggleUserClubFeatureCommand.cs` - Command definition - `ToggleUserClubFeatureCommandHandler.cs` - Command handler - `ToggleUserClubFeatureResponse.cs` - Response DTO **Purpose:** Enable or disable a specific club feature for a user. **Input:** ```csharp public record ToggleUserClubFeatureCommand : IRequest { public long UserId { get; init; } public long ClubFeatureId { get; init; } public bool IsActive { get; init; } } ``` **Output:** ```csharp public class ToggleUserClubFeatureResponse { public bool Success { get; set; } public string Message { get; set; } public long? UserClubFeatureId { get; set; } public bool? IsActive { get; set; } } ``` **Validations:** 1. ✅ User exists and not deleted 2. ✅ Club feature exists and not deleted 3. ✅ User has this feature assigned (exists in UserClubFeatures) **Logic:** - Find `UserClubFeature` record by `UserId` + `ClubFeatureId` - Update `IsActive` field - Set `LastModified` timestamp - Save changes **Error Messages:** - "کاربر یافت نشد" - User not found - "ویژگی باشگاه یافت نشد" - Club feature not found - "این ویژگی برای کاربر یافت نشد" - User doesn't have this feature **Success Messages:** - "ویژگی با موفقیت فعال شد" - Feature activated successfully - "ویژگی با موفقیت غیرفعال شد" - Feature deactivated successfully --- ### 2. gRPC Layer (Protobuf + WebApi) #### Proto Definition **File:** `/CMS/src/CMSMicroservice.Protobuf/Protos/clubmembership.proto` **Added RPC Methods:** ```protobuf rpc GetUserClubFeatures(GetUserClubFeaturesRequest) returns (GetUserClubFeaturesResponse){ option (google.api.http) = { get: "/ClubFeature/GetUserFeatures" }; }; rpc ToggleUserClubFeature(ToggleUserClubFeatureRequest) returns (ToggleUserClubFeatureResponse){ option (google.api.http) = { post: "/ClubFeature/ToggleFeature" body: "*" }; }; ``` **Message Definitions:** ```protobuf message GetUserClubFeaturesRequest { int64 user_id = 1; } message GetUserClubFeaturesResponse { repeated UserClubFeatureModel features = 1; } message UserClubFeatureModel { int64 id = 1; int64 user_id = 2; int64 club_membership_id = 3; int64 club_feature_id = 4; string feature_title = 5; string feature_description = 6; bool is_active = 7; google.protobuf.Timestamp granted_at = 8; string notes = 9; } message ToggleUserClubFeatureRequest { int64 user_id = 1; int64 club_feature_id = 2; bool is_active = 3; } message ToggleUserClubFeatureResponse { bool success = 1; string message = 2; google.protobuf.Int64Value user_club_feature_id = 3; google.protobuf.BoolValue is_active = 4; } ``` --- #### gRPC Service Implementation **File:** `/CMS/src/CMSMicroservice.WebApi/Services/ClubMembershipService.cs` **Added Methods:** ```csharp public override async Task GetUserClubFeatures( GetUserClubFeaturesRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle< GetUserClubFeaturesRequest, GetUserClubFeaturesQuery, GetUserClubFeaturesResponse>(request, context); } public override async Task ToggleUserClubFeature( ToggleUserClubFeatureRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle< ToggleUserClubFeatureRequest, ToggleUserClubFeatureCommand, Protobuf.Protos.ClubMembership.ToggleUserClubFeatureResponse>(request, context); } ``` --- #### AutoMapper Profile **File:** `/CMS/src/CMSMicroservice.WebApi/Common/Mappings/ClubFeatureProfile.cs` **Mappings:** 1. `GetUserClubFeaturesRequest` → `GetUserClubFeaturesQuery` 2. `UserClubFeatureDto` → `UserClubFeatureModel` (Proto) 3. `List` → `GetUserClubFeaturesResponse` 4. `ToggleUserClubFeatureRequest` → `ToggleUserClubFeatureCommand` 5. `ToggleUserClubFeatureResponse` (App) → `ToggleUserClubFeatureResponse` (Proto) **Special Handling:** - DateTime conversion to `Timestamp` (Protobuf format) - Null-safe mapping for optional fields - Fully qualified type names to avoid ambiguity --- ## API Endpoints ### 1. Get User Club Features **Method:** GET **Endpoint:** `/ClubFeature/GetUserFeatures` **Request:** ```json { "user_id": 123 } ``` **Response:** ```json { "features": [ { "id": 1, "user_id": 123, "club_membership_id": 456, "club_feature_id": 1, "feature_title": "دسترسی به فروشگاه تخفیف", "feature_description": "امکان خرید از فروشگاه تخفیف", "is_active": true, "granted_at": "2025-12-09T18:30:00Z", "notes": "اعطا شده به‌طور خودکار هنگام فعالسازی" } ] } ``` --- ### 2. Toggle User Club Feature **Method:** POST **Endpoint:** `/ClubFeature/ToggleFeature` **Request:** ```json { "user_id": 123, "club_feature_id": 1, "is_active": false } ``` **Response (Success):** ```json { "success": true, "message": "ویژگی با موفقیت غیرفعال شد", "user_club_feature_id": 1, "is_active": false } ``` **Response (Error - User Not Found):** ```json { "success": false, "message": "کاربر یافت نشد" } ``` **Response (Error - Feature Not Found):** ```json { "success": false, "message": "ویژگی باشگاه یافت نشد" } ``` **Response (Error - User Doesn't Have Feature):** ```json { "success": false, "message": "این ویژگی برای کاربر یافت نشد" } ``` --- ## Database Schema ### Table: UserClubFeatures Existing table with newly added `IsActive` field: ```sql CREATE TABLE [CMS].[UserClubFeatures] ( [Id] BIGINT IDENTITY(1,1) PRIMARY KEY, [UserId] BIGINT NOT NULL, [ClubMembershipId] BIGINT NOT NULL, [ClubFeatureId] BIGINT NOT NULL, [GrantedAt] DATETIME2 NOT NULL, [IsActive] BIT NOT NULL DEFAULT 1, -- ← NEW FIELD [Notes] NVARCHAR(MAX) NULL, [Created] DATETIME2 NOT NULL, [CreatedBy] NVARCHAR(MAX) NULL, [LastModified] DATETIME2 NULL, [LastModifiedBy] NVARCHAR(MAX) NULL, [IsDeleted] BIT NOT NULL DEFAULT 0, CONSTRAINT FK_UserClubFeatures_Users FOREIGN KEY ([UserId]) REFERENCES [Identity].[Users]([Id]), CONSTRAINT FK_UserClubFeatures_ClubMembership FOREIGN KEY ([ClubMembershipId]) REFERENCES [CMS].[ClubMembership]([Id]), CONSTRAINT FK_UserClubFeatures_ClubFeatures FOREIGN KEY ([ClubFeatureId]) REFERENCES [CMS].[ClubFeatures]([Id]) ); ``` --- ## Usage Examples ### Admin Panel Scenario #### 1. View User's Club Features ```csharp // Admin selects user ID: 123 var request = new GetUserClubFeaturesRequest { UserId = 123 }; var response = await client.GetUserClubFeaturesAsync(request); // Display in grid: foreach (var feature in response.Features) { Console.WriteLine($"Feature: {feature.FeatureTitle}"); Console.WriteLine($"Status: {(feature.IsActive ? "فعال" : "غیرفعال")}"); Console.WriteLine($"Granted: {feature.GrantedAt}"); Console.WriteLine("---"); } ``` **Output:** ``` Feature: دسترسی به فروشگاه تخفیف Status: فعال Granted: 2025-12-09 18:30:00 --- Feature: دسترسی به کمیسیون هفتگی Status: فعال Granted: 2025-12-09 18:30:00 --- Feature: دسترسی به شارژ شبکه Status: غیرفعال Granted: 2025-12-09 18:30:00 --- ``` --- #### 2. Disable a Feature ```csharp // Admin clicks "Disable" on Feature ID: 3 var request = new ToggleUserClubFeatureRequest { UserId = 123, ClubFeatureId = 3, IsActive = false }; var response = await client.ToggleUserClubFeatureAsync(request); if (response.Success) { Console.WriteLine(response.Message); // Output: ویژگی با موفقیت غیرفعال شد } ``` --- #### 3. Re-enable a Feature ```csharp // Admin clicks "Enable" on Feature ID: 3 var request = new ToggleUserClubFeatureRequest { UserId = 123, ClubFeatureId = 3, IsActive = true }; var response = await client.ToggleUserClubFeatureAsync(request); if (response.Success) { Console.WriteLine(response.Message); // Output: ویژگی با موفقیت فعال شد } ``` --- ## Testing Checklist ### Unit Tests (Recommended) - [ ] GetUserClubFeaturesQueryHandler returns correct DTOs - [ ] ToggleUserClubFeatureCommandHandler validates user exists - [ ] ToggleUserClubFeatureCommandHandler validates feature exists - [ ] ToggleUserClubFeatureCommandHandler validates user has feature - [ ] ToggleUserClubFeatureCommandHandler updates IsActive correctly - [ ] ToggleUserClubFeatureCommandHandler sets LastModified timestamp ### Integration Tests - [ ] gRPC GetUserClubFeatures endpoint returns data - [ ] gRPC ToggleUserClubFeature endpoint updates database - [ ] AutoMapper mappings work correctly - [ ] Proto serialization/deserialization works ### Manual Testing 1. **Get Features:** ```bash grpcurl -d '{"user_id": 123}' \ -plaintext localhost:5000 \ clubmembership.ClubMembershipContract/GetUserClubFeatures ``` 2. **Disable Feature:** ```bash grpcurl -d '{"user_id": 123, "club_feature_id": 1, "is_active": false}' \ -plaintext localhost:5000 \ clubmembership.ClubMembershipContract/ToggleUserClubFeature ``` 3. **Verify in Database:** ```sql SELECT Id, UserId, ClubFeatureId, IsActive, LastModified FROM CMS.UserClubFeatures WHERE UserId = 123; ``` --- ## Build Status ✅ **All projects build successfully** - CMSMicroservice.Domain: ✅ - CMSMicroservice.Application: ✅ (0 errors, 274 warnings) - CMSMicroservice.Protobuf: ✅ - CMSMicroservice.WebApi: ✅ (0 errors, 17 warnings) --- ## Next Steps (Optional Enhancements) 1. **Authorization:** - Add `[Authorize(Roles = "Admin")]` attribute - Validate admin permissions before toggling 2. **Audit Logging:** - Log who changed the feature status - Track `LastModifiedBy` field 3. **Bulk Operations:** - Add endpoint to toggle multiple features at once - Add endpoint to enable/disable all features for a user 4. **History Tracking:** - Create `UserClubFeatureHistory` table - Log every status change with timestamp and reason 5. **Notifications:** - Send notification to user when feature is disabled - Email/SMS alert for important features 6. **Business Rules:** - Add validation: prevent disabling critical features - Add expiration dates for features - Add feature dependencies (e.g., Feature B requires Feature A) --- ## Summary ✅ Created CQRS Query + Command for club feature management ✅ Created gRPC Proto definitions and services ✅ Created AutoMapper mappings ✅ All builds successful ✅ Ready for deployment and testing **Total Files Created:** 8 **Total Lines of Code:** ~350 **Build Errors:** 0 **Status:** ✅ Complete and ready for use