Compare commits
2 Commits
aa66ca10c8
...
12749ccb01
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12749ccb01 | ||
|
|
ff1c1d5d61 |
490
docs/club-feature-management-services.md
Normal file
490
docs/club-feature-management-services.md
Normal 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
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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 پایه
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 کاراکتر باشد")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 تعریف کنیم
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user