Compare commits

...

2 Commits

Author SHA1 Message Date
masoodafar-web
12749ccb01 chore: bump version to 0.0.146
All checks were successful
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 11m40s
2025-12-12 01:41:48 +03:30
masoodafar-web
ff1c1d5d61 feat: add IsActive field to UserClubFeatures for admin management 2025-12-12 01:40:26 +03:30
69 changed files with 11457 additions and 199 deletions

View File

@@ -0,0 +1,490 @@
# 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

View File

@@ -0,0 +1,24 @@
using MediatR;
namespace CMSMicroservice.Application.ClubFeatureCQ.Commands.ToggleUserClubFeature;
/// <summary>
/// کامند برای فعال/غیرفعال کردن یک ویژگی باشگاه برای کاربر
/// </summary>
public record ToggleUserClubFeatureCommand : IRequest<ToggleUserClubFeatureResponse>
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; init; }
/// <summary>
/// شناسه ویژگی باشگاه
/// </summary>
public long ClubFeatureId { get; init; }
/// <summary>
/// وضعیت مورد نظر (فعال/غیرفعال)
/// </summary>
public bool IsActive { get; init; }
}

View File

@@ -0,0 +1,78 @@
using CMSMicroservice.Application.Common.Interfaces;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.ClubFeatureCQ.Commands.ToggleUserClubFeature;
/// <summary>
/// هندلر برای فعال/غیرفعال کردن ویژگی باشگاه کاربر
/// </summary>
public class ToggleUserClubFeatureCommandHandler : IRequestHandler<ToggleUserClubFeatureCommand, ToggleUserClubFeatureResponse>
{
private readonly IApplicationDbContext _context;
public ToggleUserClubFeatureCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<ToggleUserClubFeatureResponse> Handle(ToggleUserClubFeatureCommand request, CancellationToken cancellationToken)
{
// بررسی وجود کاربر
var userExists = await _context.Users
.AnyAsync(u => u.Id == request.UserId && !u.IsDeleted, cancellationToken);
if (!userExists)
{
return new ToggleUserClubFeatureResponse
{
Success = false,
Message = "کاربر یافت نشد"
};
}
// بررسی وجود ویژگی
var featureExists = await _context.ClubFeatures
.AnyAsync(cf => cf.Id == request.ClubFeatureId && !cf.IsDeleted, cancellationToken);
if (!featureExists)
{
return new ToggleUserClubFeatureResponse
{
Success = false,
Message = "ویژگی باشگاه یافت نشد"
};
}
// یافتن رکورد UserClubFeature
var userClubFeature = await _context.UserClubFeatures
.FirstOrDefaultAsync(
ucf => ucf.UserId == request.UserId
&& ucf.ClubFeatureId == request.ClubFeatureId
&& !ucf.IsDeleted,
cancellationToken);
if (userClubFeature == null)
{
return new ToggleUserClubFeatureResponse
{
Success = false,
Message = "این ویژگی برای کاربر یافت نشد"
};
}
// به‌روزرسانی وضعیت
userClubFeature.IsActive = request.IsActive;
userClubFeature.LastModified = DateTime.UtcNow;
await _context.SaveChangesAsync(cancellationToken);
return new ToggleUserClubFeatureResponse
{
Success = true,
Message = request.IsActive ? "ویژگی با موفقیت فعال شد" : "ویژگی با موفقیت غیرفعال شد",
UserClubFeatureId = userClubFeature.Id,
IsActive = userClubFeature.IsActive
};
}
}

View File

@@ -0,0 +1,27 @@
namespace CMSMicroservice.Application.ClubFeatureCQ.Commands.ToggleUserClubFeature;
/// <summary>
/// پاسخ کامند فعال/غیرفعال کردن ویژگی باشگاه
/// </summary>
public class ToggleUserClubFeatureResponse
{
/// <summary>
/// موفقیت عملیات
/// </summary>
public bool Success { get; set; }
/// <summary>
/// پیام
/// </summary>
public string Message { get; set; }
/// <summary>
/// شناسه رکورد UserClubFeature
/// </summary>
public long? UserClubFeatureId { get; set; }
/// <summary>
/// وضعیت جدید
/// </summary>
public bool? IsActive { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MediatR;
namespace CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
/// <summary>
/// دریافت لیست ویژگی‌های باشگاه اختصاص یافته به کاربر
/// </summary>
public record GetUserClubFeaturesQuery : IRequest<List<UserClubFeatureDto>>
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; init; }
}

View File

@@ -0,0 +1,40 @@
using CMSMicroservice.Application.Common.Interfaces;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
/// <summary>
/// هندلر برای دریافت لیست ویژگی‌های باشگاه یک کاربر
/// </summary>
public class GetUserClubFeaturesQueryHandler : IRequestHandler<GetUserClubFeaturesQuery, List<UserClubFeatureDto>>
{
private readonly IApplicationDbContext _context;
public GetUserClubFeaturesQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<List<UserClubFeatureDto>> Handle(GetUserClubFeaturesQuery request, CancellationToken cancellationToken)
{
var userClubFeatures = await _context.UserClubFeatures
.Include(ucf => ucf.ClubFeature)
.Where(ucf => ucf.UserId == request.UserId && !ucf.IsDeleted)
.Select(ucf => new UserClubFeatureDto
{
Id = ucf.Id,
UserId = ucf.UserId,
ClubMembershipId = ucf.ClubMembershipId,
ClubFeatureId = ucf.ClubFeatureId,
FeatureTitle = ucf.ClubFeature.Title,
FeatureDescription = ucf.ClubFeature.Description,
IsActive = ucf.IsActive,
GrantedAt = ucf.GrantedAt,
Notes = ucf.Notes
})
.ToListAsync(cancellationToken);
return userClubFeatures;
}
}

View File

@@ -0,0 +1,52 @@
namespace CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
/// <summary>
/// DTO برای ویژگی‌های باشگاه کاربر
/// </summary>
public class UserClubFeatureDto
{
/// <summary>
/// شناسه رکورد UserClubFeature
/// </summary>
public long Id { get; set; }
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; set; }
/// <summary>
/// شناسه عضویت باشگاه
/// </summary>
public long ClubMembershipId { get; set; }
/// <summary>
/// شناسه ویژگی باشگاه
/// </summary>
public long ClubFeatureId { get; set; }
/// <summary>
/// عنوان ویژگی
/// </summary>
public string FeatureTitle { get; set; }
/// <summary>
/// توضیحات ویژگی
/// </summary>
public string? FeatureDescription { get; set; }
/// <summary>
/// وضعیت فعال/غیرفعال ویژگی برای این کاربر
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// تاریخ اعطای ویژگی
/// </summary>
public DateTime GrantedAt { get; set; }
/// <summary>
/// یادداشت
/// </summary>
public string? Notes { get; set; }
}

View File

@@ -3,11 +3,13 @@ using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models; using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Entities; using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Entities.Club; using CMSMicroservice.Domain.Entities.Club;
using CMSMicroservice.Domain.Entities.Commission;
using CMSMicroservice.Domain.Entities.History; using CMSMicroservice.Domain.Entities.History;
using CMSMicroservice.Domain.Enums; using CMSMicroservice.Domain.Enums;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Globalization;
namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership; namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
@@ -135,9 +137,15 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
.FirstOrDefaultAsync( .FirstOrDefaultAsync(
c => c.Key == "Club.MembershipGiftValue" && c.IsActive, c => c.Key == "Club.MembershipGiftValue" && c.IsActive,
cancellationToken cancellationToken
);
var activationFeeConfig = await _context.SystemConfigurations
.FirstOrDefaultAsync(
c => c.Key == "Club.ActivationFee" && c.IsActive,
cancellationToken
); );
long giftValue = 25_200_000; // مقدار پیش‌فرض long giftValue = 28_000_000; // مقدار پیش‌فرض
if (giftValueConfig != null && long.TryParse(giftValueConfig.Value, out var configValue)) if (giftValueConfig != null && long.TryParse(giftValueConfig.Value, out var configValue))
{ {
giftValue = configValue; giftValue = configValue;
@@ -152,11 +160,27 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
"Club.MembershipGiftValue not found in configuration, using default: {GiftValue}", "Club.MembershipGiftValue not found in configuration, using default: {GiftValue}",
giftValue giftValue
); );
}
long activationFeeValue = 25_200_000; // مقدار پیش‌فرض
if (activationFeeConfig != null && long.TryParse(activationFeeConfig.Value, out var activationFeeConfigValue))
{
activationFeeValue = activationFeeConfigValue;
_logger.LogInformation(
"Using Club.ActivationFee from configuration: {activationFeeValue}",
activationFeeValue
);
}
else
{
_logger.LogWarning(
"Club.ActivationFee not found in configuration, using default: {activationFeeValue}",
activationFeeValue
);
} }
ClubMembership entity; ClubMembership entity;
bool isNewMembership = existingMembership == null; bool isNewMembership = existingMembership == null;
var activationDate = DateTime.UtcNow; var activationDate = DateTime.Now;
if (isNewMembership) if (isNewMembership)
{ {
@@ -166,7 +190,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
UserId = user.Id, UserId = user.Id,
IsActive = true, IsActive = true,
ActivatedAt = activationDate, ActivatedAt = activationDate,
InitialContribution = 56_000_000, InitialContribution =activationFeeValue,
GiftValue = giftValue, // مقدار از تنظیمات GiftValue = giftValue, // مقدار از تنظیمات
TotalEarned = 0, TotalEarned = 0,
PurchaseMethod = user.PackagePurchaseMethod PurchaseMethod = user.PackagePurchaseMethod
@@ -225,6 +249,83 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
_context.ClubMembershipHistories.Add(history); _context.ClubMembershipHistories.Add(history);
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
// ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری
var currentWeekNumber = GetCurrentWeekNumber();
var weeklyPool = await _context.WeeklyCommissionPools
.FirstOrDefaultAsync(p => p.WeekNumber == currentWeekNumber, cancellationToken);
if (weeklyPool == null)
{
// ایجاد Pool جدید برای این هفته
weeklyPool = new WeeklyCommissionPool
{
WeekNumber = currentWeekNumber,
TotalPoolAmount = activationFeeValue, // مبلغ هدیه به Pool اضافه میشه
TotalBalances = 0, // در CalculateWeeklyBalances محاسبه میشه
ValuePerBalance = 0, // در CalculateWeeklyCommissionPool محاسبه میشه
IsCalculated = false,
CalculatedAt = null
};
await _context.WeeklyCommissionPools.AddAsync(weeklyPool, cancellationToken);
_logger.LogInformation(
"Created new WeeklyCommissionPool for {WeekNumber} with initial amount: {Amount}",
currentWeekNumber,
giftValue
);
}
else
{
// اضافه کردن به Pool موجود
weeklyPool.TotalPoolAmount += activationFeeValue;
_context.WeeklyCommissionPools.Update(weeklyPool);
_logger.LogInformation(
"Added {Amount} to existing WeeklyCommissionPool for {WeekNumber}. New total: {NewTotal}",
activationFeeValue,
currentWeekNumber,
weeklyPool.TotalPoolAmount
);
}
await _context.SaveChangesAsync(cancellationToken);
// 9. اضافه کردن ویژگی‌های باشگاه برای کاربر (فقط برای عضویت جدید)
if (isNewMembership)
{
var clubFeatures = await _context.ClubFeatures
.Where(f => !f.IsDeleted && new long[] { 1, 2, 3, 4 }.Contains(f.Id))
.ToListAsync(cancellationToken);
if (clubFeatures.Any())
{
var userClubFeatures = clubFeatures.Select(feature => new UserClubFeature
{
UserId = user.Id,
ClubMembershipId = entity.Id,
ClubFeatureId = feature.Id,
GrantedAt = activationDate,
IsActive = true,
Notes = "اعطا شده به‌طور خودکار هنگام فعالسازی"
}).ToList(); _context.UserClubFeatures.AddRange(userClubFeatures);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Granted {Count} club features to UserId {UserId}",
clubFeatures.Count,
user.Id
);
}
else
{
_logger.LogWarning(
"No club features found to grant to UserId {UserId}",
user.Id
);
}
}
_logger.LogInformation( _logger.LogInformation(
"Club membership activated successfully. UserId: {UserId}, MembershipId: {MembershipId}", "Club membership activated successfully. UserId: {UserId}, MembershipId: {MembershipId}",
user.Id, user.Id,
@@ -243,4 +344,15 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
throw; throw;
} }
} }
/// <summary>
/// دریافت شماره هفته جاری (مثال: "2025-W50")
/// </summary>
private string GetCurrentWeekNumber()
{
var now = DateTime.Now;
var calendar = CultureInfo.CurrentCulture.Calendar;
var weekOfYear = calendar.GetWeekOfYear(now, CalendarWeekRule.FirstDay, DayOfWeek.Saturday);
return $"{now.Year}-W{weekOfYear:D2}";
}
} }

