Files
CMS/docs/club-feature-management-services.md

491 lines
12 KiB
Markdown
Raw Normal View History

# 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<List<UserClubFeatureDto>>
{
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<ToggleUserClubFeatureResponse>
{
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<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:**
1. `GetUserClubFeaturesRequest``GetUserClubFeaturesQuery`
2. `UserClubFeatureDto``UserClubFeatureModel` (Proto)
3. `List<UserClubFeatureDto>``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