12 KiB
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 definitionGetUserClubFeaturesQueryHandler.cs- Query handlerUserClubFeatureDto.cs- Response DTO
Purpose: Get list of all club features for a specific user with their active status.
Input:
public record GetUserClubFeaturesQuery : IRequest<List<UserClubFeatureDto>>
{
public long UserId { get; init; }
}
Output:
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
UserClubFeatureswithClubFeaturetable - Filters by
UserIdand!IsDeleted - Returns list of features with their active status
Command: ToggleUserClubFeature
Location: /CMS/src/CMSMicroservice.Application/ClubFeatureCQ/Commands/ToggleUserClubFeature/
Files:
ToggleUserClubFeatureCommand.cs- Command definitionToggleUserClubFeatureCommandHandler.cs- Command handlerToggleUserClubFeatureResponse.cs- Response DTO
Purpose: Enable or disable a specific club feature for a user.
Input:
public record ToggleUserClubFeatureCommand : IRequest<ToggleUserClubFeatureResponse>
{
public long UserId { get; init; }
public long ClubFeatureId { get; init; }
public bool IsActive { get; init; }
}
Output:
public class ToggleUserClubFeatureResponse
{
public bool Success { get; set; }
public string Message { get; set; }
public long? UserClubFeatureId { get; set; }
public bool? IsActive { get; set; }
}
Validations:
- ✅ User exists and not deleted
- ✅ Club feature exists and not deleted
- ✅ User has this feature assigned (exists in UserClubFeatures)
Logic:
- Find
UserClubFeaturerecord byUserId+ClubFeatureId - Update
IsActivefield - Set
LastModifiedtimestamp - 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:
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:
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:
public override async Task<GetUserClubFeaturesResponse> GetUserClubFeatures(
GetUserClubFeaturesRequest request,
ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<
GetUserClubFeaturesRequest,
GetUserClubFeaturesQuery,
GetUserClubFeaturesResponse>(request, context);
}
public override async Task<Protobuf.Protos.ClubMembership.ToggleUserClubFeatureResponse>
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:
GetUserClubFeaturesRequest→GetUserClubFeaturesQueryUserClubFeatureDto→UserClubFeatureModel(Proto)List<UserClubFeatureDto>→GetUserClubFeaturesResponseToggleUserClubFeatureRequest→ToggleUserClubFeatureCommandToggleUserClubFeatureResponse(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:
{
"user_id": 123
}
Response:
{
"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:
{
"user_id": 123,
"club_feature_id": 1,
"is_active": false
}
Response (Success):
{
"success": true,
"message": "ویژگی با موفقیت غیرفعال شد",
"user_club_feature_id": 1,
"is_active": false
}
Response (Error - User Not Found):
{
"success": false,
"message": "کاربر یافت نشد"
}
Response (Error - Feature Not Found):
{
"success": false,
"message": "ویژگی باشگاه یافت نشد"
}
Response (Error - User Doesn't Have Feature):
{
"success": false,
"message": "این ویژگی برای کاربر یافت نشد"
}
Database Schema
Table: UserClubFeatures
Existing table with newly added IsActive field:
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
// 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
// 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
// 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
-
Get Features:
grpcurl -d '{"user_id": 123}' \ -plaintext localhost:5000 \ clubmembership.ClubMembershipContract/GetUserClubFeatures -
Disable Feature:
grpcurl -d '{"user_id": 123, "club_feature_id": 1, "is_active": false}' \ -plaintext localhost:5000 \ clubmembership.ClubMembershipContract/ToggleUserClubFeature -
Verify in Database:
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)
-
Authorization:
- Add
[Authorize(Roles = "Admin")]attribute - Validate admin permissions before toggling
- Add
-
Audit Logging:
- Log who changed the feature status
- Track
LastModifiedByfield
-
Bulk Operations:
- Add endpoint to toggle multiple features at once
- Add endpoint to enable/disable all features for a user
-
History Tracking:
- Create
UserClubFeatureHistorytable - Log every status change with timestamp and reason
- Create
-
Notifications:
- Send notification to user when feature is disabled
- Email/SMS alert for important features
-
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