View File

@@ -54,7 +54,7 @@ public class AssignClubFeatureCommandHandler : IRequestHandler<AssignClubFeature
UserId = request.UserId, UserId = request.UserId,
ClubMembershipId = membership.Id, ClubMembershipId = membership.Id,
ClubFeatureId = request.FeatureId, ClubFeatureId = request.FeatureId,
GrantedAt = request.GrantedAt ?? DateTime.UtcNow, GrantedAt = request.GrantedAt ?? DateTime.Now,
Notes = request.Notes Notes = request.Notes
}; };

View File

@@ -11,7 +11,7 @@ public class GetClubStatisticsQueryHandler : IRequestHandler<GetClubStatisticsQu
public async Task<GetClubStatisticsResponseDto> Handle(GetClubStatisticsQuery request, CancellationToken cancellationToken) public async Task<GetClubStatisticsResponseDto> Handle(GetClubStatisticsQuery request, CancellationToken cancellationToken)
{ {
var now = DateTime.UtcNow; var now = DateTime.Now;
// Basic statistics // Basic statistics
var totalMembers = await _context.ClubMemberships.CountAsync(cancellationToken); var totalMembers = await _context.ClubMemberships.CountAsync(cancellationToken);

View File

@@ -33,10 +33,10 @@ public class ApproveWithdrawalCommandHandler : IRequestHandler<ApproveWithdrawal
// Update status to Withdrawn (approved) // Update status to Withdrawn (approved)
payout.Status = CommissionPayoutStatus.Withdrawn; payout.Status = CommissionPayoutStatus.Withdrawn;
payout.WithdrawnAt = DateTime.UtcNow; payout.WithdrawnAt = DateTime.Now;
payout.ProcessedBy = _currentUser.GetPerformedBy(); payout.ProcessedBy = _currentUser.GetPerformedBy();
payout.ProcessedAt = DateTime.UtcNow; payout.ProcessedAt = DateTime.Now;
payout.LastModified = DateTime.UtcNow; payout.LastModified = DateTime.Now;
// TODO: Add PayoutHistory record // TODO: Add PayoutHistory record
// var history = new CommissionPayoutHistory // var history = new CommissionPayoutHistory
@@ -51,7 +51,7 @@ public class ApproveWithdrawalCommandHandler : IRequestHandler<ApproveWithdrawal
// Action = (int)CommissionPayoutAction.Approved, // Action = (int)CommissionPayoutAction.Approved,
// PerformedBy = "Admin", // TODO: Get from authenticated user // PerformedBy = "Admin", // TODO: Get from authenticated user
// Reason = request.Notes, // Reason = request.Notes,
// Created = DateTime.UtcNow // Created = DateTime.Now
// }; // };
// _context.CommissionPayoutHistories.Add(history); // _context.CommissionPayoutHistories.Add(history);

View File

@@ -28,9 +28,17 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
} }
// دریافت کاربران فعال در شبکه // دریافت همه کاربرانی که عضو فعال باشگاه هستند
// بدون محدودیت زمانی - همه اعضای فعال کلاب باید کمیسیون بگیرند
var activeClubMemberUserIds = await _context.ClubMemberships
.Where(c => c.IsActive)
.Select(c => c.UserId)
.ToHashSetAsync(cancellationToken);
// دریافت کاربران فعال در شبکه که عضو باشگاه هستند
// نکته: شرط NetworkParentId.HasValue نداریم چون ریشه شبکه (اولین نفر) هم باید حساب بشه
var usersInNetwork = await _context.Users var usersInNetwork = await _context.Users
.Where(x => x.NetworkParentId.HasValue) .Where(x => activeClubMemberUserIds.Contains(x.Id))
.Select(x => new { x.Id }) .Select(x => new { x.Id })
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -47,7 +55,7 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
.ToDictionaryAsync(x => x.UserId, cancellationToken); .ToDictionaryAsync(x => x.UserId, cancellationToken);
var balancesList = new List<NetworkWeeklyBalance>(); var balancesList = new List<NetworkWeeklyBalance>();
var calculatedAt = DateTime.UtcNow; var calculatedAt = DateTime.Now;
// خواندن یکباره Configuration ها (بهینه‌سازی - به جای N query) // خواندن یکباره Configuration ها (بهینه‌سازی - به جای N query)
var configs = await _context.SystemConfigurations var configs = await _context.SystemConfigurations
@@ -57,15 +65,15 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
x.Key == "Commission.MaxWeeklyBalancesPerLeg" || x.Key == "Commission.MaxWeeklyBalancesPerLeg" ||
x.Key == "Commission.MaxNetworkLevel")) x.Key == "Commission.MaxNetworkLevel"))
.ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken); .ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken);
var activationFee = long.Parse(configs.GetValueOrDefault("Club.ActivationFee", "25000000")); // var activationFee = long.Parse(configs.GetValueOrDefault("Club.ActivationFee", "25000000"));
var poolPercent = decimal.Parse(configs.GetValueOrDefault("Commission.WeeklyPoolContributionPercent", "20")) / 100m; // var poolPercent = decimal.Parse(configs.GetValueOrDefault("Commission.WeeklyPoolContributionPercent", "20")) / 100m;
// سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل // سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل
var maxBalancesPerLeg = int.Parse(configs.GetValueOrDefault("Commission.MaxWeeklyBalancesPerLeg", "300")); var maxBalancesPerLeg = int.Parse(configs.GetValueOrDefault("Commission.MaxWeeklyBalancesPerLeg", "300"));
// حداکثر عمق شبکه برای شمارش اعضا (15 لول) // حداکثر عمق شبکه برای شمارش اعضا (15 لول)
var maxNetworkLevel = int.Parse(configs.GetValueOrDefault("Commission.MaxNetworkLevel", "15")); var maxNetworkLevel = int.Parse(configs.GetValueOrDefault("Commission.MaxNetworkLevel", "15"));
foreach (var user in usersInNetwork) foreach (var user in usersInNetwork.OrderBy(o=>o.Id))
{ {
// دریافت باقیمانده هفته قبل // دریافت باقیمانده هفته قبل
var leftCarryover = 0; var leftCarryover = 0;
@@ -84,29 +92,34 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
var leftTotal = leftNewMembers + leftCarryover; var leftTotal = leftNewMembers + leftCarryover;
var rightTotal = rightNewMembers + rightCarryover; var rightTotal = rightNewMembers + rightCarryover;
// ✅ اصلاح شده: اعمال سقف روی هر دست جداگانه (نه روی کل) // ✅ مرحله 1: محاسبه تعادل اولیه (قبل از اعمال سقف)
// سقف 300 برای دست چپ + 300 برای دست راست = حداکثر 600 تعادل در هفته // تعادل = کمترین مقدار بین چپ و راست
var cappedLeftTotal = Math.Min(leftTotal, maxBalancesPerLeg); // مثال: چپ=500، راست=600 → تعادل=500
var cappedRightTotal = Math.Min(rightTotal, maxBalancesPerLeg); var totalBalances = Math.Min(leftTotal, rightTotal);
// محاسبه تعادل (کمترین مقدار بعد از اعمال سقف) // ✅ مرحله 2: محاسبه باقیمانده (قبل از سقف)
var totalBalances = Math.Min(cappedLeftTotal, cappedRightTotal); // باقیمانده = اضافه‌ای که یک طرف دارد
// مثال: چپ=500، راست=600، تعادل=500
// → باقی چپ = 500-500 = 0
// → باقی راست = 600-500 = 100 (می‌رود برای هفته بعد)
var leftRemainder = leftTotal - totalBalances;
var rightRemainder = rightTotal - totalBalances;
// محاسبه باقیمانده برای هفته بعد // ✅ مرحله 3: اعمال سقف 300 (برای امتیاز نهایی)
// باقیمانده = مقداری که از سقف هر دست رد شده // از تعادل، فقط 300 از هر طرف حساب می‌شود
// مثال: چپ=350، راست=450، سقف=300 // مثال: تعادل=500 → امتیاز=300
// cappedLeft = MIN(350, 300) = 300 // از چپ: 300 حساب می‌شود، 200 فلش می‌شود
// cappedRight = MIN(450, 300) = 300 // از راست: 300 حساب می‌شود، 200 فلش می‌شود
// totalBalances = MIN(300, 300) = 300 // جمع فلش = 400 (از بین می‌رود)
// leftRemainder = 350 - 300 = 50 (مازاد سقف) var cappedBalances = Math.Min(totalBalances, maxBalancesPerLeg);
// rightRemainder = 450 - 300 = 150 (مازاد سقف)
var leftRemainder = leftTotal - cappedLeftTotal; // ✅ مرحله 4: محاسبه فلش (از هر دو طرف)
var rightRemainder = rightTotal - cappedRightTotal; var flushedPerSide = totalBalances - cappedBalances; // 500-300=200
var totalFlushed = flushedPerSide * 2; // 200×2=400 (از بین می‌رود)
// محاسبه سهم استخر (20% از مجموع فعال‌سازی‌های جدید کل شبکه) // ⚠️ توجه: تعادل زیرمجموعه در این مرحله محاسبه نمیشه
// طبق گفته دکتر: کل افراد جدید در شبکه × هزینه فعال‌سازی × 20% // چون هنوز تمام تعادل‌ها محاسبه نشدن
var totalNewMembers = leftNewMembers + rightNewMembers; // بعد از ذخیره همه تعادل‌ها، در یک حلقه دوم محاسبه خواهد شد
var weeklyPoolContribution = (long)(totalNewMembers * activationFee * poolPercent);
var balance = new NetworkWeeklyBalance var balance = new NetworkWeeklyBalance
{ {
@@ -122,19 +135,26 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
// مجموع // مجموع
LeftLegTotal = leftTotal, LeftLegTotal = leftTotal,
RightLegTotal = rightTotal, RightLegTotal = rightTotal,
TotalBalances = totalBalances, // تعادل واقعی بعد از اعمال سقف روی هر دست TotalBalances = cappedBalances, // امتیاز نهایی بعد از اعمال سقف 300
// باقیمانده برای هفته بعد (مازاد سقف هر دست) // باقیمانده برای هفته بعد (اضافه‌ای که یک طرف دارد)
LeftLegRemainder = leftRemainder, LeftLegRemainder = leftRemainder,
RightLegRemainder = rightRemainder, RightLegRemainder = rightRemainder,
// فلش (از دست رفته)
FlushedPerSide = flushedPerSide,
TotalFlushed = totalFlushed,
// تعادل زیرمجموعه - فعلاً 0 (بعد از ذخیره همه تعادل‌ها محاسبه میشه)
SubordinateBalances = 0,
// فیلدهای قدیمی (deprecated) - برای سازگاری با کدهای قبلی // فیلدهای قدیمی (deprecated) - برای سازگاری با کدهای قبلی
#pragma warning disable CS0618 #pragma warning disable CS0618
LeftLegBalances = leftTotal, LeftLegBalances = leftTotal,
RightLegBalances = rightTotal, RightLegBalances = rightTotal,
#pragma warning restore CS0618 #pragma warning restore CS0618
WeeklyPoolContribution = weeklyPoolContribution, WeeklyPoolContribution = 0, // Pool در مرحله بعد محاسبه میشه
CalculatedAt = calculatedAt, CalculatedAt = calculatedAt,
IsExpired = false IsExpired = false
}; };
@@ -145,6 +165,26 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, cancellationToken); await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, cancellationToken);
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
// ⭐ مرحله 2: محاسبه تعادل زیرمجموعه برای هر کاربر (تا 15 لول)
// حالا که همه تعادل‌ها ذخیره شدن، می‌تونیم تعادل زیرمجموعه رو حساب کنیم
var balancesDictionary = balancesList.ToDictionary(x => x.UserId);
foreach (var balance in balancesList)
{
var subordinateBalances = await CalculateSubordinateBalancesAsync(
balance.UserId,
balancesDictionary,
maxNetworkLevel,
cancellationToken
);
balance.SubordinateBalances = subordinateBalances;
}
// ذخیره تعادل‌های زیرمجموعه
_context.NetworkWeeklyBalances.UpdateRange(balancesList);
await _context.SaveChangesAsync(cancellationToken);
return balancesList.Count; return balancesList.Count;
} }
@@ -248,4 +288,64 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
return (weekStart, weekEnd); return (weekStart, weekEnd);
} }
/// <summary>
/// محاسبه مجموع تعادل‌های زیرمجموعه یک کاربر تا maxLevel لول پایین‌تر
/// </summary>
private async Task<int> CalculateSubordinateBalancesAsync(
long userId,
Dictionary<long, NetworkWeeklyBalance> allBalances,
int maxLevel,
CancellationToken cancellationToken)
{
// پیدا کردن همه زیرمجموعه‌ها تا maxLevel لول
var subordinates = await GetSubordinatesRecursive(userId, 1, maxLevel, cancellationToken);
// جمع تعادل‌های آنها
var totalSubordinateBalances = 0;
foreach (var subordinateId in subordinates)
{
if (allBalances.ContainsKey(subordinateId))
{
totalSubordinateBalances += allBalances[subordinateId].TotalBalances;
}
}
return totalSubordinateBalances;
}
/// <summary>
/// پیدا کردن بازگشتی زیرمجموعه‌ها تا maxLevel لول
/// </summary>
private async Task<List<long>> GetSubordinatesRecursive(
long userId,
int currentLevel,
int maxLevel,
CancellationToken cancellationToken)
{
// محدودیت عمق
if (currentLevel > maxLevel)
{
return new List<long>();
}
var result = new List<long>();
// پیدا کردن فرزندان مستقیم
var children = await _context.Users
.Where(x => x.NetworkParentId == userId)
.Select(x => x.Id)
.ToListAsync(cancellationToken);
result.AddRange(children);
// بازگشت برای هر فرزند
foreach (var childId in children)
{
var grandChildren = await GetSubordinatesRecursive(childId, currentLevel + 1, maxLevel, cancellationToken);
result.AddRange(grandChildren);
}
return result;
}
} }

View File

@@ -11,11 +11,19 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
public async Task<long> Handle(CalculateWeeklyCommissionPoolCommand request, CancellationToken cancellationToken) public async Task<long> Handle(CalculateWeeklyCommissionPoolCommand request, CancellationToken cancellationToken)
{ {
// بررسی وجود استخر قبلی // بررسی وجود استخر
var existingPool = await _context.WeeklyCommissionPools var existingPool = await _context.WeeklyCommissionPools
.FirstOrDefaultAsync(x => x.WeekNumber == request.WeekNumber, cancellationToken); .FirstOrDefaultAsync(x => x.WeekNumber == request.WeekNumber, cancellationToken);
if (existingPool != null && existingPool.IsCalculated && !request.ForceRecalculate) if (existingPool == null)
{
throw new InvalidOperationException(
$"Pool هفته {request.WeekNumber} وجود ندارد. " +
"Pool باید در هنگام فعالسازی باشگاه مشتریان ایجاد شده باشد"
);
}
if (existingPool.IsCalculated && !request.ForceRecalculate)
{ {
throw new InvalidOperationException($"استخر کمیسیون هفته {request.WeekNumber} قبلاً محاسبه شده است"); throw new InvalidOperationException($"استخر کمیسیون هفته {request.WeekNumber} قبلاً محاسبه شده است");
} }
@@ -30,48 +38,122 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
throw new InvalidOperationException($"تعادل‌های هفته {request.WeekNumber} هنوز محاسبه نشده است. ابتدا CalculateWeeklyBalances را اجرا کنید"); throw new InvalidOperationException($"تعادل‌های هفته {request.WeekNumber} هنوز محاسبه نشده است. ابتدا CalculateWeeklyBalances را اجرا کنید");
} }
// محاسبه مجموع مشارکت‌ها در استخر // ⭐ Pool از قبل پُر شده (توسط ActivateClubMembership)
var totalPoolAmount = weeklyBalances.Sum(x => x.WeeklyPoolContribution); var totalPoolAmount = existingPool.TotalPoolAmount;
// محاسبه مجموع Balances // محاسبه مجموع تعادل‌های کل شبکه
var totalBalances = weeklyBalances.Sum(x => x.TotalBalances); // نکته: SubordinateBalances اضافه نمی‌کنیم چون وقتی همه TotalBalances رو جمع می‌زنیم،
// خودش شامل بالانس‌های زیرمجموعه‌ها هم هست (تکراری نشه)
var totalBalancesInNetwork = weeklyBalances.Sum(x => x.TotalBalances);
// محاسبه ارزش هر Balance (تقسیم صحیح برای ریال) // محاسبه ارزش هر امتیاز
long valuePerBalance = 0; long valuePerBalance = 0;
if (totalBalances > 0) if (totalBalancesInNetwork > 0)
{ {
valuePerBalance = totalPoolAmount / totalBalances; valuePerBalance = totalPoolAmount / totalBalancesInNetwork;
} }
if (existingPool != null) // به‌روزرسانی Pool
{ existingPool.TotalBalances = totalBalancesInNetwork;
// به‌روزرسانی existingPool.ValuePerBalance = valuePerBalance;
existingPool.TotalPoolAmount = totalPoolAmount; existingPool.IsCalculated = true;
existingPool.TotalBalances = totalBalances; existingPool.CalculatedAt = DateTime.Now;
existingPool.ValuePerBalance = valuePerBalance;
existingPool.IsCalculated = true;
existingPool.CalculatedAt = DateTime.UtcNow;
_context.WeeklyCommissionPools.Update(existingPool); _context.WeeklyCommissionPools.Update(existingPool);
} await _context.SaveChangesAsync(cancellationToken);
else
// حذف پرداخت‌های قبلی در صورت ForceRecalculate
if (request.ForceRecalculate)
{ {
// ایجاد جدید var oldPayouts = await _context.UserCommissionPayouts
var pool = new WeeklyCommissionPool .Where(p => p.WeekNumber == request.WeekNumber)
.ToListAsync(cancellationToken);
if (oldPayouts.Any())
{ {
var oldPayoutIds = oldPayouts.Select(p => p.Id).ToList();
// ⭐ اول باید تاریخچه‌ها حذف بشن (به خاطر FK constraint)
var oldHistories = await _context.CommissionPayoutHistories
.Where(h => oldPayoutIds.Contains(h.UserCommissionPayoutId))
.ToListAsync(cancellationToken);
if (oldHistories.Any())
{
_context.CommissionPayoutHistories.RemoveRange(oldHistories);
}
// بعد پرداخت‌ها حذف می‌شن
_context.UserCommissionPayouts.RemoveRange(oldPayouts);
await _context.SaveChangesAsync(cancellationToken);
}
}
// ⭐ ثبت پرداخت برای کاربرانی که تعادل دارند
// تعادل شخصی + زیرمجموعه قبلاً در CalculateWeeklyBalances محاسبه شده
var payouts = new List<UserCommissionPayout>();
foreach (var balance in weeklyBalances)
{
// فقط تعادل شخصی (SubordinateBalances اضافه نمی‌شه چون در SUM کل شبکه خودش حساب میشه)
var userBalance = balance.TotalBalances;
// اگر تعادل صفر است، نیازی به ثبت نیست
if (userBalance <= 0)
{
continue;
}
// محاسبه مبلغ کمیسیون
var totalAmount = (long)(userBalance * valuePerBalance);
var payout = new UserCommissionPayout
{
UserId = balance.UserId,
WeekNumber = request.WeekNumber, WeekNumber = request.WeekNumber,
TotalPoolAmount = totalPoolAmount, WeeklyPoolId = existingPool.Id,
TotalBalances = totalBalances, BalancesEarned = userBalance,
ValuePerBalance = valuePerBalance, ValuePerBalance = valuePerBalance,
IsCalculated = true, TotalAmount = totalAmount,
CalculatedAt = DateTime.UtcNow Status = CommissionPayoutStatus.Pending,
PaidAt = null,
WithdrawalMethod = null,
IbanNumber = null,
WithdrawnAt = null
}; };
await _context.WeeklyCommissionPools.AddAsync(pool, cancellationToken); payouts.Add(payout);
existingPool = pool;
} }
await _context.SaveChangesAsync(cancellationToken); if (payouts.Any())
{
await _context.UserCommissionPayouts.AddRangeAsync(payouts, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
// ثبت تاریخچه برای هر پرداخت
var historyList = new List<CommissionPayoutHistory>();
foreach (var payout in payouts)
{
var history = new CommissionPayoutHistory
{
UserCommissionPayoutId = payout.Id,
UserId = payout.UserId,
WeekNumber = request.WeekNumber,
AmountBefore = 0,
AmountAfter = payout.TotalAmount,
OldStatus = default(CommissionPayoutStatus),
NewStatus = CommissionPayoutStatus.Pending,
Action = CommissionPayoutAction.Created,
PerformedBy = "System",
Reason = "پردازش خودکار کمیسیون هفتگی"
};
historyList.Add(history);
}
await _context.CommissionPayoutHistories.AddRangeAsync(historyList, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
return existingPool.Id; return existingPool.Id;
} }

View File

@@ -39,7 +39,7 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
} }
var oldStatus = payout.Status; var oldStatus = payout.Status;
var now = DateTime.UtcNow; var now = DateTime.Now;
if (request.IsApproved) if (request.IsApproved)
{ {

View File

@@ -34,9 +34,9 @@ public class RejectWithdrawalCommandHandler : IRequestHandler<RejectWithdrawalCo
// Update status to Cancelled (rejected) // Update status to Cancelled (rejected)
payout.Status = CommissionPayoutStatus.Cancelled; payout.Status = CommissionPayoutStatus.Cancelled;
payout.ProcessedBy = _currentUser.GetPerformedBy(); payout.ProcessedBy = _currentUser.GetPerformedBy();
payout.ProcessedAt = DateTime.UtcNow; payout.ProcessedAt = DateTime.Now;
payout.RejectionReason = request.Reason; payout.RejectionReason = request.Reason;
payout.LastModified = DateTime.UtcNow; payout.LastModified = DateTime.Now;
// TODO: Add PayoutHistory record with rejection reason // TODO: Add PayoutHistory record with rejection reason
// var history = new CommissionPayoutHistory // var history = new CommissionPayoutHistory
@@ -51,7 +51,7 @@ public class RejectWithdrawalCommandHandler : IRequestHandler<RejectWithdrawalCo
// Action = (int)CommissionPayoutAction.Rejected, // Action = (int)CommissionPayoutAction.Rejected,
// PerformedBy = "Admin", // TODO: Get from authenticated user // PerformedBy = "Admin", // TODO: Get from authenticated user
// Reason = request.Reason, // Reason = request.Reason,
// Created = DateTime.UtcNow // Created = DateTime.Now
// }; // };
// _context.CommissionPayoutHistories.Add(history); // _context.CommissionPayoutHistories.Add(history);

View File

@@ -13,17 +13,12 @@ public record TriggerWeeklyCalculationCommand : IRequest<TriggerWeeklyCalculatio
public bool ForceRecalculate { get; init; } public bool ForceRecalculate { get; init; }
/// <summary> /// <summary>
/// Skip balance calculation /// Skip balance calculation (Step 1)
/// </summary> /// </summary>
public bool SkipBalances { get; init; } public bool SkipBalances { get; init; }
/// <summary> /// <summary>
/// Skip pool calculation /// Skip pool calculation and payout processing (Step 2)
/// </summary>
public bool SkipPool { get; init; }
/// <summary>
/// Skip payout processing
/// </summary> /// </summary>
public bool SkipPayouts { get; init; } public bool SkipPayouts { get; init; }
} }

View File

@@ -1,7 +1,6 @@
using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances; using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool; using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
using CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
namespace CMSMicroservice.Application.CommissionCQ.Commands.TriggerWeeklyCalculation; namespace CMSMicroservice.Application.CommissionCQ.Commands.TriggerWeeklyCalculation;
@@ -23,7 +22,7 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var executionId = Guid.NewGuid().ToString(); var executionId = Guid.NewGuid().ToString();
var startedAt = DateTime.UtcNow; var startedAt = DateTime.Now;
try try
{ {
@@ -41,7 +40,7 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
var steps = new List<string>(); var steps = new List<string>();
// Step 1: Calculate Weekly Balances // Step 1: Calculate Weekly Balances (تا 15 لول)
if (!request.SkipBalances) if (!request.SkipBalances)
{ {
await _mediator.Send(new CalculateWeeklyBalancesCommand await _mediator.Send(new CalculateWeeklyBalancesCommand
@@ -52,25 +51,15 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
steps.Add("محاسبه امتیازات هفتگی"); steps.Add("محاسبه امتیازات هفتگی");
} }
// Step 2: Calculate Weekly Commission Pool // Step 2: Calculate Pool & Process Payouts (محاسبه استخر + پرداخت کاربران)
if (!request.SkipPool) if (!request.SkipPayouts)
{ {
await _mediator.Send(new CalculateWeeklyCommissionPoolCommand await _mediator.Send(new CalculateWeeklyCommissionPoolCommand
{
WeekNumber = request.WeekNumber
}, cancellationToken);
steps.Add("محاسبه استخر کمیسیون");
}
// Step 3: Process User Payouts
if (!request.SkipPayouts)
{
await _mediator.Send(new ProcessUserPayoutsCommand
{ {
WeekNumber = request.WeekNumber, WeekNumber = request.WeekNumber,
ForceReprocess = request.ForceRecalculate ForceRecalculate = request.ForceRecalculate
}, cancellationToken); }, cancellationToken);
steps.Add("پردازش پرداخت‌های کاربران"); steps.Add("محاسبه استخر و پرداخت کاربران");
} }
return new TriggerWeeklyCalculationResponseDto return new TriggerWeeklyCalculationResponseDto

View File

@@ -0,0 +1,13 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
/// <summary>
/// دریافت لیست هفته‌های قابل انتخاب برای محاسبه کمیسیون
/// </summary>
public class GetAvailableWeeksQuery : IRequest<GetAvailableWeeksResponseDto>
{
/// <summary>تعداد هفته‌های آینده برای نمایش (پیش‌فرض: 4)</summary>
public int FutureWeeksCount { get; init; } = 4;
/// <summary>تعداد هفته‌های گذشته برای نمایش (پیش‌فرض: 12)</summary>
public int PastWeeksCount { get; init; } = 12;
}

View File

@@ -0,0 +1,132 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Enums;
using Microsoft.EntityFrameworkCore;
using System.Globalization;
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
public class GetAvailableWeeksQueryHandler : IRequestHandler<GetAvailableWeeksQuery, GetAvailableWeeksResponseDto>
{
private readonly IApplicationDbContext _context;
public GetAvailableWeeksQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetAvailableWeeksResponseDto> Handle(
GetAvailableWeeksQuery request,
CancellationToken cancellationToken)
{
var currentDate = DateTime.Now;
var currentWeekNumber = GetWeekNumber(currentDate);
// دریافت هفته‌های محاسبه شده از دیتابیس
var calculatedPools = await _context.WeeklyCommissionPools
.Where(p => p.IsCalculated)
.OrderByDescending(p => p.WeekNumber)
.Take(request.PastWeeksCount)
.ToListAsync(cancellationToken);
// دریافت لاگ‌های اجرا
var executionLogs = await _context.WorkerExecutionLogs
.Where(log => log.Status == WorkerExecutionStatus.Success ||
log.Status == WorkerExecutionStatus.Failed)
.GroupBy(log => log.WeekNumber)
.Select(g => new
{
WeekNumber = g.Key,
LastLog = g.OrderByDescending(l => l.StartedAt).First()
})
.ToDictionaryAsync(x => x.WeekNumber, x => x.LastLog, cancellationToken);
var allWeeks = new List<WeekInfoDto>();
// هفته جاری
var currentWeekInfo = CreateWeekInfo(currentDate, currentWeekNumber, calculatedPools, executionLogs);
// هفته‌های گذشته (12 هفته)
var pastWeeks = new List<WeekInfoDto>();
for (int i = 1; i <= request.PastWeeksCount; i++)
{
var pastDate = currentDate.AddDays(-7 * i);
var weekNumber = GetWeekNumber(pastDate);
pastWeeks.Add(CreateWeekInfo(pastDate, weekNumber, calculatedPools, executionLogs));
}
// هفته‌های آینده (4 هفته)
var futureWeeks = new List<WeekInfoDto>();
for (int i = 1; i <= request.FutureWeeksCount; i++)
{
var futureDate = currentDate.AddDays(7 * i);
var weekNumber = GetWeekNumber(futureDate);
futureWeeks.Add(CreateWeekInfo(futureDate, weekNumber, calculatedPools, executionLogs));
}
// تفکیک به calculated و pending
var calculatedWeeks = pastWeeks.Where(w => w.IsCalculated).ToList();
var pendingWeeks = pastWeeks.Where(w => !w.IsCalculated).ToList();
return new GetAvailableWeeksResponseDto
{
CurrentWeek = currentWeekInfo,
CalculatedWeeks = calculatedWeeks,
PendingWeeks = pendingWeeks,
FutureWeeks = futureWeeks
};
}
private WeekInfoDto CreateWeekInfo(
DateTime date,
string weekNumber,
List<Domain.Entities.Commission.WeeklyCommissionPool> calculatedPools,
Dictionary<string, Domain.Entities.Commission.WorkerExecutionLog> executionLogs)
{
var (startDate, endDate) = GetWeekRange(date);
var pool = calculatedPools.FirstOrDefault(p => p.WeekNumber == weekNumber);
var log = executionLogs.GetValueOrDefault(weekNumber);
var isCalculated = pool != null && pool.IsCalculated;
var displayText = $"{weekNumber} ({startDate:yyyy/MM/dd} - {endDate:yyyy/MM/dd})";
if (isCalculated)
{
displayText += " ✅ محاسبه شده";
}
return new WeekInfoDto
{
WeekNumber = weekNumber,
StartDate = startDate,
EndDate = endDate,
IsCalculated = isCalculated,
CalculatedAt = pool?.CalculatedAt,
LastExecutionStatus = log?.Status.ToString(),
TotalPoolAmount = pool?.TotalPoolAmount,
EligibleUsersCount = pool?.UserCommissionPayouts?.Count ?? 0,
DisplayText = displayText
};
}
private static string GetWeekNumber(DateTime date)
{
var calendar = CultureInfo.InvariantCulture.Calendar;
var weekOfYear = calendar.GetWeekOfYear(
date,
CalendarWeekRule.FirstFourDayWeek,
DayOfWeek.Monday);
return $"{date.Year}-W{weekOfYear:D2}";
}
private static (DateTime startDate, DateTime endDate) GetWeekRange(DateTime date)
{
var dayOfWeek = (int)date.DayOfWeek;
var daysToMonday = dayOfWeek == 0 ? 6 : dayOfWeek - 1; // اگر یکشنبه باشد، 6 روز عقب برو
var startDate = date.Date.AddDays(-daysToMonday);
var endDate = startDate.AddDays(6);
return (startDate, endDate);
}
}

View File

@@ -0,0 +1,46 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
public class GetAvailableWeeksResponseDto
{
/// <summary>هفته جاری</summary>
public required WeekInfoDto CurrentWeek { get; init; }
/// <summary>هفته‌های محاسبه شده (از جدیدترین به قدیمی‌ترین)</summary>
public required List<WeekInfoDto> CalculatedWeeks { get; init; }
/// <summary>هفته‌های محاسبه نشده (از جدیدترین به قدیمی‌ترین)</summary>
public required List<WeekInfoDto> PendingWeeks { get; init; }
/// <summary>هفته‌های آینده قابل انتخاب</summary>
public required List<WeekInfoDto> FutureWeeks { get; init; }
}
public class WeekInfoDto
{
/// <summary>شماره هفته (YYYY-Www)</summary>
public required string WeekNumber { get; init; }
/// <summary>تاریخ شروع هفته</summary>
public required DateTime StartDate { get; init; }
/// <summary>تاریخ پایان هفته</summary>
public required DateTime EndDate { get; init; }
/// <summary>آیا محاسبه شده؟</summary>
public bool IsCalculated { get; init; }
/// <summary>تاریخ محاسبه (اگر محاسبه شده باشد)</summary>
public DateTime? CalculatedAt { get; init; }
/// <summary>وضعیت اجرای آخرین محاسبه</summary>
public string? LastExecutionStatus { get; init; }
/// <summary>مبلغ کل استخر کمیسیون (اگر محاسبه شده باشد)</summary>
public long? TotalPoolAmount { get; init; }
/// <summary>تعداد کاربران واجد شرایط</summary>
public int? EligibleUsersCount { get; init; }
/// <summary>نمایش فارسی (برای UI)</summary>
public required string DisplayText { get; init; }
}

View File

@@ -19,7 +19,7 @@ public class GetWithdrawalReportsQueryHandler : IRequestHandler<GetWithdrawalRep
public async Task<WithdrawalReportsDto> Handle(GetWithdrawalReportsQuery request, CancellationToken cancellationToken) public async Task<WithdrawalReportsDto> Handle(GetWithdrawalReportsQuery request, CancellationToken cancellationToken)
{ {
// تعیین بازه زمانی پیش‌فرض (30 روز گذشته) // تعیین بازه زمانی پیش‌فرض (30 روز گذشته)
var endDate = request.EndDate ?? DateTime.UtcNow; var endDate = request.EndDate ?? DateTime.Now;
var startDate = request.StartDate ?? endDate.AddDays(-30); var startDate = request.StartDate ?? endDate.AddDays(-30);
// Query پایه // Query پایه

View File

@@ -27,8 +27,8 @@ public class GetWorkerStatusQueryHandler : IRequestHandler<GetWorkerStatusQuery,
CurrentExecutionId = null, CurrentExecutionId = null,
CurrentWeekNumber = null, CurrentWeekNumber = null,
CurrentStep = "Idle", CurrentStep = "Idle",
LastRunAt = DateTime.UtcNow.AddHours(-24), LastRunAt = DateTime.Now.AddHours(-24),
NextScheduledRun = DateTime.UtcNow.AddDays(7), NextScheduledRun = DateTime.Now.AddDays(7),
TotalExecutions = 48, TotalExecutions = 48,
SuccessfulExecutions = 47, SuccessfulExecutions = 47,
FailedExecutions = 1 FailedExecutions = 1

View File

@@ -48,7 +48,7 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
if (existingContract != null) if (existingContract != null)
{ {
existingContract.LastCheckDate = DateTime.UtcNow; existingContract.LastCheckDate = DateTime.Now;
existingContract.Status = dayaResult.Status; existingContract.Status = dayaResult.Status;
existingContract.ContractNumber = dayaResult.ContractNumber; existingContract.ContractNumber = dayaResult.ContractNumber;
} }
@@ -65,7 +65,7 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
NationalCode = dayaResult.NationalCode, NationalCode = dayaResult.NationalCode,
Status = dayaResult.Status, Status = dayaResult.Status,
ContractNumber = dayaResult.ContractNumber, ContractNumber = dayaResult.ContractNumber,
LastCheckDate = DateTime.UtcNow, LastCheckDate = DateTime.Now,
IsProcessed = false IsProcessed = false
}; };

View File

@@ -36,7 +36,7 @@ public class ProcessDayaLoanApprovalCommandHandler : IRequestHandler<ProcessDaya
Amount = request.WalletAmount + request.LockedWalletAmount + request.DiscountWalletAmount, // 168 میلیون Amount = request.WalletAmount + request.LockedWalletAmount + request.DiscountWalletAmount, // 168 میلیون
Description = $"دریافت اعتبار دایا - قرارداد {request.ContractNumber}", Description = $"دریافت اعتبار دایا - قرارداد {request.ContractNumber}",
PaymentStatus = PaymentStatus.Success, PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow, PaymentDate = DateTime.Now,
RefId = request.ContractNumber, // شماره قرارداد دایا RefId = request.ContractNumber, // شماره قرارداد دایا
Type = TransactionType.DepositExternal1 Type = TransactionType.DepositExternal1
}; };
@@ -114,7 +114,7 @@ public class ProcessDayaLoanApprovalCommandHandler : IRequestHandler<ProcessDaya
// به‌روزرسانی وضعیت کاربر // به‌روزرسانی وضعیت کاربر
user.HasReceivedDayaCredit = true; user.HasReceivedDayaCredit = true;
user.DayaCreditReceivedAt = DateTime.UtcNow; user.DayaCreditReceivedAt = DateTime.Now;
// تنظیم نحوه خرید پکیج به DayaLoan // تنظیم نحوه خرید پکیج به DayaLoan
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan; user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan;
@@ -139,7 +139,7 @@ public class ProcessDayaLoanApprovalCommandHandler : IRequestHandler<ProcessDaya
PackageId = goldenPackage.Id, PackageId = goldenPackage.Id,
Amount = request.WalletAmount, // 56 میلیون Amount = request.WalletAmount, // 56 میلیون
PaymentStatus = PaymentStatus.Success, PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow, PaymentDate = DateTime.Now,
DeliveryStatus = DeliveryStatus.None, DeliveryStatus = DeliveryStatus.None,
UserAddressId = defaultAddress.Id, UserAddressId = defaultAddress.Id,
TransactionId = transaction.Id, TransactionId = transaction.Id,

View File

@@ -46,12 +46,12 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
{ {
// Update transaction // Update transaction
transaction.PaymentStatus = PaymentStatus.Success; transaction.PaymentStatus = PaymentStatus.Success;
transaction.PaymentDate = DateTime.UtcNow; transaction.PaymentDate = DateTime.Now;
transaction.RefId = request.RefId; transaction.RefId = request.RefId;
// Update order // Update order
order.PaymentStatus = PaymentStatus.Success; order.PaymentStatus = PaymentStatus.Success;
order.PaymentDate = DateTime.UtcNow; order.PaymentDate = DateTime.Now;
order.DeliveryStatus = DeliveryStatus.InTransit; order.DeliveryStatus = DeliveryStatus.InTransit;
// Deduct discount balance from user wallet // Deduct discount balance from user wallet

View File

@@ -47,7 +47,7 @@ public class CreateDiscountCategoryCommandHandler : IRequestHandler<CreateDiscou
ParentCategoryId = request.ParentCategoryId, ParentCategoryId = request.ParentCategoryId,
SortOrder = request.SortOrder, SortOrder = request.SortOrder,
IsActive = request.IsActive, IsActive = request.IsActive,
Created = DateTime.UtcNow Created = DateTime.Now
}; };
_context.DiscountCategories.Add(category); _context.DiscountCategories.Add(category);

View File

@@ -86,7 +86,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
Amount = manualPayment.Amount, Amount = manualPayment.Amount,
Description = $"پرداخت دستی - {manualPayment.Type} - {manualPayment.Description}", Description = $"پرداخت دستی - {manualPayment.Type} - {manualPayment.Description}",
PaymentStatus = PaymentStatus.Success, PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow, PaymentDate = DateTime.Now,
RefId = manualPayment.ReferenceNumber, RefId = manualPayment.ReferenceNumber,
Type = MapToTransactionType(manualPayment.Type) Type = MapToTransactionType(manualPayment.Type)
}; };
@@ -219,7 +219,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
// 7. به‌روزرسانی ManualPayment // 7. به‌روزرسانی ManualPayment
manualPayment.Status = ManualPaymentStatus.Approved; manualPayment.Status = ManualPaymentStatus.Approved;
manualPayment.ApprovedBy = approvedById; manualPayment.ApprovedBy = approvedById;
manualPayment.ApprovedAt = DateTime.UtcNow; manualPayment.ApprovedAt = DateTime.Now;
manualPayment.TransactionId = transaction.Id; manualPayment.TransactionId = transaction.Id;
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);

View File

@@ -71,7 +71,7 @@ public class RejectManualPaymentCommandHandler : IRequestHandler<RejectManualPay
// 4. رد درخواست // 4. رد درخواست
manualPayment.Status = ManualPaymentStatus.Rejected; manualPayment.Status = ManualPaymentStatus.Rejected;
manualPayment.ApprovedBy = rejectedById; manualPayment.ApprovedBy = rejectedById;
manualPayment.ApprovedAt = DateTime.UtcNow; manualPayment.ApprovedAt = DateTime.Now;
manualPayment.RejectionReason = request.RejectionReason; manualPayment.RejectionReason = request.RejectionReason;
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);

View File

@@ -55,7 +55,7 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
} }
// Monthly growth (last 6 months) - using Created date // Monthly growth (last 6 months) - using Created date
var sixMonthsAgo = DateTime.UtcNow.AddMonths(-6); var sixMonthsAgo = DateTime.Now.AddMonths(-6);
var monthlyGrowth = await _context.Users var monthlyGrowth = await _context.Users
.Where(x => x.NetworkParentId != null && x.Created >= sixMonthsAgo) .Where(x => x.NetworkParentId != null && x.Created >= sixMonthsAgo)
.GroupBy(x => new { x.Created.Year, x.Created.Month }) .GroupBy(x => new { x.Created.Year, x.Created.Month })

View File

@@ -125,7 +125,7 @@ public class VerifyGoldenPackagePurchaseCommandHandler : IRequestHandler<VerifyG
Amount = order.Amount, Amount = order.Amount,
Description = $"خرید پکیج طلایی از درگاه - سفارش #{order.Id}", Description = $"خرید پکیج طلایی از درگاه - سفارش #{order.Id}",
PaymentStatus = PaymentStatus.Success, PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow, PaymentDate = DateTime.Now,
RefId = verifyResult.RefId, RefId = verifyResult.RefId,
Type = TransactionType.DepositIpg Type = TransactionType.DepositIpg
}; };
@@ -152,7 +152,7 @@ public class VerifyGoldenPackagePurchaseCommandHandler : IRequestHandler<VerifyG
// 7. به‌روزرسانی سفارش و کاربر // 7. به‌روزرسانی سفارش و کاربر
order.TransactionId = transaction.Id; order.TransactionId = transaction.Id;
order.PaymentStatus = PaymentStatus.Success; order.PaymentStatus = PaymentStatus.Success;
order.PaymentDate = DateTime.UtcNow; order.PaymentDate = DateTime.Now;
order.PaymentMethod = PaymentMethod.IPG; order.PaymentMethod = PaymentMethod.IPG;
order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase; order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;

View File

@@ -116,7 +116,7 @@ public class VerifyPackagePurchaseCommandHandler
Amount = order.Amount, Amount = order.Amount,
Description = $"خرید پکیج از درگاه - سفارش #{order.Id}", Description = $"خرید پکیج از درگاه - سفارش #{order.Id}",
PaymentStatus = PaymentStatus.Success, PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow, PaymentDate = DateTime.Now,
RefId = verifyResult.RefId, RefId = verifyResult.RefId,
Type = TransactionType.DepositIpg Type = TransactionType.DepositIpg
}; };
@@ -157,7 +157,7 @@ public class VerifyPackagePurchaseCommandHandler
// 8. به‌روزرسانی Order // 8. به‌روزرسانی Order
order.TransactionId = transaction.Id; order.TransactionId = transaction.Id;
order.PaymentStatus = PaymentStatus.Success; order.PaymentStatus = PaymentStatus.Success;
order.PaymentDate = DateTime.UtcNow; order.PaymentDate = DateTime.Now;
order.PaymentMethod = PaymentMethod.IPG; order.PaymentMethod = PaymentMethod.IPG;
// 9. تغییر User.PackagePurchaseMethod // 9. تغییر User.PackagePurchaseMethod

View File

@@ -31,7 +31,7 @@ public class ArchiveMessageCommandHandler : IRequestHandler<ArchiveMessageComman
// 3. آرشیو کردن: // 3. آرشیو کردن:
// - message.IsArchived = true // - message.IsArchived = true
// - message.IsActive = false // غیرفعال هم می‌شود // - message.IsActive = false // غیرفعال هم می‌شود
// - message.ArchivedAt = DateTime.UtcNow // - message.ArchivedAt = DateTime.Now
// //
// 4. ذخیره و Log: // 4. ذخیره و Log:
// - await _context.SaveChangesAsync(cancellationToken) // - await _context.SaveChangesAsync(cancellationToken)

View File

@@ -26,7 +26,7 @@ public class CreatePublicMessageCommandValidator : AbstractValidator<CreatePubli
RuleFor(x => x.ExpiresAt) RuleFor(x => x.ExpiresAt)
.NotEmpty().WithMessage("تاریخ پایان الزامی است") .NotEmpty().WithMessage("تاریخ پایان الزامی است")
.GreaterThan(DateTime.UtcNow).WithMessage("تاریخ پایان باید در آینده باشد"); .GreaterThan(DateTime.Now).WithMessage("تاریخ پایان باید در آینده باشد");
RuleFor(x => x.LinkUrl) RuleFor(x => x.LinkUrl)
.MaximumLength(500).WithMessage("لینک نمی‌تواند بیشتر از 500 کاراکتر باشد") .MaximumLength(500).WithMessage("لینک نمی‌تواند بیشتر از 500 کاراکتر باشد")

View File

@@ -33,10 +33,10 @@ public class PublishMessageCommandHandler : IRequestHandler<PublishMessageComman
// //
// 3. فعال‌سازی پیام: // 3. فعال‌سازی پیام:
// - message.IsActive = true // - message.IsActive = true
// - message.PublishedAt = DateTime.UtcNow // - message.PublishedAt = DateTime.Now
// - اگر StartDate خالی است، از الان شروع کن: // - اگر StartDate خالی است، از الان شروع کن:
// if (!message.StartDate.HasValue) // if (!message.StartDate.HasValue)
// message.StartDate = DateTime.UtcNow // message.StartDate = DateTime.Now
// //
// 4. ذخیره و Log: // 4. ذخیره و Log:
// - await _context.SaveChangesAsync(cancellationToken) // - await _context.SaveChangesAsync(cancellationToken)

View File

@@ -21,7 +21,7 @@ public class GetActiveMessagesQueryHandler : IRequestHandler<GetActiveMessagesQu
public async Task<List<PublicMessageDto>> Handle(GetActiveMessagesQuery request, CancellationToken cancellationToken) public async Task<List<PublicMessageDto>> Handle(GetActiveMessagesQuery request, CancellationToken cancellationToken)
{ {
var now = DateTime.UtcNow; var now = DateTime.Now;
var query = _context.PublicMessages var query = _context.PublicMessages
.Where(x => !x.IsDeleted .Where(x => !x.IsDeleted

View File

@@ -22,7 +22,7 @@ public class GetAllMessagesQueryHandler : IRequestHandler<GetAllMessagesQuery, G
public async Task<GetAllMessagesResponseDto> Handle(GetAllMessagesQuery request, CancellationToken cancellationToken) public async Task<GetAllMessagesResponseDto> Handle(GetAllMessagesQuery request, CancellationToken cancellationToken)
{ {
var now = DateTime.UtcNow; var now = DateTime.Now;
// Query پایه // Query پایه
var query = _context.PublicMessages var query = _context.PublicMessages

View File

@@ -43,7 +43,7 @@ public class RefundTransactionCommandHandler : IRequestHandler<RefundTransaction
Amount = -refundAmount, // مبلغ منفی برای استرداد Amount = -refundAmount, // مبلغ منفی برای استرداد
Description = $"استرداد تراکنش {request.TransactionId}: {request.RefundReason}", Description = $"استرداد تراکنش {request.TransactionId}: {request.RefundReason}",
PaymentStatus = PaymentStatus.Success, PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow, PaymentDate = DateTime.Now,
RefId = $"REFUND-{originalTransaction.RefId}", RefId = $"REFUND-{originalTransaction.RefId}",
Type = TransactionType.Buy // یا می‌تونیم یک نوع جدید برای Refund تعریف کنیم Type = TransactionType.Buy // یا می‌تونیم یک نوع جدید برای Refund تعریف کنیم
}; };

View File

@@ -49,7 +49,7 @@ public class ApplyDiscountToOrderCommandHandler : IRequestHandler<ApplyDiscountT
// DiscountAmount = request.DiscountAmount, // DiscountAmount = request.DiscountAmount,
// Reason = request.Reason, // Reason = request.Reason,
// DiscountCode = request.DiscountCode, // DiscountCode = request.DiscountCode,
// AppliedAt = DateTime.UtcNow // AppliedAt = DateTime.Now
// } // }
// - await _context.OrderDiscountLogs.AddAsync(discountLog, cancellationToken) // - await _context.OrderDiscountLogs.AddAsync(discountLog, cancellationToken)
// //

View File

@@ -50,7 +50,7 @@ public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, Can
Amount = -order.Amount, Amount = -order.Amount,
Description = $"بازگشت وجه سفارش {request.OrderId}: {request.CancelReason}", Description = $"بازگشت وجه سفارش {request.OrderId}: {request.CancelReason}",
PaymentStatus = PaymentStatus.Success, PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow, PaymentDate = DateTime.Now,
RefId = $"REFUND-ORDER-{order.Id}", RefId = $"REFUND-ORDER-{order.Id}",
Type = TransactionType.Buy Type = TransactionType.Buy
}; };

View File

@@ -147,7 +147,7 @@ public class
VATAmount = vatAmount, VATAmount = vatAmount,
TotalAmount = totalAmount, TotalAmount = totalAmount,
IsPaid = true, IsPaid = true,
PaidAt = DateTime.UtcNow PaidAt = DateTime.Now
}; };
await _context.OrderVATs.AddAsync(orderVAT, cancellationToken); await _context.OrderVATs.AddAsync(orderVAT, cancellationToken);

View File

@@ -9,7 +9,7 @@ public class GetOrdersByDateRangeQueryValidator : AbstractValidator<GetOrdersByD
.WithMessage("تاریخ شروع باید کوچکتر یا مساوی تاریخ پایان باشد"); .WithMessage("تاریخ شروع باید کوچکتر یا مساوی تاریخ پایان باشد");
RuleFor(x => x.EndDate) RuleFor(x => x.EndDate)
.LessThanOrEqualTo(DateTime.UtcNow.AddDays(1)) .LessThanOrEqualTo(DateTime.Now.AddDays(1))
.WithMessage("تاریخ پایان نمی‌تواند در آینده باشد"); .WithMessage("تاریخ پایان نمی‌تواند در آینده باشد");
RuleFor(x => x.PageIndex) RuleFor(x => x.PageIndex)

View File

@@ -92,7 +92,7 @@ public class VerifyDiscountWalletChargeCommandHandler
Amount = request.Amount, Amount = request.Amount,
Description = $"شارژ کیف پول تخفیفی - کاربر {user.Id}", Description = $"شارژ کیف پول تخفیفی - کاربر {user.Id}",
PaymentStatus = PaymentStatus.Success, PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow, PaymentDate = DateTime.Now,
RefId = verifyResult.RefId, RefId = verifyResult.RefId,
Type = TransactionType.DiscountWalletCharge Type = TransactionType.DiscountWalletCharge
}; };

View File

@@ -40,6 +40,11 @@ public class UserClubFeature : BaseAuditableEntity
/// </summary> /// </summary>
public DateTime GrantedAt { get; set; } public DateTime GrantedAt { get; set; }
/// <summary>
/// وضعیت فعال/غیرفعال بودن ویژگی برای کاربر (قابل مدیریت توسط ادمین)
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary> /// <summary>
/// یادداشت اختیاری /// یادداشت اختیاری
/// </summary> /// </summary>

View File

@@ -65,6 +65,23 @@ public class NetworkWeeklyBalance : BaseAuditableEntity
/// </summary> /// </summary>
public int RightLegRemainder { get; set; } public int RightLegRemainder { get; set; }
/// <summary>
/// مقدار فلش هر طرف (بعد از اعمال Cap): TotalBalances - CappedBalances
/// </summary>
public int FlushedPerSide { get; set; }
/// <summary>
/// مجموع فلش از دو طرف: FlushedPerSide × 2
/// این مقدار از دست می‌رود (نه به هفته بعد، نه به کمیسیون)
/// </summary>
public int TotalFlushed { get; set; }
/// <summary>
/// مجموع تعادل زیرمجموعه این کاربر (تا 15 لول پایین‌تر)
/// این مقدار در CalculateWeeklyBalances محاسبه و ذخیره می‌شود
/// </summary>
public int SubordinateBalances { get; set; }
/// <summary> /// <summary>
/// [DEPRECATED] تعداد تعادل شاخه چپ - استفاده نشود /// [DEPRECATED] تعداد تعادل شاخه چپ - استفاده نشود
/// </summary> /// </summary>

View File

@@ -1,6 +1,7 @@
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances; using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool; using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
using CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts; using CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
using CMSMicroservice.Application.CommissionCQ.Commands.TriggerWeeklyCalculation;
using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities; using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums; using CMSMicroservice.Domain.Enums;
@@ -54,16 +55,30 @@ public class WeeklyCommissionJob
/// <summary> /// <summary>
/// Execute weekly commission calculation with retry logic /// Execute weekly commission calculation with retry logic
/// Called by Hangfire scheduler /// Called by Hangfire scheduler or manually triggered
/// </summary> /// </summary>
public async Task ExecuteAsync(CancellationToken cancellationToken = default) /// <param name="weekNumber">Week number in YYYY-Www format (e.g., 2025-W48). If null, uses previous week.</param>
/// <param name="cancellationToken">Cancellation token</param>
public async Task ExecuteAsync(string? weekNumber = null, CancellationToken cancellationToken = default)
{ {
var executionId = Guid.NewGuid(); var executionId = Guid.NewGuid();
var startTime = DateTime.UtcNow; var startTime = DateTime.Now;
// Calculate for PREVIOUS week (completed week) // Use provided week number or calculate for PREVIOUS week (completed week)
var previousWeek = DateTime.UtcNow.AddDays(-7); string targetWeekNumber;
var previousWeekNumber = GetWeekNumber(previousWeek); if (!string.IsNullOrWhiteSpace(weekNumber))
{
targetWeekNumber = weekNumber;
_logger.LogInformation("📅 Using manually specified week: {WeekNumber}", targetWeekNumber);
}
else
{
var previousWeek = DateTime.Now.AddDays(-7);
targetWeekNumber = GetWeekNumber(previousWeek);
_logger.LogInformation("📅 Using previous week (auto-calculated): {WeekNumber}", targetWeekNumber);
}
var previousWeekNumber = targetWeekNumber;
_logger.LogInformation( _logger.LogInformation(
"🚀 [{ExecutionId}] Starting weekly commission calculation for {WeekNumber}", "🚀 [{ExecutionId}] Starting weekly commission calculation for {WeekNumber}",
@@ -89,7 +104,7 @@ public class WeeklyCommissionJob
}, cancellationToken); }, cancellationToken);
// Update log on success // Update log on success
var completedAt = DateTime.UtcNow; var completedAt = DateTime.Now;
var duration = completedAt - startTime; var duration = completedAt - startTime;
log.Status = WorkerExecutionStatus.Success; log.Status = WorkerExecutionStatus.Success;
@@ -113,7 +128,7 @@ public class WeeklyCommissionJob
catch (Exception ex) catch (Exception ex)
{ {
// Update log on failure // Update log on failure
var completedAt = DateTime.UtcNow; var completedAt = DateTime.Now;
var duration = completedAt - startTime; var duration = completedAt - startTime;
log.Status = WorkerExecutionStatus.Failed; log.Status = WorkerExecutionStatus.Failed;
@@ -165,38 +180,43 @@ public class WeeklyCommissionJob
"📊 [{ExecutionId}] Step 1/3: Calculating weekly balances...", "📊 [{ExecutionId}] Step 1/3: Calculating weekly balances...",
executionId); executionId);
await _mediator.Send(new CalculateWeeklyBalancesCommand await _mediator.Send(new TriggerWeeklyCalculationCommand
{ {
WeekNumber = weekNumber, WeekNumber = weekNumber,
ForceRecalculate = false ForceRecalculate = false
}, cancellationToken); }, cancellationToken);
// await _mediator.Send(new CalculateWeeklyBalancesCommand
// Step 2: Calculate global commission pool // {
_logger.LogInformation( // WeekNumber = weekNumber,
"💰 [{ExecutionId}] Step 2/3: Calculating commission pool...", // ForceRecalculate = false
executionId); // }, cancellationToken);
//
await _mediator.Send(new CalculateWeeklyCommissionPoolCommand // // Step 2: Calculate global commission pool
{ // _logger.LogInformation(
WeekNumber = weekNumber, // "💰 [{ExecutionId}] Step 2/3: Calculating commission pool...",
ForceRecalculate = false // executionId);
}, cancellationToken); //
// await _mediator.Send(new CalculateWeeklyCommissionPoolCommand
// Step 3: Distribute commissions to users // {
_logger.LogInformation( // WeekNumber = weekNumber,
"💸 [{ExecutionId}] Step 3/3: Processing user payouts...", // ForceRecalculate = false
executionId); // }, cancellationToken);
//
await _mediator.Send(new ProcessUserPayoutsCommand // // Step 3: Distribute commissions to users
{ // _logger.LogInformation(
WeekNumber = weekNumber, // "💸 [{ExecutionId}] Step 3/3: Processing user payouts...",
ForceReprocess = false // executionId);
}, cancellationToken); //
// await _mediator.Send(new ProcessUserPayoutsCommand
// {
// WeekNumber = weekNumber,
// ForceReprocess = false
// }, cancellationToken);
transaction.Complete(); transaction.Complete();
_logger.LogInformation( _logger.LogInformation(
"✅ [{ExecutionId}] All 3 steps completed successfully", "✅ [{ExecutionId}] All 2 steps completed successfully",
executionId); executionId);
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -109,7 +109,7 @@ public class WeeklyNetworkCommissionWorker : BackgroundService
private async Task ExecuteWeeklyCalculationAsync(CancellationToken cancellationToken) private async Task ExecuteWeeklyCalculationAsync(CancellationToken cancellationToken)
{ {
var executionId = Guid.NewGuid(); var executionId = Guid.NewGuid();
var startTime = DateTime.UtcNow; var startTime = DateTime.Now;
_logger.LogInformation("=== Starting Weekly Commission Calculation [{ExecutionId}] at {Time} (UTC) ===", _logger.LogInformation("=== Starting Weekly Commission Calculation [{ExecutionId}] at {Time} (UTC) ===",
executionId, startTime); executionId, startTime);
@@ -154,7 +154,7 @@ public class WeeklyNetworkCommissionWorker : BackgroundService
// Update log // Update log
log.Status = WorkerExecutionStatus.SuccessWithWarnings; log.Status = WorkerExecutionStatus.SuccessWithWarnings;
log.CompletedAt = DateTime.UtcNow; log.CompletedAt = DateTime.Now;
log.DurationMs = (long)(log.CompletedAt.Value - log.StartedAt).TotalMilliseconds; log.DurationMs = (long)(log.CompletedAt.Value - log.StartedAt).TotalMilliseconds;
log.Details = "Week already calculated - skipped"; log.Details = "Week already calculated - skipped";
await context.SaveChangesAsync(cancellationToken); await context.SaveChangesAsync(cancellationToken);
@@ -222,7 +222,7 @@ public class WeeklyNetworkCommissionWorker : BackgroundService
// Commit Transaction // Commit Transaction
transaction.Complete(); transaction.Complete();
var completedAt = DateTime.UtcNow; var completedAt = DateTime.Now;
var duration = completedAt - startTime; var duration = completedAt - startTime;
// Update log - Success // Update log - Success
@@ -297,7 +297,7 @@ public class WeeklyNetworkCommissionWorker : BackgroundService
var context = errorScope.ServiceProvider.GetRequiredService<IApplicationDbContext>(); var context = errorScope.ServiceProvider.GetRequiredService<IApplicationDbContext>();
log.Status = WorkerExecutionStatus.Failed; log.Status = WorkerExecutionStatus.Failed;
log.CompletedAt = DateTime.UtcNow; log.CompletedAt = DateTime.Now;
log.DurationMs = (long)(log.CompletedAt.Value - log.StartedAt).TotalMilliseconds; log.DurationMs = (long)(log.CompletedAt.Value - log.StartedAt).TotalMilliseconds;
log.ErrorCount = 1; log.ErrorCount = 1;
log.ErrorMessage = ex.Message; log.ErrorMessage = ex.Message;

View File

@@ -6,13 +6,13 @@
BEGIN TRANSACTION; BEGIN TRANSACTION;
-- Step 1: Validation - Find users with more than 2 children (INVALID for binary tree) -- Step 1: Validation - Find CMS.Users with more than 2 children (INVALID for binary tree)
-- این کاربران باید قبل از Migration بررسی شوند -- این کاربران باید قبل از Migration بررسی شوند
SELECT SELECT
ParentId, ParentId,
COUNT(*) as ChildCount, COUNT(*) as ChildCount,
STRING_AGG(CAST(Id AS VARCHAR), ', ') as ChildIds STRING_AGG(CAST(Id AS VARCHAR), ', ') as ChildIds
FROM Users FROM CMS.Users
WHERE ParentId IS NOT NULL WHERE ParentId IS NOT NULL
GROUP BY ParentId GROUP BY ParentId
HAVING COUNT(*) > 2; HAVING COUNT(*) > 2;
@@ -20,8 +20,8 @@ HAVING COUNT(*) > 2;
-- اگر نتیجه‌ای بود، باید دستی تصمیم بگیرید کدام 2 فرزند باقی بمانند! -- اگر نتیجه‌ای بود، باید دستی تصمیم بگیرید کدام 2 فرزند باقی بمانند!
-- اگر نتیجه‌ای نبود، ادامه دهید: -- اگر نتیجه‌ای نبود، ادامه دهید:
-- Step 2: Copy ParentId → NetworkParentId for all users -- Step 2: Copy ParentId → NetworkParentId for all CMS.Users
UPDATE Users UPDATE CMS.Users
SET NetworkParentId = ParentId SET NetworkParentId = ParentId
WHERE ParentId IS NOT NULL WHERE ParentId IS NOT NULL
AND NetworkParentId IS NULL; AND NetworkParentId IS NULL;
@@ -33,16 +33,16 @@ WITH RankedChildren AS (
Id, Id,
ParentId, ParentId,
ROW_NUMBER() OVER (PARTITION BY ParentId ORDER BY Id ASC) as ChildRank ROW_NUMBER() OVER (PARTITION BY ParentId ORDER BY Id ASC) as ChildRank
FROM Users FROM CMS.Users
WHERE ParentId IS NOT NULL WHERE ParentId IS NOT NULL
) )
UPDATE Users UPDATE CMS.Users
SET LegPosition = CASE SET LegPosition = CASE
WHEN rc.ChildRank = 1 THEN 0 -- Left = 0 (enum value) WHEN rc.ChildRank = 1 THEN 0 -- Left = 0 (enum value)
WHEN rc.ChildRank = 2 THEN 1 -- Right = 1 (enum value) WHEN rc.ChildRank = 2 THEN 1 -- Right = 1 (enum value)
ELSE NULL -- اگر بیشتر از 2 فرزند بود (نباید اتفاق بیفته) ELSE NULL -- اگر بیشتر از 2 فرزند بود (نباید اتفاق بیفته)
END END
FROM Users u FROM CMS.Users u
INNER JOIN RankedChildren rc ON u.Id = rc.Id; INNER JOIN RankedChildren rc ON u.Id = rc.Id;
-- Step 4: Validation - Check for orphaned nodes (Parent doesn't exist) -- Step 4: Validation - Check for orphaned nodes (Parent doesn't exist)
@@ -50,9 +50,9 @@ SELECT
Id, Id,
NetworkParentId, NetworkParentId,
'Orphaned: Parent does not exist' as Issue 'Orphaned: Parent does not exist' as Issue
FROM Users FROM CMS.Users
WHERE NetworkParentId IS NOT NULL WHERE NetworkParentId IS NOT NULL
AND NetworkParentId NOT IN (SELECT Id FROM Users); AND NetworkParentId NOT IN (SELECT Id FROM CMS.Users);
-- اگر Orphan یافت شد، باید آنها را NULL کنید یا Parent صحیح تخصیص دهید -- اگر Orphan یافت شد، باید آنها را NULL کنید یا Parent صحیح تخصیص دهید
@@ -62,7 +62,7 @@ SELECT
NetworkParentId, NetworkParentId,
COUNT(*) as ChildCount, COUNT(*) as ChildCount,
STRING_AGG(CAST(Id AS VARCHAR), ', ') as ChildIds STRING_AGG(CAST(Id AS VARCHAR), ', ') as ChildIds
FROM Users FROM CMS.Users
WHERE NetworkParentId IS NOT NULL WHERE NetworkParentId IS NOT NULL
GROUP BY NetworkParentId GROUP BY NetworkParentId
HAVING COUNT(*) > 2; HAVING COUNT(*) > 2;
@@ -71,26 +71,26 @@ HAVING COUNT(*) > 2;
-- Step 6: Statistics -- Step 6: Statistics
SELECT SELECT
'Total Users' as Metric, 'Total CMS.Users' as Metric,
COUNT(*) as Count COUNT(*) as Count
FROM Users FROM CMS.Users
UNION ALL UNION ALL
SELECT SELECT
'Users with NetworkParentId', 'CMS.Users with NetworkParentId',
COUNT(*) COUNT(*)
FROM Users FROM CMS.Users
WHERE NetworkParentId IS NOT NULL WHERE NetworkParentId IS NOT NULL
UNION ALL UNION ALL
SELECT SELECT
'Users with LegPosition Left', 'CMS.Users with LegPosition Left',
COUNT(*) COUNT(*)
FROM Users FROM CMS.Users
WHERE LegPosition = 0 WHERE LegPosition = 0
UNION ALL UNION ALL
SELECT SELECT
'Users with LegPosition Right', 'CMS.Users with LegPosition Right',
COUNT(*) COUNT(*)
FROM Users FROM CMS.Users
WHERE LegPosition = 1; WHERE LegPosition = 1;
-- Commit if validation passes -- Commit if validation passes

View File

@@ -20,6 +20,7 @@ public class UserClubFeatureConfiguration : IEntityTypeConfiguration<UserClubFea
builder.Property(entity => entity.ClubMembershipId).IsRequired(); builder.Property(entity => entity.ClubMembershipId).IsRequired();
builder.Property(entity => entity.ClubFeatureId).IsRequired(); builder.Property(entity => entity.ClubFeatureId).IsRequired();
builder.Property(entity => entity.GrantedAt).IsRequired(); builder.Property(entity => entity.GrantedAt).IsRequired();
builder.Property(entity => entity.IsActive).IsRequired().HasDefaultValue(true);
builder.Property(entity => entity.Notes).IsRequired(false).HasMaxLength(500); builder.Property(entity => entity.Notes).IsRequired(false).HasMaxLength(500);
// رابطه با User // رابطه با User

View File

@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddIsActiveToUserClubFeatures : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsActive",
schema: "CMS",
table: "UserClubFeatures",
type: "bit",
nullable: false,
defaultValue: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsActive",
schema: "CMS",
table: "UserClubFeatures");
}
}
}

View File

@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddFlushedFieldsToNetworkWeeklyBalance : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "FlushedPerSide",
schema: "CMS",
table: "NetworkWeeklyBalances",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "TotalFlushed",
schema: "CMS",
table: "NetworkWeeklyBalances",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FlushedPerSide",
schema: "CMS",
table: "NetworkWeeklyBalances");
migrationBuilder.DropColumn(
name: "TotalFlushed",
schema: "CMS",
table: "NetworkWeeklyBalances");
}
}
}

View File

@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddSubordinateBalancesToNetworkWeeklyBalance : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "SubordinateBalances",
schema: "CMS",
table: "NetworkWeeklyBalances",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SubordinateBalances",
schema: "CMS",
table: "NetworkWeeklyBalances");
}
}
}

View File

@@ -204,6 +204,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<DateTime>("GrantedAt") b.Property<DateTime>("GrantedAt")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.Property<bool>("IsDeleted") b.Property<bool>("IsDeleted")
.HasColumnType("bit"); .HasColumnType("bit");
@@ -1289,6 +1294,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<string>("CreatedBy") b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<int>("FlushedPerSide")
.HasColumnType("int");
b.Property<bool>("IsDeleted") b.Property<bool>("IsDeleted")
.HasColumnType("bit"); .HasColumnType("bit");
@@ -1331,9 +1339,15 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<int>("RightLegTotal") b.Property<int>("RightLegTotal")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("SubordinateBalances")
.HasColumnType("int");
b.Property<int>("TotalBalances") b.Property<int>("TotalBalances")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("TotalFlushed")
.HasColumnType("int");
b.Property<long>("UserId") b.Property<long>("UserId")
.HasColumnType("bigint"); .HasColumnType("bigint");

View File

@@ -130,7 +130,7 @@ public class DayaLoanApiService : IDayaLoanApiService
"/api/merchant/contracts", "/api/merchant/contracts",
requestBody, requestBody,
cancellationToken); cancellationToken);
var x = await response.Content.ReadAsStringAsync();
// خواندن پاسخ // خواندن پاسخ
var apiResponse = await response.Content.ReadFromJsonAsync<DayaContractsResponse>(cancellationToken); var apiResponse = await response.Content.ReadFromJsonAsync<DayaContractsResponse>(cancellationToken);

View File

@@ -211,7 +211,7 @@ public class DayaPaymentService : IPaymentGatewayService
{ {
IsSuccess = false, IsSuccess = false,
Message = "فرمت شماره شبا نامعتبر است", Message = "فرمت شماره شبا نامعتبر است",
ProcessedAt = DateTime.UtcNow ProcessedAt = DateTime.Now
}; };
} }
@@ -222,7 +222,7 @@ public class DayaPaymentService : IPaymentGatewayService
{ {
IsSuccess = false, IsSuccess = false,
Message = "حداقل مبلغ برداشت 10,000 تومان است", Message = "حداقل مبلغ برداشت 10,000 تومان است",
ProcessedAt = DateTime.UtcNow ProcessedAt = DateTime.Now
}; };
} }
@@ -252,7 +252,7 @@ public class DayaPaymentService : IPaymentGatewayService
{ {
IsSuccess = false, IsSuccess = false,
Message = $"خطا در واریز: {response.StatusCode}", Message = $"خطا در واریز: {response.StatusCode}",
ProcessedAt = DateTime.UtcNow ProcessedAt = DateTime.Now
}; };
} }
@@ -264,7 +264,7 @@ public class DayaPaymentService : IPaymentGatewayService
{ {
IsSuccess = false, IsSuccess = false,
Message = "پاسخ نامعتبر از درگاه", Message = "پاسخ نامعتبر از درگاه",
ProcessedAt = DateTime.UtcNow ProcessedAt = DateTime.Now
}; };
} }
@@ -278,7 +278,7 @@ public class DayaPaymentService : IPaymentGatewayService
BankRefId = result.BankRefId, BankRefId = result.BankRefId,
TrackingCode = result.TrackingCode, TrackingCode = result.TrackingCode,
Message = result.Message, Message = result.Message,
ProcessedAt = DateTime.UtcNow ProcessedAt = DateTime.Now
}; };
} }
catch (Exception ex) catch (Exception ex)
@@ -288,7 +288,7 @@ public class DayaPaymentService : IPaymentGatewayService
{ {
IsSuccess = false, IsSuccess = false,
Message = "خطا در پردازش واریز", Message = "خطا در پردازش واریز",
ProcessedAt = DateTime.UtcNow ProcessedAt = DateTime.Now
}; };
} }
} }

View File

@@ -91,7 +91,7 @@ public class MockPaymentGatewayService : IPaymentGatewayService
{ {
IsSuccess = false, IsSuccess = false,
Message = "فرمت شماره شبا نامعتبر است", Message = "فرمت شماره شبا نامعتبر است",
ProcessedAt = DateTime.UtcNow ProcessedAt = DateTime.Now
}; };
} }
@@ -103,7 +103,7 @@ public class MockPaymentGatewayService : IPaymentGatewayService
{ {
IsSuccess = false, IsSuccess = false,
Message = "حداقل مبلغ برداشت 10,000 تومان است", Message = "حداقل مبلغ برداشت 10,000 تومان است",
ProcessedAt = DateTime.UtcNow ProcessedAt = DateTime.Now
}; };
} }
@@ -121,7 +121,7 @@ public class MockPaymentGatewayService : IPaymentGatewayService
BankRefId = bankRefId, BankRefId = bankRefId,
TrackingCode = trackingCode, TrackingCode = trackingCode,
Message = $"واریز {request.Amount:N0} تومان به حساب {request.Iban} با موفقیت انجام شد (Mock)", Message = $"واریز {request.Amount:N0} تومان به حساب {request.Iban} با موفقیت انجام شد (Mock)",
ProcessedAt = DateTime.UtcNow ProcessedAt = DateTime.Now
}; };
} }
} }

View File

@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.0.145</Version> <Version>0.0.146</Version>
<DebugType>None</DebugType> <DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols> <DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild> <GeneratePackageOnBuild>False</GeneratePackageOnBuild>

View File

@@ -50,6 +50,19 @@ service ClubMembershipContract
get: "/ClubMembership/GetStatistics" get: "/ClubMembership/GetStatistics"
}; };
}; };
// New: Admin Club Feature Management
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: "*"
};
};
} }
// Activate Command // Activate Command
@@ -205,3 +218,43 @@ message MonthlyMembershipTrend
int32 expirations = 3; int32 expirations = 3;
int32 net_change = 4; int32 net_change = 4;
} }
// GetUserClubFeatures Query
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;
}
// ToggleUserClubFeature Command
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;
}

View File

@@ -105,6 +105,11 @@ service CommissionContract
get: "/Commission/GetWorkerLogs" get: "/Commission/GetWorkerLogs"
}; };
}; };
rpc GetAvailableWeeks(GetAvailableWeeksRequest) returns (GetAvailableWeeksResponse){
option (google.api.http) = {
get: "/Commission/GetAvailableWeeks"
};
};
// Financial Reports // Financial Reports
rpc GetWithdrawalReports(GetWithdrawalReportsRequest) returns (GetWithdrawalReportsResponse){ rpc GetWithdrawalReports(GetWithdrawalReportsRequest) returns (GetWithdrawalReportsResponse){
@@ -247,9 +252,39 @@ message CommissionPayoutHistoryModel
int32 action = 9; // CommissionPayoutAction enum int32 action = 9; // CommissionPayoutAction enum
string performed_by = 10; string performed_by = 10;
string reason = 11; string reason = 11;
google.protobuf.Timestamp created = 12; google.protobuf.Timestamp created = 13;
} }
// ============ GetAvailableWeeks ============
message GetAvailableWeeksRequest
{
int32 future_weeks_count = 1; // تعداد هفته‌های آینده (پیش‌فرض: 4)
int32 past_weeks_count = 2; // تعداد هفته‌های گذشته (پیش‌فرض: 12)
}
message GetAvailableWeeksResponse
{
WeekInfo current_week = 1;
repeated WeekInfo calculated_weeks = 2;
repeated WeekInfo pending_weeks = 3;
repeated WeekInfo future_weeks = 4;
}
message WeekInfo
{
string week_number = 1; // YYYY-Www format
google.protobuf.Timestamp start_date = 2;
google.protobuf.Timestamp end_date = 3;
bool is_calculated = 4;
google.protobuf.Timestamp calculated_at = 5;
string last_execution_status = 6;
int64 total_pool_amount = 7;
int32 eligible_users_count = 8;
string display_text = 9; // نمایش فارسی برای UI
}
// GetUserWeeklyBalances Query // GetUserWeeklyBalances Query
message GetUserWeeklyBalancesRequest message GetUserWeeklyBalancesRequest
{ {

View File

@@ -0,0 +1,47 @@
using CMSMicroservice.Application.ClubFeatureCQ.Commands.ToggleUserClubFeature;
using CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
using CMSMicroservice.Protobuf.Protos.ClubMembership;
using Google.Protobuf.WellKnownTypes;
using System;
using System.Collections.Generic;
namespace CMSMicroservice.WebApi.Common.Mappings;
public class ClubFeatureProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// GetUserClubFeaturesRequest → GetUserClubFeaturesQuery
config.NewConfig<GetUserClubFeaturesRequest, GetUserClubFeaturesQuery>()
.Map(dest => dest.UserId, src => src.UserId);
// UserClubFeatureDto → UserClubFeatureModel
config.NewConfig<UserClubFeatureDto, UserClubFeatureModel>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.UserId, src => src.UserId)
.Map(dest => dest.ClubMembershipId, src => src.ClubMembershipId)
.Map(dest => dest.ClubFeatureId, src => src.ClubFeatureId)
.Map(dest => dest.FeatureTitle, src => src.FeatureTitle)
.Map(dest => dest.FeatureDescription, src => src.FeatureDescription ?? "")
.Map(dest => dest.IsActive, src => src.IsActive)
.Map(dest => dest.GrantedAt, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.GrantedAt, DateTimeKind.Utc)))
.Map(dest => dest.Notes, src => src.Notes ?? "");
// List<UserClubFeatureDto> → GetUserClubFeaturesResponse
config.NewConfig<List<UserClubFeatureDto>, GetUserClubFeaturesResponse>()
.Map(dest => dest.Features, src => src.Adapt<List<UserClubFeatureModel>>());
// ToggleUserClubFeatureRequest → ToggleUserClubFeatureCommand
config.NewConfig<ToggleUserClubFeatureRequest, ToggleUserClubFeatureCommand>()
.Map(dest => dest.UserId, src => src.UserId)
.Map(dest => dest.ClubFeatureId, src => src.ClubFeatureId)
.Map(dest => dest.IsActive, src => src.IsActive);
// ToggleUserClubFeatureResponse (App) → ToggleUserClubFeatureResponse (Proto)
config.NewConfig<Application.ClubFeatureCQ.Commands.ToggleUserClubFeature.ToggleUserClubFeatureResponse, Protobuf.Protos.ClubMembership.ToggleUserClubFeatureResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.UserClubFeatureId, src => src.UserClubFeatureId.HasValue ? (long?)src.UserClubFeatureId.Value : null)
.Map(dest => dest.IsActive, src => src.IsActive.HasValue ? (bool?)src.IsActive.Value : null);
}
}

View File

@@ -0,0 +1,38 @@
using CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
using CMSMicroservice.Protobuf.Protos.Commission;
using Google.Protobuf.WellKnownTypes;
using Mapster;
namespace CMSMicroservice.WebApi.Common.Mappings;
public class CommissionProfile : IRegister
{
public void Register(TypeAdapterConfig config)
{
// GetAvailableWeeks Request Mapping
config.NewConfig<GetAvailableWeeksRequest, GetAvailableWeeksQuery>()
.Map(dest => dest.FutureWeeksCount, src => src.FutureWeeksCount > 0 ? src.FutureWeeksCount : 4)
.Map(dest => dest.PastWeeksCount, src => src.PastWeeksCount > 0 ? src.PastWeeksCount : 12);
// GetAvailableWeeks Response Mapping
config.NewConfig<GetAvailableWeeksResponseDto, GetAvailableWeeksResponse>()
.Map(dest => dest.CurrentWeek, src => src.CurrentWeek)
.Map(dest => dest.CalculatedWeeks, src => src.CalculatedWeeks)
.Map(dest => dest.PendingWeeks, src => src.PendingWeeks)
.Map(dest => dest.FutureWeeks, src => src.FutureWeeks);
// WeekInfo Mapping
config.NewConfig<WeekInfoDto, WeekInfo>()
.Map(dest => dest.WeekNumber, src => src.WeekNumber)
.Map(dest => dest.StartDate, src => Timestamp.FromDateTime(src.StartDate.ToUniversalTime()))
.Map(dest => dest.EndDate, src => Timestamp.FromDateTime(src.EndDate.ToUniversalTime()))
.Map(dest => dest.IsCalculated, src => src.IsCalculated)
.Map(dest => dest.CalculatedAt, src => src.CalculatedAt.HasValue
? Timestamp.FromDateTime(src.CalculatedAt.Value.ToUniversalTime())
: null)
.Map(dest => dest.LastExecutionStatus, src => src.LastExecutionStatus ?? string.Empty)
.Map(dest => dest.TotalPoolAmount, src => src.TotalPoolAmount ?? 0)
.Map(dest => dest.EligibleUsersCount, src => src.EligibleUsersCount ?? 0)
.Map(dest => dest.DisplayText, src => src.DisplayText);
}
}

View File

@@ -38,9 +38,9 @@ public class AdminController : ControllerBase
{ {
_logger.LogInformation("🔧 Manual trigger requested by admin for week: {WeekNumber}", weekNumber ?? "previous"); _logger.LogInformation("🔧 Manual trigger requested by admin for week: {WeekNumber}", weekNumber ?? "previous");
// Enqueue immediate job execution // Enqueue immediate job execution with specified week number
var jobId = _backgroundJobClient.Enqueue<WeeklyCommissionJob>( var jobId = _backgroundJobClient.Enqueue<WeeklyCommissionJob>(
job => job.ExecuteAsync(CancellationToken.None)); job => job.ExecuteAsync(weekNumber, CancellationToken.None));
_logger.LogInformation("✅ Job enqueued with ID: {JobId}", jobId); _logger.LogInformation("✅ Job enqueued with ID: {JobId}", jobId);

View File

@@ -196,7 +196,7 @@ using (var scope = app.Services.CreateScope())
// Weekly Commission Calculation: Every Sunday at 00:05 (UTC) // Weekly Commission Calculation: Every Sunday at 00:05 (UTC)
recurringJobManager.AddOrUpdate<CMSMicroservice.Infrastructure.BackgroundJobs.WeeklyCommissionJob>( recurringJobManager.AddOrUpdate<CMSMicroservice.Infrastructure.BackgroundJobs.WeeklyCommissionJob>(
recurringJobId: "weekly-commission-calculation", recurringJobId: "weekly-commission-calculation",
methodCall: job => job.ExecuteAsync(CancellationToken.None), methodCall: job => job.ExecuteAsync(null, CancellationToken.None),
cronExpression: "5 0 * * 0", // Sunday at 00:05 cronExpression: "5 0 * * 0", // Sunday at 00:05
options: new RecurringJobOptions options: new RecurringJobOptions
{ {

View File

@@ -7,6 +7,8 @@ using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership;
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetAllClubMemberships; using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetAllClubMemberships;
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembershipHistory; using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembershipHistory;
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics; using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics;
using CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
using CMSMicroservice.Application.ClubFeatureCQ.Commands.ToggleUserClubFeature;
namespace CMSMicroservice.WebApi.Services; namespace CMSMicroservice.WebApi.Services;
@@ -53,4 +55,14 @@ public class ClubMembershipService : ClubMembershipContract.ClubMembershipContra
{ {
return await _dispatchRequestToCQRS.Handle<GetClubStatisticsRequest, GetClubStatisticsQuery, GetClubStatisticsResponse>(request, context); return await _dispatchRequestToCQRS.Handle<GetClubStatisticsRequest, GetClubStatisticsQuery, GetClubStatisticsResponse>(request, context);
} }
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);
}
} }

View File

@@ -17,6 +17,7 @@ using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalRequests;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerStatus; using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerStatus;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerExecutionLogs; using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerExecutionLogs;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalReports; using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalReports;
using CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
namespace CMSMicroservice.WebApi.Services; namespace CMSMicroservice.WebApi.Services;
@@ -112,6 +113,11 @@ public class CommissionService : CommissionContract.CommissionContractBase
return await _dispatchRequestToCQRS.Handle<GetWorkerExecutionLogsRequest, GetWorkerExecutionLogsQuery, GetWorkerExecutionLogsResponse>(request, context); return await _dispatchRequestToCQRS.Handle<GetWorkerExecutionLogsRequest, GetWorkerExecutionLogsQuery, GetWorkerExecutionLogsResponse>(request, context);
} }
public override async Task<GetAvailableWeeksResponse> GetAvailableWeeks(GetAvailableWeeksRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAvailableWeeksRequest, GetAvailableWeeksQuery, GetAvailableWeeksResponse>(request, context);
}
public override async Task<GetWithdrawalReportsResponse> GetWithdrawalReports(GetWithdrawalReportsRequest request, ServerCallContext context) public override async Task<GetWithdrawalReportsResponse> GetWithdrawalReports(GetWithdrawalReportsRequest request, ServerCallContext context)
{ {
return await _dispatchRequestToCQRS.Handle<GetWithdrawalReportsRequest, GetWithdrawalReportsQuery, GetWithdrawalReportsResponse>(request, context); return await _dispatchRequestToCQRS.Handle<GetWithdrawalReportsRequest, GetWithdrawalReportsQuery, GetWithdrawalReportsResponse>(request, context);

View File

@@ -42,7 +42,6 @@ public class NetworkMembershipService : NetworkMembershipContract.NetworkMembers
public override async Task<GetNetworkTreeResponse> GetNetworkTree(GetNetworkTreeRequest request, ServerCallContext context) public override async Task<GetNetworkTreeResponse> GetNetworkTree(GetNetworkTreeRequest request, ServerCallContext context)
{ {
var x=request.Adapt<GetNetworkTreeQuery>();
return await _dispatchRequestToCQRS.Handle<GetNetworkTreeRequest, GetNetworkTreeQuery, GetNetworkTreeResponse>(request, context); return await _dispatchRequestToCQRS.Handle<GetNetworkTreeRequest, GetNetworkTreeQuery, GetNetworkTreeResponse>(request, context);
} }

View File

@@ -35,7 +35,7 @@ public class DayaLoanCheckWorker
[AutomaticRetry(Attempts = 3)] [AutomaticRetry(Attempts = 3)]
public async Task ExecuteAsync() public async Task ExecuteAsync()
{ {
_logger.LogInformation("DayaLoanCheckWorker started at {Time}", DateTime.UtcNow); _logger.LogInformation("DayaLoanCheckWorker started at {Time}", DateTime.Now);
try try
{ {
@@ -114,7 +114,7 @@ public class DayaLoanCheckWorker
recurringJobManager.AddOrUpdate<DayaLoanCheckWorker>( recurringJobManager.AddOrUpdate<DayaLoanCheckWorker>(
"daya-loan-check", "daya-loan-check",
worker => worker.ExecuteAsync(), worker => worker.ExecuteAsync(),
"*/15 * * * *", // هر 15 دقیقه "*/01 * * * *", // هر 15 دقیقه
TimeZoneInfo.Local TimeZoneInfo.Local
); );
} }