Compare commits
59 Commits
1f533cf60a
...
stage_new
| Author | SHA1 | Date | |
|---|---|---|---|
| e383c64e5b | |||
| f5642f88fb | |||
| 14b5db645b | |||
| 14b4022de9 | |||
| d24f326fbc | |||
| 50ba9e3794 | |||
| c5915bb074 | |||
| 25476ba120 | |||
| 9864302bab | |||
| 8b784101be | |||
| 3cb95dc349 | |||
| c7211fb9a7 | |||
| ead0cf4235 | |||
| 26e1243cfe | |||
| 189812e874 | |||
| 730e5a252b | |||
| cf75f14003 | |||
| 6ae1b0cd70 | |||
| 2ae6034fbb | |||
| 094846ce8b | |||
| b19cf5e32e | |||
| b1c3fcfd66 | |||
| 1f6c5a1f45 | |||
| 99b217d5b5 | |||
| 8ee3fe6f7b | |||
| 3c7ac68eeb | |||
| f27418cff4 | |||
| aba534e07c | |||
| af3a29ed27 | |||
| db951699f8 | |||
| bb6b7c709c | |||
| 12749ccb01 | |||
| ff1c1d5d61 | |||
| aa66ca10c8 | |||
| 69d5ecf0d7 | |||
| 1dbe90d020 | |||
| 7ec9c4077c | |||
| 70a06933a9 | |||
| d51ef47c16 | |||
| d9ccfcc587 | |||
| 9e3e0c68c2 | |||
| 6f3bb3e2c3 | |||
| 0fe39cf9f6 | |||
| e5fc13b9e2 | |||
| e7937fe7ee | |||
| 9fb5119fc1 | |||
| 4d6d0dfc9a | |||
| adcc776230 | |||
| a0da7eb8e0 | |||
| e46f54ca5b | |||
| 00ba911703 | |||
| bfcb0d880f | |||
| ed642947dc | |||
| c8db56ab2b | |||
| 80f4e124d6 | |||
| e17b7f4588 | |||
| 4438a4d248 | |||
| eb43b084b2 | |||
| 0c054739ba |
@@ -4,7 +4,7 @@ name: Push nuget and docker image Actions Workflow
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- stage
|
||||
- stage_new
|
||||
jobs:
|
||||
Deploy:
|
||||
runs-on: windows
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- kub-stage
|
||||
|
||||
env:
|
||||
REGISTRY: 194.5.195.53:30080
|
||||
REGISTRY: git.foursat.afrino.co
|
||||
IMAGE_NAME: admin/cms
|
||||
|
||||
jobs:
|
||||
@@ -16,26 +16,34 @@ jobs:
|
||||
image: docker:latest
|
||||
options: --privileged
|
||||
env:
|
||||
HTTP_PROXY: http://proxyuser:87zH26nbqT@46.249.98.211:3128
|
||||
HTTPS_PROXY: http://proxyuser:87zH26nbqT@46.249.98.211:3128
|
||||
HTTP_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
|
||||
HTTPS_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
|
||||
NO_PROXY: localhost,127.0.0.1,gitea-svc,194.5.195.53,10.0.0.0/8
|
||||
steps:
|
||||
- name: Install git
|
||||
run: apk add --no-cache git
|
||||
|
||||
- name: Clone repo
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/CMS.git .
|
||||
apk add --no-cache git curl
|
||||
|
||||
# Install kubectl
|
||||
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
chmod +x kubectl
|
||||
mv kubectl /usr/local/bin/
|
||||
|
||||
- name: Start Docker daemon with proxy
|
||||
- name: Start Docker daemon with insecure registry
|
||||
run: |
|
||||
mkdir -p /etc/docker
|
||||
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||
{
|
||||
"insecure-registries": ["git.foursat.afrino.co", "gitea-svc:3000"]
|
||||
}
|
||||
DAEMON
|
||||
mkdir -p ~/.docker
|
||||
cat > ~/.docker/config.json << CONF
|
||||
cat > ~/.docker/config.json << 'CONF'
|
||||
{
|
||||
"proxies": {
|
||||
"default": {
|
||||
"httpProxy": "http://proxyuser:87zH26nbqT@46.249.98.211:3128",
|
||||
"httpsProxy": "http://proxyuser:87zH26nbqT@46.249.98.211:3128",
|
||||
"httpProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
|
||||
"httpsProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
|
||||
"noProxy": "localhost,127.0.0.1,gitea-svc,194.5.195.53,10.0.0.0/8"
|
||||
}
|
||||
}
|
||||
@@ -46,13 +54,17 @@ jobs:
|
||||
docker info >/dev/null 2>&1 && break || sleep 2
|
||||
done
|
||||
docker info
|
||||
|
||||
|
||||
- name: Checkout code
|
||||
run: |
|
||||
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/CMS.git .
|
||||
git log -1 --format="%H %s"
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
|
||||
--build-arg HTTP_PROXY=http://proxyuser:87zH26nbqT@46.249.98.211:3128 \
|
||||
--build-arg HTTPS_PROXY=http://proxyuser:87zH26nbqT@46.249.98.211:3128 \
|
||||
--build-arg HTTP_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
|
||||
--build-arg HTTPS_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
|
||||
.
|
||||
|
||||
- name: Push to Registry
|
||||
@@ -60,3 +72,15 @@ jobs:
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Deploy to Kubernetes
|
||||
run: |
|
||||
# Setup kubeconfig
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config
|
||||
|
||||
# Restart deployment to pull new image
|
||||
kubectl rollout restart deployment/cms || echo "Deployment doesn't exist yet"
|
||||
|
||||
# Wait for rollout to complete
|
||||
kubectl rollout status deployment/cms --timeout=5m || echo "Deployment rollout pending"
|
||||
|
||||
+6
-5
@@ -1,11 +1,12 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
WORKDIR /src
|
||||
COPY src/*.sln ./ 2>/dev/null || true
|
||||
COPY src/*/*.csproj ./
|
||||
RUN for file in *.csproj; do mkdir -p "${file%.*}" && mv "$file" "${file%.*}/"; done 2>/dev/null || true
|
||||
RUN dotnet restore "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" || dotnet restore
|
||||
|
||||
# Copy solution and project files
|
||||
COPY src/ ./
|
||||
RUN dotnet publish "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" -c Release -o /app/publish
|
||||
|
||||
# Restore and publish
|
||||
RUN dotnet restore "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj"
|
||||
RUN dotnet publish "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" -c Release -o /app/publish --no-restore
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
@@ -256,3 +256,4 @@ curl http://localhost:5133/health/live # Liveness probe (K8s)
|
||||
## 📝 License
|
||||
|
||||
Proprietary - FourSat Company
|
||||
# Multi-remote push enabled
|
||||
|
||||
@@ -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
|
||||
@@ -12,7 +12,10 @@
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.4.0" />
|
||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.10" />
|
||||
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
||||
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CMSMicroservice.Domain\CMSMicroservice.Domain.csproj" />
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
namespace CMSMicroservice.Application.CityCQ.Queries.GetAllCitiesByFilter;
|
||||
|
||||
public record GetAllCitiesByFilterQuery : IRequest<GetAllCitiesByFilterResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// موقعیت صفحه بندی
|
||||
/// </summary>
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// مرتب سازی بر اساس
|
||||
/// </summary>
|
||||
public string? SortBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فیلتر
|
||||
/// </summary>
|
||||
public GetAllCitiesByFilterFilter? Filter { get; init; }
|
||||
}
|
||||
|
||||
public class GetAllCitiesByFilterFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه
|
||||
/// </summary>
|
||||
public long? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نام شهر (Contains)
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نام بومی شهر (Contains)
|
||||
/// </summary>
|
||||
public string? Native { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه استان
|
||||
/// </summary>
|
||||
public long? StateId { get; set; }
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.CityCQ.Queries.GetAllCitiesByFilter;
|
||||
|
||||
public class GetAllCitiesByFilterQueryHandler : IRequestHandler<GetAllCitiesByFilterQuery, GetAllCitiesByFilterResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetAllCitiesByFilterQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetAllCitiesByFilterResponseDto> Handle(
|
||||
GetAllCitiesByFilterQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.Cities
|
||||
.Include(c => c.State)
|
||||
.ApplyOrder(sortBy: request.SortBy)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
if (request.Filter is not null)
|
||||
{
|
||||
query = query
|
||||
.Where(x => request.Filter.Id == null || x.Id == request.Filter.Id)
|
||||
.Where(x => request.Filter.Name == null || x.Name.Contains(request.Filter.Name))
|
||||
.Where(x => request.Filter.Native == null || x.Native.Contains(request.Filter.Native))
|
||||
.Where(x => request.Filter.StateId == null || x.StateId == request.Filter.StateId);
|
||||
}
|
||||
|
||||
return new GetAllCitiesByFilterResponseDto
|
||||
{
|
||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||
Models = await query
|
||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.ProjectToType<GetAllCitiesByFilterResponseModel>()
|
||||
.ToListAsync(cancellationToken)
|
||||
};
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.CityCQ.Queries.GetAllCitiesByFilter;
|
||||
|
||||
public class GetAllCitiesByFilterQueryValidator : AbstractValidator<GetAllCitiesByFilterQuery>
|
||||
{
|
||||
public GetAllCitiesByFilterQueryValidator()
|
||||
{
|
||||
// Validation rules اگر نیاز باشد
|
||||
}
|
||||
|
||||
public async Task<bool> Validate(object model, string propertyName)
|
||||
{
|
||||
var result = await ValidateAsync(
|
||||
ValidationContext<GetAllCitiesByFilterQuery>.CreateWithOptions(
|
||||
(GetAllCitiesByFilterQuery)model,
|
||||
x => x.IncludeProperties(propertyName)));
|
||||
return result.IsValid;
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
namespace CMSMicroservice.Application.CityCQ.Queries.GetAllCitiesByFilter;
|
||||
|
||||
public class GetAllCitiesByFilterResponseDto
|
||||
{
|
||||
/// <summary>
|
||||
/// متادیتا
|
||||
/// </summary>
|
||||
public MetaData MetaData { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// مدل خروجی
|
||||
/// </summary>
|
||||
public List<GetAllCitiesByFilterResponseModel>? Models { get; set; }
|
||||
}
|
||||
|
||||
public class GetAllCitiesByFilterResponseModel
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه خارجی
|
||||
/// </summary>
|
||||
public long ExternalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نام شهر
|
||||
/// </summary>
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// نام بومی شهر (فارسی)
|
||||
/// </summary>
|
||||
public string Native { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// عرض جغرافیایی
|
||||
/// </summary>
|
||||
public string Latitude { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// طول جغرافیایی
|
||||
/// </summary>
|
||||
public string Longitude { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// شناسه استان
|
||||
/// </summary>
|
||||
public long StateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نام استان
|
||||
/// </summary>
|
||||
public string StateName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// نام بومی استان
|
||||
/// </summary>
|
||||
public string StateNative { get; set; } = null!;
|
||||
}
|
||||
+24
@@ -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; }
|
||||
}
|
||||
+78
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
+27
@@ -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; }
|
||||
}
|
||||
+14
@@ -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; }
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
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,
|
||||
SortOrder = ucf.ClubFeature.SortOrder,
|
||||
CreatedAt = ucf.Created
|
||||
})
|
||||
.OrderBy(ucf => ucf.SortOrder)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return userClubFeatures;
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// ترتیب نمایش
|
||||
/// </summary>
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// تاریخ ایجاد رکورد
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.AcceptClubMembershipContract;
|
||||
|
||||
/// <summary>
|
||||
/// Command برای پذیرش قرارداد باشگاه مشتریان
|
||||
/// </summary>
|
||||
public record AcceptClubMembershipContractCommand : IRequest<AcceptClubMembershipContractResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه کاربر
|
||||
/// </summary>
|
||||
public long UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// کد OTP برای تایید
|
||||
/// </summary>
|
||||
public string OtpCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه یکتای امضا (GUID)
|
||||
/// </summary>
|
||||
public string SignGuid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// محتوای HTML قرارداد
|
||||
/// </summary>
|
||||
public string ContractHtml { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO پاسخ پذیرش قرارداد باشگاه مشتریان
|
||||
/// </summary>
|
||||
public class AcceptClubMembershipContractResponseDto
|
||||
{
|
||||
/// <summary>
|
||||
/// آیا عملیات موفق بود؟
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// پیام نتیجه
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه قرارداد ثبت شده
|
||||
/// </summary>
|
||||
public long ContractId { get; set; }
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.AcceptClubMembershipContract;
|
||||
|
||||
/// <summary>
|
||||
/// Handler برای پذیرش قرارداد باشگاه مشتریان
|
||||
/// این handler:
|
||||
/// 1. کد OTP را تایید میکند
|
||||
/// 2. قرارداد را در جدول UserContract ثبت میکند
|
||||
/// 3. باشگاه مشتری را فعال میکند (IsActive = true)
|
||||
/// </summary>
|
||||
public class AcceptClubMembershipContractCommandHandler
|
||||
: IRequestHandler<AcceptClubMembershipContractCommand, AcceptClubMembershipContractResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IConfiguration _cfg;
|
||||
private readonly IHashService _hashService;
|
||||
private readonly ILogger<AcceptClubMembershipContractCommandHandler> _logger;
|
||||
|
||||
private const int MaxAttempts = 5;
|
||||
private const string OtpPurpose = "signClubContract";
|
||||
|
||||
public AcceptClubMembershipContractCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IConfiguration cfg,
|
||||
IHashService hashService,
|
||||
ILogger<AcceptClubMembershipContractCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_cfg = cfg;
|
||||
_hashService = hashService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<AcceptClubMembershipContractResponseDto> Handle(
|
||||
AcceptClubMembershipContractCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Processing club membership contract for UserId: {UserId}",
|
||||
request.UserId
|
||||
);
|
||||
|
||||
// 1. دریافت کاربر
|
||||
var user = await _context.Users
|
||||
.Include(u => u.ClubMembership)
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User not found: {UserId}", request.UserId);
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
}
|
||||
|
||||
// 2. بررسی خرید پکیج
|
||||
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
|
||||
{
|
||||
return new AcceptClubMembershipContractResponseDto
|
||||
{
|
||||
Success = false,
|
||||
Message = "برای امضای قرارداد باشگاه مشتریان ابتدا باید پکیج پایه را خریداری کنید"
|
||||
};
|
||||
}
|
||||
|
||||
// 3. بررسی عدم فعال بودن قبلی باشگاه
|
||||
if (user.ClubMembership?.IsActive == true)
|
||||
{
|
||||
return new AcceptClubMembershipContractResponseDto
|
||||
{
|
||||
Success = false,
|
||||
Message = "شما قبلاً عضو باشگاه مشتریان شدهاید"
|
||||
};
|
||||
}
|
||||
|
||||
// 4. تایید OTP
|
||||
var otpResult = await VerifyOtpAsync(user.Mobile, request.OtpCode, cancellationToken);
|
||||
if (!otpResult.Success)
|
||||
{
|
||||
return new AcceptClubMembershipContractResponseDto
|
||||
{
|
||||
Success = false,
|
||||
Message = otpResult.Message
|
||||
};
|
||||
}
|
||||
|
||||
// 5. ثبت قرارداد در جدول UserContract
|
||||
var contract = await _context.Contracts
|
||||
.FirstOrDefaultAsync(c => c.Type == ContractType.ClubMembership, cancellationToken);
|
||||
|
||||
if (contract == null)
|
||||
{
|
||||
// اگر قرارداد وجود ندارد، یک قرارداد پیشفرض ایجاد کنید
|
||||
contract = new Contract
|
||||
{
|
||||
Title = "قرارداد باشگاه مشتریان",
|
||||
Description = "قوانین و مقررات باشگاه مشتریان کارابازار",
|
||||
HtmlContent = request.ContractHtml,
|
||||
Type = ContractType.ClubMembership
|
||||
};
|
||||
await _context.Contracts.AddAsync(contract, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
var userContract = new UserContract
|
||||
{
|
||||
UserId = user.Id,
|
||||
ContractId = contract.Id,
|
||||
SignGuid = request.SignGuid,
|
||||
SignedPdfFile = request.ContractHtml
|
||||
};
|
||||
await _context.UserContracts.AddAsync(userContract, cancellationToken);
|
||||
|
||||
// 6. فعالسازی باشگاه مشتریان
|
||||
if (user.ClubMembership == null)
|
||||
{
|
||||
user.ClubMembership = new Domain.Entities.Club.ClubMembership
|
||||
{
|
||||
UserId = user.Id,
|
||||
IsActive = true,
|
||||
ActivatedAt = DateTime.Now,
|
||||
InitialContribution = 56_000_000,
|
||||
GiftValue = 25_200_000,
|
||||
PurchaseMethod = user.PackagePurchaseMethod
|
||||
};
|
||||
await _context.ClubMemberships.AddAsync(user.ClubMembership, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
user.ClubMembership.IsActive = true;
|
||||
user.ClubMembership.ActivatedAt = DateTime.Now;
|
||||
_context.ClubMemberships.Update(user.ClubMembership);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Club membership contract accepted and activated for UserId: {UserId}, ContractId: {ContractId}",
|
||||
request.UserId,
|
||||
userContract.Id
|
||||
);
|
||||
|
||||
return new AcceptClubMembershipContractResponseDto
|
||||
{
|
||||
Success = true,
|
||||
Message = "قرارداد باشگاه مشتریان با موفقیت ثبت شد و عضویت شما فعال گردید",
|
||||
ContractId = userContract.Id
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<(bool Success, string Message)> VerifyOtpAsync(
|
||||
string mobile,
|
||||
string code,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var normalizedMobile = mobile.NormalizeIranMobile();
|
||||
var now = DateTime.Now;
|
||||
|
||||
var otp = await _context.OtpTokens
|
||||
.Where(o => o.Mobile == normalizedMobile
|
||||
&& o.Purpose == OtpPurpose
|
||||
&& !o.IsUsed
|
||||
&& o.ExpiresAt > now)
|
||||
.OrderByDescending(o => o.Created)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (otp == null)
|
||||
{
|
||||
return (false, "کد تایید پیدا نشد یا منقضی شده است");
|
||||
}
|
||||
|
||||
if (otp.Attempts >= MaxAttempts)
|
||||
{
|
||||
return (false, "تعداد تلاشها بیش از حد مجاز است. لطفاً کد جدید دریافت کنید");
|
||||
}
|
||||
|
||||
otp.Attempts++;
|
||||
|
||||
var secret = _cfg["Otp:Secret"] ?? throw new InvalidOperationException("Otp:Secret not configured");
|
||||
|
||||
if (!_hashService.VerifyHmacSha256Hex(code, otp.CodeHash, secret))
|
||||
{
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return (false, "کد تایید نادرست است");
|
||||
}
|
||||
|
||||
// کد صحیح است - علامتگذاری به عنوان استفاده شده
|
||||
otp.IsUsed = true;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return (true, "کد تایید صحیح است");
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.AcceptClubMembershipContract;
|
||||
|
||||
public class AcceptClubMembershipContractCommandValidator : AbstractValidator<AcceptClubMembershipContractCommand>
|
||||
{
|
||||
public AcceptClubMembershipContractCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.UserId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه کاربر الزامی است");
|
||||
|
||||
RuleFor(x => x.OtpCode)
|
||||
.NotEmpty()
|
||||
.WithMessage("کد تایید الزامی است")
|
||||
.Length(6)
|
||||
.WithMessage("کد تایید باید ۶ رقم باشد");
|
||||
|
||||
RuleFor(x => x.SignGuid)
|
||||
.NotEmpty()
|
||||
.WithMessage("شناسه امضا الزامی است");
|
||||
|
||||
RuleFor(x => x.ContractHtml)
|
||||
.NotEmpty()
|
||||
.WithMessage("محتوای قرارداد الزامی است");
|
||||
}
|
||||
}
|
||||
+120
-3
@@ -3,11 +3,13 @@ using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Entities.Club;
|
||||
using CMSMicroservice.Domain.Entities.Commission;
|
||||
using CMSMicroservice.Domain.Entities.History;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
|
||||
|
||||
@@ -15,15 +17,18 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
private readonly IWeekDefinitionRepository _weekRepository;
|
||||
private readonly ILogger<ActivateClubMembershipCommandHandler> _logger;
|
||||
|
||||
public ActivateClubMembershipCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUser,
|
||||
IWeekDefinitionRepository weekRepository,
|
||||
ILogger<ActivateClubMembershipCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_currentUser = currentUser;
|
||||
_weekRepository = weekRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -135,9 +140,15 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
.FirstOrDefaultAsync(
|
||||
c => c.Key == "Club.MembershipGiftValue" && c.IsActive,
|
||||
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))
|
||||
{
|
||||
giftValue = configValue;
|
||||
@@ -152,11 +163,27 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
"Club.MembershipGiftValue not found in configuration, using default: {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;
|
||||
bool isNewMembership = existingMembership == null;
|
||||
var activationDate = DateTime.UtcNow;
|
||||
var activationDate = DateTime.Now;
|
||||
|
||||
if (isNewMembership)
|
||||
{
|
||||
@@ -166,7 +193,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
UserId = user.Id,
|
||||
IsActive = true,
|
||||
ActivatedAt = activationDate,
|
||||
InitialContribution = 56_000_000,
|
||||
InitialContribution =activationFeeValue,
|
||||
GiftValue = giftValue, // مقدار از تنظیمات
|
||||
TotalEarned = 0,
|
||||
PurchaseMethod = user.PackagePurchaseMethod
|
||||
@@ -225,6 +252,83 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
_context.ClubMembershipHistories.Add(history);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری
|
||||
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
|
||||
var weeklyPool = await _context.WeeklyCommissionPools
|
||||
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId, cancellationToken);
|
||||
|
||||
if (weeklyPool == null)
|
||||
{
|
||||
// ایجاد Pool جدید برای این هفته
|
||||
weeklyPool = new WeeklyCommissionPool
|
||||
{
|
||||
WeekDefinitionId = currentWeekDefinitionId,
|
||||
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 WeekDefinitionId={WeekDefinitionId} with initial amount: {Amount}",
|
||||
currentWeekDefinitionId,
|
||||
giftValue
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// اضافه کردن به Pool موجود
|
||||
weeklyPool.TotalPoolAmount += activationFeeValue;
|
||||
_context.WeeklyCommissionPools.Update(weeklyPool);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Added {Amount} to existing WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId}. New total: {NewTotal}",
|
||||
activationFeeValue,
|
||||
currentWeekDefinitionId,
|
||||
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(
|
||||
"Club membership activated successfully. UserId: {UserId}, MembershipId: {MembershipId}",
|
||||
user.Id,
|
||||
@@ -243,4 +347,17 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت شناسه تعریف هفته جاری
|
||||
/// </summary>
|
||||
private long GetCurrentWeekDefinitionId()
|
||||
{
|
||||
var week = _weekRepository.GetCurrentWeek();
|
||||
if (week == null)
|
||||
{
|
||||
throw new InvalidOperationException("هفته جاری در سیستم تعریف نشده است");
|
||||
}
|
||||
return week.Id;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ public class AssignClubFeatureCommandHandler : IRequestHandler<AssignClubFeature
|
||||
UserId = request.UserId,
|
||||
ClubMembershipId = membership.Id,
|
||||
ClubFeatureId = request.FeatureId,
|
||||
GrantedAt = request.GrantedAt ?? DateTime.UtcNow,
|
||||
GrantedAt = request.GrantedAt ?? DateTime.Now,
|
||||
Notes = request.Notes
|
||||
};
|
||||
|
||||
|
||||
+2
@@ -12,6 +12,7 @@ public class GetAllClubMembershipsQueryHandler : IRequestHandler<GetAllClubMembe
|
||||
public async Task<GetAllClubMembershipsResponseDto> Handle(GetAllClubMembershipsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.ClubMemberships
|
||||
.Include(x => x.User)
|
||||
.ApplyOrder(sortBy: request.SortBy)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
@@ -33,6 +34,7 @@ public class GetAllClubMembershipsQueryHandler : IRequestHandler<GetAllClubMembe
|
||||
{
|
||||
Id = x.Id,
|
||||
UserId = x.UserId,
|
||||
UserName = x.User != null ? $"{x.User.FirstName} {x.User.LastName}" : string.Empty,
|
||||
IsActive = x.IsActive,
|
||||
ActivatedAt = x.ActivatedAt,
|
||||
InitialContribution = x.InitialContribution,
|
||||
|
||||
+1
@@ -10,6 +10,7 @@ public class GetAllClubMembershipsResponseModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime? ActivatedAt { get; set; }
|
||||
public long InitialContribution { get; set; }
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ public class GetClubMembershipHistoryQueryHandler : IRequestHandler<GetClubMembe
|
||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
||||
}
|
||||
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "-Created"); // پیشفرض: جدیدترین اول
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "Created"); // پیشفرض: جدیدترین اول
|
||||
|
||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ public class GetClubStatisticsQueryHandler : IRequestHandler<GetClubStatisticsQu
|
||||
|
||||
public async Task<GetClubStatisticsResponseDto> Handle(GetClubStatisticsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var now = DateTime.Now;
|
||||
|
||||
// Basic statistics
|
||||
var totalMembers = await _context.ClubMemberships.CountAsync(cancellationToken);
|
||||
|
||||
+4
-4
@@ -33,10 +33,10 @@ public class ApproveWithdrawalCommandHandler : IRequestHandler<ApproveWithdrawal
|
||||
|
||||
// Update status to Withdrawn (approved)
|
||||
payout.Status = CommissionPayoutStatus.Withdrawn;
|
||||
payout.WithdrawnAt = DateTime.UtcNow;
|
||||
payout.WithdrawnAt = DateTime.Now;
|
||||
payout.ProcessedBy = _currentUser.GetPerformedBy();
|
||||
payout.ProcessedAt = DateTime.UtcNow;
|
||||
payout.LastModified = DateTime.UtcNow;
|
||||
payout.ProcessedAt = DateTime.Now;
|
||||
payout.LastModified = DateTime.Now;
|
||||
|
||||
// TODO: Add PayoutHistory record
|
||||
// var history = new CommissionPayoutHistory
|
||||
@@ -51,7 +51,7 @@ public class ApproveWithdrawalCommandHandler : IRequestHandler<ApproveWithdrawal
|
||||
// Action = (int)CommissionPayoutAction.Approved,
|
||||
// PerformedBy = "Admin", // TODO: Get from authenticated user
|
||||
// Reason = request.Notes,
|
||||
// Created = DateTime.UtcNow
|
||||
// Created = DateTime.Now
|
||||
// };
|
||||
// _context.CommissionPayoutHistories.Add(history);
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ public record CalculateWeeklyBalancesCommand : IRequest<int>
|
||||
/// <summary>
|
||||
/// شماره هفته (فرمت: YYYY-Www مثل 2025-W01)
|
||||
/// </summary>
|
||||
public string WeekNumber { get; init; } = string.Empty;
|
||||
public long WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// آیا محاسبه مجدد انجام شود؟ (پیشفرض: false)
|
||||
|
||||
+193
-75
@@ -3,22 +3,33 @@ namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalan
|
||||
public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWeeklyBalancesCommand, int>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekRepository;
|
||||
|
||||
public CalculateWeeklyBalancesCommandHandler(IApplicationDbContext context)
|
||||
public CalculateWeeklyBalancesCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekRepository = weekRepository;
|
||||
}
|
||||
|
||||
public async Task<int> Handle(CalculateWeeklyBalancesCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
var regorianWeekNumber = _weekRepository.GetGregorianWeekNumber(request.WeekDefinitionId);
|
||||
if (regorianWeekNumber == null)
|
||||
{
|
||||
throw new InvalidOperationException($"هفته {request.WeekDefinitionId} در سیستم تعریف نشده است");
|
||||
}
|
||||
|
||||
// بررسی وجود محاسبه قبلی
|
||||
var existingBalances = await _context.NetworkWeeklyBalances
|
||||
.Where(x => x.WeekNumber == request.WeekNumber)
|
||||
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (existingBalances.Any() && !request.ForceRecalculate)
|
||||
{
|
||||
throw new InvalidOperationException($"تعادلهای هفته {request.WeekNumber} قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید");
|
||||
throw new InvalidOperationException($"تعادلهای هفته {request.WeekDefinitionId} قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید");
|
||||
}
|
||||
|
||||
// حذف محاسبات قبلی در صورت ForceRecalculate
|
||||
@@ -28,26 +39,40 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
||||
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
|
||||
.Where(x => x.NetworkParentId.HasValue)
|
||||
.Where(x => activeClubMemberUserIds.Contains(x.Id))
|
||||
.Select(x => new { x.Id })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// دریافت باقیماندههای هفته قبل
|
||||
var previousWeekNumber = GetPreviousWeekNumber(request.WeekNumber);
|
||||
var previousWeekCarryovers = await _context.NetworkWeeklyBalances
|
||||
.Where(x => x.WeekNumber == previousWeekNumber)
|
||||
.Select(x => new
|
||||
{
|
||||
x.UserId,
|
||||
x.LeftLegRemainder,
|
||||
x.RightLegRemainder
|
||||
})
|
||||
.ToDictionaryAsync(x => x.UserId, cancellationToken);
|
||||
var previousWeekDefinitionId = GetPreviousWeekDefinitionId(request.WeekDefinitionId);
|
||||
Dictionary<long, (int LeftLegRemainder, int RightLegRemainder)> previousWeekCarryovers;
|
||||
|
||||
if (previousWeekDefinitionId.HasValue)
|
||||
{
|
||||
previousWeekCarryovers = await _context.NetworkWeeklyBalances
|
||||
.Where(x => x.WeekDefinitionId == previousWeekDefinitionId.Value)
|
||||
.ToDictionaryAsync(
|
||||
x => x.UserId,
|
||||
x => (x.LeftLegRemainder, x.RightLegRemainder),
|
||||
cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
previousWeekCarryovers = new Dictionary<long, (int, int)>();
|
||||
}
|
||||
|
||||
var balancesList = new List<NetworkWeeklyBalance>();
|
||||
var calculatedAt = DateTime.UtcNow;
|
||||
var calculatedAt = DateTime.Now;
|
||||
|
||||
// خواندن یکباره Configuration ها (بهینهسازی - به جای N query)
|
||||
var configs = await _context.SystemConfigurations
|
||||
@@ -57,15 +82,15 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
||||
x.Key == "Commission.MaxWeeklyBalancesPerLeg" ||
|
||||
x.Key == "Commission.MaxNetworkLevel"))
|
||||
.ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken);
|
||||
|
||||
var activationFee = long.Parse(configs.GetValueOrDefault("Club.ActivationFee", "25000000"));
|
||||
var poolPercent = decimal.Parse(configs.GetValueOrDefault("Commission.WeeklyPoolContributionPercent", "20")) / 100m;
|
||||
|
||||
// var activationFee = long.Parse(configs.GetValueOrDefault("Club.ActivationFee", "25000000"));
|
||||
// var poolPercent = decimal.Parse(configs.GetValueOrDefault("Commission.WeeklyPoolContributionPercent", "20")) / 100m;
|
||||
// سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل
|
||||
var maxBalancesPerLeg = int.Parse(configs.GetValueOrDefault("Commission.MaxWeeklyBalancesPerLeg", "300"));
|
||||
// حداکثر عمق شبکه برای شمارش اعضا (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;
|
||||
@@ -77,41 +102,46 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
||||
}
|
||||
|
||||
// محاسبه تعداد اعضای جدید در این هفته (تا maxNetworkLevel لول پایینتر)
|
||||
var leftNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Left, request.WeekNumber, maxNetworkLevel, cancellationToken);
|
||||
var rightNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Right, request.WeekNumber, maxNetworkLevel, cancellationToken);
|
||||
var leftNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Left, request.WeekDefinitionId, maxNetworkLevel, cancellationToken);
|
||||
var rightNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Right, request.WeekDefinitionId, maxNetworkLevel, cancellationToken);
|
||||
|
||||
// محاسبه مجموع هر پا (جدید + باقیمانده)
|
||||
var leftTotal = leftNewMembers + leftCarryover;
|
||||
var rightTotal = rightNewMembers + rightCarryover;
|
||||
|
||||
// ✅ اصلاح شده: اعمال سقف روی هر دست جداگانه (نه روی کل)
|
||||
// سقف 300 برای دست چپ + 300 برای دست راست = حداکثر 600 تعادل در هفته
|
||||
var cappedLeftTotal = Math.Min(leftTotal, maxBalancesPerLeg);
|
||||
var cappedRightTotal = Math.Min(rightTotal, maxBalancesPerLeg);
|
||||
// ✅ مرحله 1: محاسبه تعادل اولیه (قبل از اعمال سقف)
|
||||
// تعادل = کمترین مقدار بین چپ و راست
|
||||
// مثال: چپ=500، راست=600 → تعادل=500
|
||||
var totalBalances = Math.Min(leftTotal, rightTotal);
|
||||
|
||||
// محاسبه تعادل (کمترین مقدار بعد از اعمال سقف)
|
||||
var totalBalances = Math.Min(cappedLeftTotal, cappedRightTotal);
|
||||
// ✅ مرحله 2: محاسبه باقیمانده (قبل از سقف)
|
||||
// باقیمانده = اضافهای که یک طرف دارد
|
||||
// مثال: چپ=500، راست=600، تعادل=500
|
||||
// → باقی چپ = 500-500 = 0
|
||||
// → باقی راست = 600-500 = 100 (میرود برای هفته بعد)
|
||||
var leftRemainder = leftTotal - totalBalances;
|
||||
var rightRemainder = rightTotal - totalBalances;
|
||||
|
||||
// محاسبه باقیمانده برای هفته بعد
|
||||
// باقیمانده = مقداری که از سقف هر دست رد شده
|
||||
// مثال: چپ=350، راست=450، سقف=300
|
||||
// cappedLeft = MIN(350, 300) = 300
|
||||
// cappedRight = MIN(450, 300) = 300
|
||||
// totalBalances = MIN(300, 300) = 300
|
||||
// leftRemainder = 350 - 300 = 50 (مازاد سقف)
|
||||
// rightRemainder = 450 - 300 = 150 (مازاد سقف)
|
||||
var leftRemainder = leftTotal - cappedLeftTotal;
|
||||
var rightRemainder = rightTotal - cappedRightTotal;
|
||||
// ✅ مرحله 3: اعمال سقف 300 (برای امتیاز نهایی)
|
||||
// از تعادل، فقط 300 از هر طرف حساب میشود
|
||||
// مثال: تعادل=500 → امتیاز=300
|
||||
// از چپ: 300 حساب میشود، 200 فلش میشود
|
||||
// از راست: 300 حساب میشود، 200 فلش میشود
|
||||
// جمع فلش = 400 (از بین میرود)
|
||||
var cappedBalances = Math.Min(totalBalances, maxBalancesPerLeg);
|
||||
|
||||
// ✅ مرحله 4: محاسبه فلش (از هر دو طرف)
|
||||
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
|
||||
{
|
||||
UserId = user.Id,
|
||||
WeekNumber = request.WeekNumber,
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
|
||||
// اطلاعات جدید
|
||||
LeftLegNewMembers = leftNewMembers,
|
||||
@@ -122,19 +152,26 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
||||
// مجموع
|
||||
LeftLegTotal = leftTotal,
|
||||
RightLegTotal = rightTotal,
|
||||
TotalBalances = totalBalances, // تعادل واقعی بعد از اعمال سقف روی هر دست
|
||||
TotalBalances = cappedBalances, // امتیاز نهایی بعد از اعمال سقف 300
|
||||
|
||||
// باقیمانده برای هفته بعد (مازاد سقف هر دست)
|
||||
// باقیمانده برای هفته بعد (اضافهای که یک طرف دارد)
|
||||
LeftLegRemainder = leftRemainder,
|
||||
RightLegRemainder = rightRemainder,
|
||||
|
||||
// فلش (از دست رفته)
|
||||
FlushedPerSide = flushedPerSide,
|
||||
TotalFlushed = totalFlushed,
|
||||
|
||||
// تعادل زیرمجموعه - فعلاً 0 (بعد از ذخیره همه تعادلها محاسبه میشه)
|
||||
SubordinateBalances = 0,
|
||||
|
||||
// فیلدهای قدیمی (deprecated) - برای سازگاری با کدهای قبلی
|
||||
#pragma warning disable CS0618
|
||||
LeftLegBalances = leftTotal,
|
||||
RightLegBalances = rightTotal,
|
||||
#pragma warning restore CS0618
|
||||
|
||||
WeeklyPoolContribution = weeklyPoolContribution,
|
||||
WeeklyPoolContribution = 0, // Pool در مرحله بعد محاسبه میشه
|
||||
CalculatedAt = calculatedAt,
|
||||
IsExpired = false
|
||||
};
|
||||
@@ -145,40 +182,54 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
||||
await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// شماره هفته قبل را محاسبه میکند
|
||||
/// شناسه هفته قبل را از WeekDefinitionRepository میگیرد
|
||||
/// </summary>
|
||||
private string GetPreviousWeekNumber(string currentWeekNumber)
|
||||
private long? GetPreviousWeekDefinitionId(long currentWeekDefinitionId)
|
||||
{
|
||||
// مثال: "2025-W48" -> "2025-W47"
|
||||
var parts = currentWeekNumber.Split('-');
|
||||
var year = int.Parse(parts[0]);
|
||||
var week = int.Parse(parts[1].Replace("W", ""));
|
||||
var currentWeekNumber = _weekRepository.GetGregorianWeekNumber(currentWeekDefinitionId);
|
||||
if (currentWeekNumber == null) return null;
|
||||
|
||||
week--;
|
||||
if (week < 1)
|
||||
{
|
||||
year--;
|
||||
week = 52; // یا 53 بسته به سال
|
||||
}
|
||||
var previousWeekNumber = _weekRepository.GetPreviousWeekNumber(currentWeekNumber);
|
||||
if (previousWeekNumber == null) return null;
|
||||
|
||||
return $"{year}-W{week:D2}";
|
||||
return _weekRepository.GetWeekDefinitionId(previousWeekNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// شمارش اعضای جدیدی که در این هفته به یک پا اضافه شدند
|
||||
/// تا maxLevel لول پایینتر شمارش میشود
|
||||
/// </summary>
|
||||
private async Task<int> CountNewMembersInLeg(long userId, NetworkLeg leg, string weekNumber, int maxLevel, CancellationToken cancellationToken)
|
||||
private async Task<int> CountNewMembersInLeg(long userId, NetworkLeg leg, long WeekDefinitionId, int maxLevel, CancellationToken cancellationToken)
|
||||
{
|
||||
// تبدیل WeekNumber به بازه تاریخی
|
||||
var (startDate, endDate) = GetWeekDateRange(weekNumber);
|
||||
var startDateEndDate =_weekRepository.GetWeekDateRange(WeekDefinitionId);
|
||||
|
||||
// شمارش تمام اعضای زیرمجموعه که در این هفته فعال شدند (تا maxLevel لول)
|
||||
var count = await CountNewMembersRecursive(userId, leg, startDate, endDate, 0, maxLevel, cancellationToken);
|
||||
var count = await CountNewMembersRecursive(userId, leg, startDateEndDate.Value.startDate, startDateEndDate.Value.endDate, 0, maxLevel, cancellationToken);
|
||||
|
||||
return count;
|
||||
}
|
||||
@@ -230,22 +281,89 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تبدیل شماره هفته به بازه تاریخی
|
||||
/// تبدیل شماره هفته به بازه تاریخی از WeekDefinitionRepository
|
||||
/// </summary>
|
||||
private (DateTime startDate, DateTime endDate) GetWeekDateRange(string weekNumber)
|
||||
// private (DateTime startDate, DateTime endDate) GetWeekDateRange(string weekNumber)
|
||||
// {
|
||||
// var dateRange = _weekRepository.GetWeekDateRange(weekNumber);
|
||||
// if (dateRange.HasValue)
|
||||
// {
|
||||
// return dateRange.Value;
|
||||
// }
|
||||
//
|
||||
// // Fallback: محاسبه دستی اگر در کش نبود
|
||||
// var parts = weekNumber.Split('-');
|
||||
// var year = int.Parse(parts[0]);
|
||||
// var week = int.Parse(parts[1].Replace("W", ""));
|
||||
//
|
||||
// var jan1 = new DateTime(year, 1, 1);
|
||||
// var jan1DayOfWeek = (int)jan1.DayOfWeek;
|
||||
// var daysToFirstSaturday = jan1DayOfWeek == 6 ? 0 : (6 - jan1DayOfWeek + 7) % 7;
|
||||
// var firstSaturday = jan1.AddDays(daysToFirstSaturday);
|
||||
//
|
||||
// var weekStart = firstSaturday.AddDays((week - 1) * 7);
|
||||
// var weekEnd = weekStart.AddDays(6).AddHours(23).AddMinutes(59).AddSeconds(59);
|
||||
//
|
||||
// return (weekStart, weekEnd);
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه مجموع تعادلهای زیرمجموعه یک کاربر تا maxLevel لول پایینتر
|
||||
/// </summary>
|
||||
private async Task<int> CalculateSubordinateBalancesAsync(
|
||||
long userId,
|
||||
Dictionary<long, NetworkWeeklyBalance> allBalances,
|
||||
int maxLevel,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// مثال: "2025-W48"
|
||||
var parts = weekNumber.Split('-');
|
||||
var year = int.Parse(parts[0]);
|
||||
var week = int.Parse(parts[1].Replace("W", ""));
|
||||
// پیدا کردن همه زیرمجموعهها تا maxLevel لول
|
||||
var subordinates = await GetSubordinatesRecursive(userId, 1, maxLevel, cancellationToken);
|
||||
|
||||
// محاسبه اولین روز هفته (شنبه)
|
||||
var jan1 = new DateTime(year, 1, 1);
|
||||
var daysOffset = DayOfWeek.Saturday - jan1.DayOfWeek;
|
||||
var firstSaturday = jan1.AddDays(daysOffset);
|
||||
var weekStart = firstSaturday.AddDays((week - 1) * 7);
|
||||
var weekEnd = weekStart.AddDays(6).AddHours(23).AddMinutes(59).AddSeconds(59);
|
||||
// جمع تعادلهای آنها
|
||||
var totalSubordinateBalances = 0;
|
||||
foreach (var subordinateId in subordinates)
|
||||
{
|
||||
if (allBalances.ContainsKey(subordinateId))
|
||||
{
|
||||
totalSubordinateBalances += allBalances[subordinateId].TotalBalances;
|
||||
}
|
||||
}
|
||||
|
||||
return (weekStart, weekEnd);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -4,11 +4,11 @@ public class CalculateWeeklyBalancesCommandValidator : AbstractValidator<Calcula
|
||||
{
|
||||
public CalculateWeeklyBalancesCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.WeekNumber)
|
||||
.NotEmpty()
|
||||
RuleFor(x => x.WeekDefinitionId)
|
||||
.NotNull()
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شماره هفته نمیتواند خالی باشد")
|
||||
.Matches(@"^\d{4}-W\d{2}$")
|
||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد (مثل 2025-W01)");
|
||||
;
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
|
||||
+2
-2
@@ -8,10 +8,10 @@ public record CalculateWeeklyCommissionPoolCommand : IRequest<long>
|
||||
/// <summary>
|
||||
/// شماره هفته (فرمت: YYYY-Www)
|
||||
/// </summary>
|
||||
public string WeekNumber { get; init; } = string.Empty;
|
||||
public long WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// آیا محاسبه مجدد انجام شود؟
|
||||
/// </summary>
|
||||
public bool ForceRecalculate { get; init; }
|
||||
public bool ForceRecalculate { get; init; }
|
||||
}
|
||||
|
||||
+266
-37
@@ -3,76 +3,305 @@ namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommi
|
||||
public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<CalculateWeeklyCommissionPoolCommand, long>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekRepository;
|
||||
|
||||
public CalculateWeeklyCommissionPoolCommandHandler(IApplicationDbContext context)
|
||||
public CalculateWeeklyCommissionPoolCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekRepository = weekRepository;
|
||||
}
|
||||
|
||||
public async Task<long> Handle(CalculateWeeklyCommissionPoolCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// بررسی وجود استخر قبلی
|
||||
var existingPool = await _context.WeeklyCommissionPools
|
||||
.FirstOrDefaultAsync(x => x.WeekNumber == request.WeekNumber, cancellationToken);
|
||||
|
||||
if (existingPool != null && existingPool.IsCalculated && !request.ForceRecalculate)
|
||||
// تبدیل WeekNumber به WeekDefinitionId
|
||||
var weekDefinitionId = _weekRepository.GetGregorianWeekNumber(request.WeekDefinitionId);
|
||||
if (weekDefinitionId == null)
|
||||
{
|
||||
throw new InvalidOperationException($"استخر کمیسیون هفته {request.WeekNumber} قبلاً محاسبه شده است");
|
||||
throw new InvalidOperationException($"هفته {request.WeekDefinitionId} در سیستم تعریف نشده است");
|
||||
}
|
||||
|
||||
// بررسی وجود استخر
|
||||
var existingPool = await _context.WeeklyCommissionPools
|
||||
.FirstOrDefaultAsync(x => x.WeekDefinitionId == request.WeekDefinitionId, cancellationToken);
|
||||
|
||||
if (existingPool == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Pool هفته {request.WeekDefinitionId} وجود ندارد. " +
|
||||
"Pool باید در هنگام فعالسازی باشگاه مشتریان ایجاد شده باشد"
|
||||
);
|
||||
}
|
||||
|
||||
if (existingPool.IsCalculated && !request.ForceRecalculate)
|
||||
{
|
||||
throw new InvalidOperationException($"استخر کمیسیون هفته {request.WeekDefinitionId} قبلاً محاسبه شده است");
|
||||
}
|
||||
|
||||
// بررسی وجود تعادلهای هفتگی
|
||||
var weeklyBalances = await _context.NetworkWeeklyBalances
|
||||
.Where(x => x.WeekNumber == request.WeekNumber)
|
||||
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (!weeklyBalances.Any())
|
||||
{
|
||||
throw new InvalidOperationException($"تعادلهای هفته {request.WeekNumber} هنوز محاسبه نشده است. ابتدا CalculateWeeklyBalances را اجرا کنید");
|
||||
throw new InvalidOperationException($"تعادلهای هفته {request.WeekDefinitionId} هنوز محاسبه نشده است. ابتدا CalculateWeeklyBalances را اجرا کنید");
|
||||
}
|
||||
|
||||
// محاسبه مجموع مشارکتها در استخر
|
||||
var totalPoolAmount = weeklyBalances.Sum(x => x.WeeklyPoolContribution);
|
||||
// ⭐ Pool از قبل پُر شده (توسط ActivateClubMembership)
|
||||
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;
|
||||
if (totalBalances > 0)
|
||||
if (totalBalancesInNetwork > 0)
|
||||
{
|
||||
valuePerBalance = totalPoolAmount / totalBalances;
|
||||
valuePerBalance = totalPoolAmount / totalBalancesInNetwork;
|
||||
}
|
||||
|
||||
if (existingPool != null)
|
||||
{
|
||||
// بهروزرسانی
|
||||
existingPool.TotalPoolAmount = totalPoolAmount;
|
||||
existingPool.TotalBalances = totalBalances;
|
||||
existingPool.ValuePerBalance = valuePerBalance;
|
||||
existingPool.IsCalculated = true;
|
||||
existingPool.CalculatedAt = DateTime.UtcNow;
|
||||
// بهروزرسانی Pool
|
||||
existingPool.TotalBalances = totalBalancesInNetwork;
|
||||
existingPool.ValuePerBalance = valuePerBalance;
|
||||
existingPool.IsCalculated = true;
|
||||
existingPool.CalculatedAt = DateTime.Now;
|
||||
|
||||
_context.WeeklyCommissionPools.Update(existingPool);
|
||||
}
|
||||
else
|
||||
_context.WeeklyCommissionPools.Update(existingPool);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// حذف پرداختهای قبلی در صورت ForceRecalculate
|
||||
if (request.ForceRecalculate)
|
||||
{
|
||||
// ایجاد جدید
|
||||
var pool = new WeeklyCommissionPool
|
||||
var oldPayouts = await _context.UserCommissionPayouts
|
||||
.Where(p => p.WeekDefinitionId == request.WeekDefinitionId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (oldPayouts.Any())
|
||||
{
|
||||
WeekNumber = request.WeekNumber,
|
||||
TotalPoolAmount = totalPoolAmount,
|
||||
TotalBalances = totalBalances,
|
||||
var oldPayoutIds = oldPayouts.Select(p => p.Id).ToList();
|
||||
|
||||
// ⭐ ۱. برگرداندن NetworkBalance به حالت قبل (Revert)
|
||||
await RevertUserWalletsAsync(oldPayoutIds, cancellationToken);
|
||||
|
||||
// ⭐ ۲. حذف تاریخچهها (به خاطر 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,
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
WeeklyPoolId = existingPool.Id,
|
||||
BalancesEarned = userBalance,
|
||||
ValuePerBalance = valuePerBalance,
|
||||
IsCalculated = true,
|
||||
CalculatedAt = DateTime.UtcNow
|
||||
TotalAmount = totalAmount,
|
||||
Status = CommissionPayoutStatus.Pending,
|
||||
PaidAt = null,
|
||||
WithdrawalMethod = null,
|
||||
IbanNumber = null,
|
||||
WithdrawnAt = null
|
||||
};
|
||||
|
||||
await _context.WeeklyCommissionPools.AddAsync(pool, cancellationToken);
|
||||
existingPool = pool;
|
||||
payouts.Add(payout);
|
||||
}
|
||||
|
||||
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,
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
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);
|
||||
|
||||
// ⭐ شارژ کیف پول کاربران (NetworkBalance) و ثبت لاگ
|
||||
await ChargeUserWalletsAsync(payouts, request.WeekDefinitionId, cancellationToken);
|
||||
}
|
||||
|
||||
return existingPool.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// شارژ کیف پول کاربران با مبلغ کمیسیون (به NetworkBalance اضافه میشود)
|
||||
/// و ثبت لاگ تغییرات کیف پول
|
||||
/// </summary>
|
||||
private async Task ChargeUserWalletsAsync(
|
||||
List<UserCommissionPayout> payouts,
|
||||
long weekDefinitionId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userIds = payouts.Select(p => p.UserId).Distinct().ToList();
|
||||
|
||||
// دریافت کیف پولهای موجود
|
||||
var existingWallets = await _context.UserWallets
|
||||
.Where(w => userIds.Contains(w.UserId))
|
||||
.ToDictionaryAsync(w => w.UserId, cancellationToken);
|
||||
|
||||
var newWallets = new List<UserWallet>();
|
||||
var walletLogs = new List<UserWalletChangeLog>();
|
||||
|
||||
foreach (var payout in payouts)
|
||||
{
|
||||
if (payout.TotalAmount <= 0) continue;
|
||||
|
||||
UserWallet wallet;
|
||||
|
||||
// پیدا کردن یا ایجاد کیف پول
|
||||
if (existingWallets.TryGetValue(payout.UserId, out var existingWallet))
|
||||
{
|
||||
wallet = existingWallet;
|
||||
}
|
||||
else
|
||||
{
|
||||
// ایجاد کیف پول جدید
|
||||
wallet = new UserWallet
|
||||
{
|
||||
UserId = payout.UserId,
|
||||
Balance = 0,
|
||||
NetworkBalance = 0,
|
||||
DiscountBalance = 0
|
||||
};
|
||||
newWallets.Add(wallet);
|
||||
existingWallets[payout.UserId] = wallet; // برای جلوگیری از تکرار
|
||||
}
|
||||
}
|
||||
|
||||
// ذخیره کیف پولهای جدید
|
||||
if (newWallets.Any())
|
||||
{
|
||||
await _context.UserWallets.AddRangeAsync(newWallets, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// شارژ کیف پولها و ثبت لاگ
|
||||
foreach (var payout in payouts)
|
||||
{
|
||||
if (payout.TotalAmount <= 0) continue;
|
||||
|
||||
var wallet = existingWallets[payout.UserId];
|
||||
|
||||
// ذخیره موجودی قبلی برای لاگ
|
||||
var previousNetworkBalance = wallet.NetworkBalance;
|
||||
|
||||
// شارژ NetworkBalance
|
||||
wallet.NetworkBalance += payout.TotalAmount;
|
||||
|
||||
// ایجاد لاگ تغییر کیف پول
|
||||
var walletLog = new UserWalletChangeLog
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
ChangeValue = 0, // Balance اصلی تغییر نمیکند
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = payout.TotalAmount,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeDiscountValue = 0,
|
||||
IsIncrease = true,
|
||||
RefrenceId = payout.Id // ارجاع به UserCommissionPayout
|
||||
};
|
||||
|
||||
walletLogs.Add(walletLog);
|
||||
}
|
||||
|
||||
// ذخیره تغییرات کیف پول و لاگها
|
||||
await _context.UserWalletChangeLogs.AddRangeAsync(walletLogs, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// برگرداندن NetworkBalance به حالت قبل از شارژ کمیسیون
|
||||
/// این متد قبل از محاسبه مجدد (ForceRecalculate) اجرا میشود
|
||||
/// </summary>
|
||||
private async Task RevertUserWalletsAsync(
|
||||
List<long> oldPayoutIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// پیدا کردن لاگهای کیف پول مرتبط با پرداختهای قبلی
|
||||
var oldWalletLogs = await _context.UserWalletChangeLogs
|
||||
.Where(l => l.RefrenceId.HasValue && oldPayoutIds.Contains(l.RefrenceId.Value))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (!oldWalletLogs.Any()) return;
|
||||
|
||||
// پیدا کردن کیف پولهای مرتبط
|
||||
var walletIds = oldWalletLogs.Select(l => l.WalletId).Distinct().ToList();
|
||||
var wallets = await _context.UserWallets
|
||||
.Where(w => walletIds.Contains(w.Id))
|
||||
.ToDictionaryAsync(w => w.Id, cancellationToken);
|
||||
|
||||
// برگرداندن NetworkBalance
|
||||
foreach (var log in oldWalletLogs)
|
||||
{
|
||||
if (wallets.TryGetValue(log.WalletId, out var wallet))
|
||||
{
|
||||
// کم کردن مبلغ شارژ شده قبلی از NetworkBalance
|
||||
wallet.NetworkBalance -= log.ChangeNerworkValue;
|
||||
|
||||
// اطمینان از منفی نشدن
|
||||
if (wallet.NetworkBalance < 0)
|
||||
{
|
||||
wallet.NetworkBalance = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// حذف لاگهای قبلی
|
||||
_context.UserWalletChangeLogs.RemoveRange(oldWalletLogs);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -4,11 +4,10 @@ public class CalculateWeeklyCommissionPoolCommandValidator : AbstractValidator<C
|
||||
{
|
||||
public CalculateWeeklyCommissionPoolCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.WeekNumber)
|
||||
.NotEmpty()
|
||||
.WithMessage("شماره هفته نمیتواند خالی باشد")
|
||||
.Matches(@"^\d{4}-W\d{2}$")
|
||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد");
|
||||
RuleFor(x => x.WeekDefinitionId)
|
||||
.NotNull()
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شماره هفته نمیتواند خالی باشد");
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ public record ProcessUserPayoutsCommand : IRequest<int>
|
||||
/// <summary>
|
||||
/// شماره هفته
|
||||
/// </summary>
|
||||
public string WeekNumber { get; init; } = string.Empty;
|
||||
public long WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// آیا پرداخت مجدد انجام شود؟
|
||||
|
||||
+21
-11
@@ -3,31 +3,41 @@ namespace CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
|
||||
public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayoutsCommand, int>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekRepository;
|
||||
|
||||
public ProcessUserPayoutsCommandHandler(IApplicationDbContext context)
|
||||
public ProcessUserPayoutsCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekRepository = weekRepository;
|
||||
}
|
||||
|
||||
public async Task<int> Handle(ProcessUserPayoutsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var regorianWeekNumber = _weekRepository.GetGregorianWeekNumber(request.WeekDefinitionId);
|
||||
if (regorianWeekNumber == null)
|
||||
{
|
||||
throw new InvalidOperationException($"هفته {request.WeekDefinitionId} در سیستم تعریف نشده است");
|
||||
}
|
||||
|
||||
// بررسی وجود استخر
|
||||
var pool = await _context.WeeklyCommissionPools
|
||||
.FirstOrDefaultAsync(x => x.WeekNumber == request.WeekNumber, cancellationToken);
|
||||
.FirstOrDefaultAsync(x => x.WeekDefinitionId == request.WeekDefinitionId, cancellationToken);
|
||||
|
||||
if (pool == null || !pool.IsCalculated)
|
||||
{
|
||||
throw new InvalidOperationException($"استخر کمیسیون هفته {request.WeekNumber} هنوز محاسبه نشده است");
|
||||
throw new InvalidOperationException($"استخر کمیسیون هفته {request.WeekDefinitionId} هنوز محاسبه نشده است");
|
||||
}
|
||||
|
||||
// بررسی پرداخت قبلی
|
||||
var existingPayouts = await _context.UserCommissionPayouts
|
||||
.Where(x => x.WeekNumber == request.WeekNumber)
|
||||
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (existingPayouts.Any() && !request.ForceReprocess)
|
||||
{
|
||||
throw new InvalidOperationException($"پرداختهای هفته {request.WeekNumber} قبلاً انجام شده است");
|
||||
throw new InvalidOperationException($"پرداختهای هفته {request.WeekDefinitionId} قبلاً انجام شده است");
|
||||
}
|
||||
|
||||
// حذف پرداختهای قبلی در صورت ForceReprocess
|
||||
@@ -46,12 +56,12 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
||||
|
||||
// دریافت همه تعادلهای هفتگی (شامل صفرها هم برای محاسبه زیرمجموعه)
|
||||
var allWeeklyBalances = await _context.NetworkWeeklyBalances
|
||||
.Where(x => x.WeekNumber == request.WeekNumber)
|
||||
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId)
|
||||
.ToDictionaryAsync(x => x.UserId, cancellationToken);
|
||||
|
||||
// دریافت کاربرانی که تعادل > 0 دارند (یا زیرمجموعهشان دارد)
|
||||
var usersWithBalances = await _context.NetworkWeeklyBalances
|
||||
.Where(x => x.WeekNumber == request.WeekNumber && x.TotalBalances > 0)
|
||||
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId && x.TotalBalances > 0)
|
||||
.Select(x => x.UserId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -82,7 +92,7 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
||||
// ⭐ محاسبه مجموع تعادلهای زیرمجموعه تا maxNetworkLevel لول
|
||||
var subordinateBalances = await CalculateSubordinateBalancesAsync(
|
||||
userId,
|
||||
request.WeekNumber,
|
||||
request.WeekDefinitionId,
|
||||
allWeeklyBalances,
|
||||
maxNetworkLevel,
|
||||
cancellationToken
|
||||
@@ -103,7 +113,7 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
||||
var payout = new UserCommissionPayout
|
||||
{
|
||||
UserId = userId,
|
||||
WeekNumber = request.WeekNumber,
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
WeeklyPoolId = pool.Id,
|
||||
BalancesEarned = totalBalancesWithSubordinates, // ⭐ شامل زیرمجموعه
|
||||
ValuePerBalance = pool.ValuePerBalance,
|
||||
@@ -129,7 +139,7 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
||||
{
|
||||
UserCommissionPayoutId = payout.Id,
|
||||
UserId = payout.UserId,
|
||||
WeekNumber = request.WeekNumber,
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
AmountBefore = 0,
|
||||
AmountAfter = payout.TotalAmount,
|
||||
OldStatus = default(CommissionPayoutStatus),
|
||||
@@ -180,7 +190,7 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
||||
/// </summary>
|
||||
private async Task<int> CalculateSubordinateBalancesAsync(
|
||||
long userId,
|
||||
string weekNumber,
|
||||
long WeekDefinitionId,
|
||||
Dictionary<long, NetworkWeeklyBalance> allBalances,
|
||||
int maxLevel,
|
||||
CancellationToken cancellationToken)
|
||||
|
||||
+4
-5
@@ -4,11 +4,10 @@ public class ProcessUserPayoutsCommandValidator : AbstractValidator<ProcessUserP
|
||||
{
|
||||
public ProcessUserPayoutsCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.WeekNumber)
|
||||
.NotEmpty()
|
||||
.WithMessage("شماره هفته نمیتواند خالی باشد")
|
||||
.Matches(@"^\d{4}-W\d{2}$")
|
||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد");
|
||||
RuleFor(x => x.WeekDefinitionId)
|
||||
.NotNull()
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شماره هفته نمیتواند خالی باشد");
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
|
||||
+4
-4
@@ -39,7 +39,7 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
|
||||
}
|
||||
|
||||
var oldStatus = payout.Status;
|
||||
var now = DateTime.UtcNow;
|
||||
var now = DateTime.Now;
|
||||
|
||||
if (request.IsApproved)
|
||||
{
|
||||
@@ -73,7 +73,7 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
|
||||
UserId = payout.UserId,
|
||||
Iban = payout.IbanNumber ?? throw new InvalidOperationException("شماره شبا یافت نشد"),
|
||||
AccountHolderName = $"{payout.User.FirstName} {payout.User.LastName}",
|
||||
Description = $"برداشت کمیسیون هفته {payout.WeekNumber}",
|
||||
Description = $"برداشت کمیسیون هفته {payout.WeekDefinitionId}",
|
||||
InternalRefId = $"PAYOUT-{payout.Id}"
|
||||
};
|
||||
|
||||
@@ -120,7 +120,7 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
|
||||
{
|
||||
UserCommissionPayoutId = payout.Id,
|
||||
UserId = payout.UserId,
|
||||
WeekNumber = payout.WeekNumber,
|
||||
WeekDefinitionId = payout.WeekDefinitionId,
|
||||
AmountBefore = payout.TotalAmount,
|
||||
AmountAfter = payout.TotalAmount,
|
||||
OldStatus = oldStatus,
|
||||
@@ -147,7 +147,7 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
|
||||
{
|
||||
UserCommissionPayoutId = payout.Id,
|
||||
UserId = payout.UserId,
|
||||
WeekNumber = payout.WeekNumber,
|
||||
WeekDefinitionId = payout.WeekDefinitionId,
|
||||
AmountBefore = payout.TotalAmount,
|
||||
AmountAfter = payout.TotalAmount,
|
||||
OldStatus = oldStatus,
|
||||
|
||||
+3
-3
@@ -34,9 +34,9 @@ public class RejectWithdrawalCommandHandler : IRequestHandler<RejectWithdrawalCo
|
||||
// Update status to Cancelled (rejected)
|
||||
payout.Status = CommissionPayoutStatus.Cancelled;
|
||||
payout.ProcessedBy = _currentUser.GetPerformedBy();
|
||||
payout.ProcessedAt = DateTime.UtcNow;
|
||||
payout.ProcessedAt = DateTime.Now;
|
||||
payout.RejectionReason = request.Reason;
|
||||
payout.LastModified = DateTime.UtcNow;
|
||||
payout.LastModified = DateTime.Now;
|
||||
|
||||
// TODO: Add PayoutHistory record with rejection reason
|
||||
// var history = new CommissionPayoutHistory
|
||||
@@ -51,7 +51,7 @@ public class RejectWithdrawalCommandHandler : IRequestHandler<RejectWithdrawalCo
|
||||
// Action = (int)CommissionPayoutAction.Rejected,
|
||||
// PerformedBy = "Admin", // TODO: Get from authenticated user
|
||||
// Reason = request.Reason,
|
||||
// Created = DateTime.UtcNow
|
||||
// Created = DateTime.Now
|
||||
// };
|
||||
// _context.CommissionPayoutHistories.Add(history);
|
||||
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawal
|
||||
{
|
||||
UserCommissionPayoutId = payout.Id,
|
||||
UserId = payout.UserId,
|
||||
WeekNumber = payout.WeekNumber,
|
||||
WeekDefinitionId = payout.WeekDefinitionId,
|
||||
AmountBefore = payout.TotalAmount,
|
||||
AmountAfter = payout.TotalAmount,
|
||||
OldStatus = oldStatus,
|
||||
|
||||
+3
-8
@@ -5,7 +5,7 @@ public record TriggerWeeklyCalculationCommand : IRequest<TriggerWeeklyCalculatio
|
||||
/// <summary>
|
||||
/// شماره هفته (فرمت: "YYYY-Www")
|
||||
/// </summary>
|
||||
public string WeekNumber { get; init; } = string.Empty;
|
||||
public long WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// اگر true باشد، محاسبات قبلی را حذف و دوباره محاسبه میکند
|
||||
@@ -13,17 +13,12 @@ public record TriggerWeeklyCalculationCommand : IRequest<TriggerWeeklyCalculatio
|
||||
public bool ForceRecalculate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Skip balance calculation
|
||||
/// Skip balance calculation (Step 1)
|
||||
/// </summary>
|
||||
public bool SkipBalances { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Skip pool calculation
|
||||
/// </summary>
|
||||
public bool SkipPool { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Skip payout processing
|
||||
/// Skip pool calculation and payout processing (Step 2)
|
||||
/// </summary>
|
||||
public bool SkipPayouts { get; init; }
|
||||
}
|
||||
|
||||
+25
-29
@@ -1,7 +1,6 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
|
||||
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
|
||||
using CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Commands.TriggerWeeklyCalculation;
|
||||
|
||||
@@ -9,13 +8,16 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ICommissionCalculationStrategyFactory _strategyFactory;
|
||||
|
||||
public TriggerWeeklyCalculationCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IMediator mediator)
|
||||
IMediator mediator,
|
||||
ICommissionCalculationStrategyFactory strategyFactory)
|
||||
{
|
||||
_context = context;
|
||||
_mediator = mediator;
|
||||
_strategyFactory = strategyFactory;
|
||||
}
|
||||
|
||||
public async Task<TriggerWeeklyCalculationResponseDto> Handle(
|
||||
@@ -23,12 +25,12 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var executionId = Guid.NewGuid().ToString();
|
||||
var startedAt = DateTime.UtcNow;
|
||||
var startedAt = DateTime.Now;
|
||||
|
||||
try
|
||||
{
|
||||
// Validate week number format
|
||||
if (string.IsNullOrWhiteSpace(request.WeekNumber))
|
||||
if (request.WeekDefinitionId <= 0)
|
||||
{
|
||||
return new TriggerWeeklyCalculationResponseDto
|
||||
{
|
||||
@@ -40,43 +42,37 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
|
||||
}
|
||||
|
||||
var steps = new List<string>();
|
||||
|
||||
// ⭐ دریافت استراتژی براساس Config (ORM یا SP)
|
||||
var strategy = await _strategyFactory.CreateStrategyAsync(cancellationToken);
|
||||
var strategyName = strategy.GetType().Name.Contains("StoredProcedure") ? "SP" : "ORM";
|
||||
|
||||
// Step 1: Calculate Weekly Balances
|
||||
// Step 1: Calculate Weekly Balances (تا 15 لول)
|
||||
if (!request.SkipBalances)
|
||||
{
|
||||
await _mediator.Send(new CalculateWeeklyBalancesCommand
|
||||
{
|
||||
WeekNumber = request.WeekNumber,
|
||||
ForceRecalculate = request.ForceRecalculate
|
||||
}, cancellationToken);
|
||||
steps.Add("محاسبه امتیازات هفتگی");
|
||||
var balancesCount = await strategy.CalculateWeeklyBalancesAsync(
|
||||
request.WeekDefinitionId,
|
||||
request.ForceRecalculate,
|
||||
cancellationToken);
|
||||
|
||||
steps.Add($"محاسبه امتیازات هفتگی ({balancesCount} کاربر)");
|
||||
}
|
||||
|
||||
// Step 2: Calculate Weekly Commission Pool
|
||||
if (!request.SkipPool)
|
||||
{
|
||||
await _mediator.Send(new CalculateWeeklyCommissionPoolCommand
|
||||
{
|
||||
WeekNumber = request.WeekNumber
|
||||
}, cancellationToken);
|
||||
steps.Add("محاسبه استخر کمیسیون");
|
||||
}
|
||||
|
||||
// Step 3: Process User Payouts
|
||||
// Step 2: Calculate Pool & Process Payouts (محاسبه استخر + پرداخت کاربران)
|
||||
if (!request.SkipPayouts)
|
||||
{
|
||||
await _mediator.Send(new ProcessUserPayoutsCommand
|
||||
{
|
||||
WeekNumber = request.WeekNumber,
|
||||
ForceReprocess = request.ForceRecalculate
|
||||
}, cancellationToken);
|
||||
steps.Add("پردازش پرداختهای کاربران");
|
||||
var poolId = await strategy.CalculateWeeklyCommissionPoolAsync(
|
||||
request.WeekDefinitionId,
|
||||
request.ForceRecalculate,
|
||||
cancellationToken);
|
||||
|
||||
steps.Add($"محاسبه استخر و پرداخت کاربران (Pool: {poolId})");
|
||||
}
|
||||
|
||||
return new TriggerWeeklyCalculationResponseDto
|
||||
{
|
||||
Success = true,
|
||||
Message = $"محاسبات هفته {request.WeekNumber} با موفقیت انجام شد. مراحل: {string.Join(", ", steps)}",
|
||||
Message = $"محاسبات هفته {request.WeekDefinitionId} با موفقیت انجام شد [{strategyName}]. مراحل: {string.Join(", ", steps)}",
|
||||
ExecutionId = executionId,
|
||||
StartedAt = startedAt
|
||||
};
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
using CMSMicroservice.Application.CommissionCQ.Commands.ProcessWithdrawal;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Commands.TriggerWeeklyCalculation;
|
||||
|
||||
public class TriggerWeeklyCalculationCommandValidator : AbstractValidator<TriggerWeeklyCalculationCommand>
|
||||
{
|
||||
public TriggerWeeklyCalculationCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.WeekDefinitionId)
|
||||
.NotNull()
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شماره هفته نمیتواند خالی باشد");
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
{
|
||||
var result = await ValidateAsync(
|
||||
ValidationContext<TriggerWeeklyCalculationCommand>.CreateWithOptions(
|
||||
(TriggerWeeklyCalculationCommand)model,
|
||||
x => x.IncludeProperties(propertyName)));
|
||||
|
||||
if (result.IsValid)
|
||||
return Array.Empty<string>();
|
||||
|
||||
return result.Errors.Select(e => e.ErrorMessage);
|
||||
};
|
||||
}
|
||||
+2
-2
@@ -8,12 +8,12 @@ public record GetAllWeeklyPoolsQuery : IRequest<GetAllWeeklyPoolsResponseDto>
|
||||
/// <summary>
|
||||
/// از هفته (فیلتر اختیاری)
|
||||
/// </summary>
|
||||
public string? FromWeek { get; init; }
|
||||
public int? FromWeekOrder { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// تا هفته (فیلتر اختیاری)
|
||||
/// </summary>
|
||||
public string? ToWeek { get; init; }
|
||||
public int? ToWeekOrder { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فقط Pool های محاسبه شده
|
||||
|
||||
+28
-8
@@ -3,25 +3,31 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools;
|
||||
public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQuery, GetAllWeeklyPoolsResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
|
||||
public GetAllWeeklyPoolsQueryHandler(IApplicationDbContext context)
|
||||
public GetAllWeeklyPoolsQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekDefinitionRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
}
|
||||
|
||||
public async Task<GetAllWeeklyPoolsResponseDto> Handle(GetAllWeeklyPoolsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.WeeklyCommissionPools.AsNoTracking();
|
||||
var query = _context.WeeklyCommissionPools
|
||||
.Include(i=> i.WeekDefinition)
|
||||
.AsNoTracking();
|
||||
|
||||
// Apply filters
|
||||
if (!string.IsNullOrWhiteSpace(request.FromWeek))
|
||||
if (request.FromWeekOrder!=null)
|
||||
{
|
||||
query = query.Where(x => string.Compare(x.WeekNumber, request.FromWeek) >= 0);
|
||||
query = query.Where(x => x.WeekDefinition.WeekOrder>=request.FromWeekOrder );
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.ToWeek))
|
||||
if (request.ToWeekOrder!=null)
|
||||
{
|
||||
query = query.Where(x => string.Compare(x.WeekNumber, request.ToWeek) <= 0);
|
||||
query = query.Where(x =>x.WeekDefinition.WeekOrder<= request.ToWeekOrder);
|
||||
}
|
||||
|
||||
if (request.OnlyCalculated.HasValue && request.OnlyCalculated.Value)
|
||||
@@ -30,7 +36,7 @@ public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQu
|
||||
}
|
||||
|
||||
// Order by week number descending (newest first)
|
||||
query = query.OrderByDescending(x => x.WeekNumber);
|
||||
query = query.OrderByDescending(x => x.WeekDefinitionId);
|
||||
|
||||
// Count total
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
@@ -42,7 +48,8 @@ public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQu
|
||||
.Select(x => new WeeklyCommissionPoolDto
|
||||
{
|
||||
Id = x.Id,
|
||||
WeekNumber = x.WeekNumber,
|
||||
WeekDefinitionId = x.WeekDefinitionId,
|
||||
WeekDisplayName = x.WeekDefinition.PersianWeekNumber,
|
||||
TotalPoolAmount = x.TotalPoolAmount,
|
||||
TotalBalances = x.TotalBalances,
|
||||
ValuePerBalance = x.ValuePerBalance,
|
||||
@@ -52,6 +59,19 @@ public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQu
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Populate WeekDisplayName from cache
|
||||
// foreach (var pool in pools)
|
||||
// {
|
||||
// // WeeklyCommissionPoolDto is a record, need to create new instance with display name
|
||||
// // Since records are immutable, we can't modify them directly
|
||||
// }
|
||||
|
||||
// // Create new list with WeekDisplayName populated
|
||||
// var poolsWithDisplayName = pools.Select(p => p with
|
||||
// {
|
||||
// WeekDisplayName = _weekDefinitionRepository.GetDisplayNameByGregorianWeekNumber(p.WeekNumber)
|
||||
// }).ToList();
|
||||
|
||||
return new GetAllWeeklyPoolsResponseDto
|
||||
{
|
||||
MetaData = new MetaDataDto
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@ public record GetAllWeeklyPoolsResponseDto
|
||||
public record WeeklyCommissionPoolDto
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string WeekNumber { get; init; } = string.Empty;
|
||||
public long WeekDefinitionId { get; init; }
|
||||
public string WeekDisplayName { get; init; } = string.Empty;
|
||||
public long TotalPoolAmount { get; init; }
|
||||
public int TotalBalances { get; init; }
|
||||
public long ValuePerBalance { get; init; }
|
||||
|
||||
+13
@@ -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;
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
using DateTimeConverterCL;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
|
||||
|
||||
public class GetAvailableWeeksQueryHandler : IRequestHandler<GetAvailableWeeksQuery, GetAvailableWeeksResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
|
||||
public GetAvailableWeeksQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekDefinitionRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
}
|
||||
|
||||
public async Task<GetAvailableWeeksResponseDto> Handle(
|
||||
GetAvailableWeeksQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var currentWeekDef = _weekDefinitionRepository.GetCurrentWeek();
|
||||
// var currentWeekNumber = currentWeekDef != null
|
||||
// ? $"{currentWeekDef.GregorianYear}-W{currentWeekDef.GregorianWeekNumber:D2}"
|
||||
// : _weekDefinitionRepository.CalculateGregorianWeekNumber(DateTime.Now);
|
||||
|
||||
// دریافت هفتههای محاسبه شده از دیتابیس
|
||||
var calculatedPools = await _context.WeeklyCommissionPools
|
||||
.Where(p => p.IsCalculated)
|
||||
.OrderByDescending(p => p.WeekDefinitionId)
|
||||
.Take(request.PastWeeksCount)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// دریافت لاگهای اجرا
|
||||
var executionLogs = await _context.WorkerExecutionLogs
|
||||
.Where(log => log.Status == WorkerExecutionStatus.Success ||
|
||||
log.Status == WorkerExecutionStatus.Failed)
|
||||
.GroupBy(log => log.WeekDefinitionId)
|
||||
.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 currentDate = DateTime.Now;
|
||||
var currentWeekInfo = CreateWeekInfo(currentDate, currentWeekDef.Id, calculatedPools, executionLogs);
|
||||
|
||||
// هفتههای گذشته (12 هفته)
|
||||
var pastWeeks = new List<WeekInfoDto>();
|
||||
for (int i = 1; i <= request.PastWeeksCount; i++)
|
||||
{
|
||||
var pastDate = currentDate.AddDays(-7 * i);
|
||||
var weekNumber = GetWeekNumberUsingRepository(pastDate);
|
||||
pastWeeks.Add(CreateWeekInfo(pastDate, currentWeekDef.Id, calculatedPools, executionLogs));
|
||||
}
|
||||
|
||||
// هفتههای آینده (4 هفته)
|
||||
var futureWeeks = new List<WeekInfoDto>();
|
||||
for (int i = 1; i <= request.FutureWeeksCount; i++)
|
||||
{
|
||||
var futureDate = currentDate.AddDays(7 * i);
|
||||
var weekNumber = GetWeekNumberUsingRepository(futureDate);
|
||||
futureWeeks.Add(CreateWeekInfo(futureDate, currentWeekDef.Id, 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 string GetWeekNumberUsingRepository(DateTime date)
|
||||
{
|
||||
var weekDef = _weekDefinitionRepository.GetWeekByDate(date);
|
||||
if (weekDef != null)
|
||||
{
|
||||
return $"{weekDef.GregorianYear}-W{weekDef.GregorianWeekNumber:D2}";
|
||||
}
|
||||
|
||||
// Fallback to manual calculation if not in cache
|
||||
return _weekDefinitionRepository.CalculateGregorianWeekNumber(date);
|
||||
}
|
||||
|
||||
private WeekInfoDto CreateWeekInfo(
|
||||
DateTime date,
|
||||
long weekDefinitionId,
|
||||
List<Domain.Entities.Commission.WeeklyCommissionPool> calculatedPools,
|
||||
Dictionary<long, Domain.Entities.Commission.WorkerExecutionLog> executionLogs)
|
||||
{
|
||||
// Get the actual WeekDefinition for this date
|
||||
var weekDef = _weekDefinitionRepository.GetWeekByDate(date);
|
||||
var actualWeekDefId = weekDef?.Id ?? weekDefinitionId;
|
||||
|
||||
var (startDate, endDate) = GetWeekRange(date);
|
||||
var pool = calculatedPools.FirstOrDefault(p => p.WeekDefinitionId == actualWeekDefId);
|
||||
var log = executionLogs.GetValueOrDefault(actualWeekDefId);
|
||||
|
||||
var isCalculated = pool != null && pool.IsCalculated;
|
||||
//
|
||||
// // تبدیل تاریخ به شمسی برای نمایش
|
||||
// var persianCalendar = new PersianCalendar();
|
||||
// var startDatePersian = $"{persianCalendar.GetYear(startDate):D4}/{persianCalendar.GetMonth(startDate):D2}/{persianCalendar.GetDayOfMonth(startDate):D2}";
|
||||
// var endDatePersian = $"{persianCalendar.GetYear(endDate):D4}/{persianCalendar.GetMonth(endDate):D2}/{persianCalendar.GetDayOfMonth(endDate):D2}";
|
||||
|
||||
// استفاده از DisplayName از WeekDefinition
|
||||
var displayName = weekDef?.DisplayName ?? GetDisplayName(startDate);
|
||||
string displayText;
|
||||
|
||||
if (!string.IsNullOrEmpty(displayName))
|
||||
{
|
||||
// اگر DisplayName وجود داشت، از آن استفاده کن
|
||||
displayText = $"{displayName} ({startDate.MiladiToJalali()} - {endDate.MiladiToJalali()})";
|
||||
}
|
||||
else
|
||||
{
|
||||
// تبدیل weekNumber به شمسی فقط برای نمایش (fallback)
|
||||
var persianWeekNumber = _weekDefinitionRepository.GetWeekById(actualWeekDefId);
|
||||
displayText = $"{persianWeekNumber?.PersianWeekNumber} ({startDate.MiladiToJalali()} - {endDate.MiladiToJalali()})";
|
||||
}
|
||||
|
||||
if (isCalculated)
|
||||
{
|
||||
displayText += " ✅ محاسبه شده";
|
||||
}
|
||||
|
||||
return new WeekInfoDto
|
||||
{
|
||||
WeekDefinitionId = actualWeekDefId,
|
||||
DisplayName = weekDef?.DisplayName ?? _weekDefinitionRepository.GetWeekById(actualWeekDefId)?.DisplayName ?? "", // میلادی (برای API)
|
||||
StartDate = startDate,
|
||||
EndDate = endDate,
|
||||
IsCalculated = isCalculated,
|
||||
CalculatedAt = pool?.CalculatedAt,
|
||||
LastExecutionStatus = log?.Status.ToString(),
|
||||
TotalPoolAmount = pool?.TotalPoolAmount,
|
||||
EligibleUsersCount = pool?.UserCommissionPayouts?.Count ?? 0,
|
||||
DisplayText = displayText // شمسی (برای نمایش)
|
||||
};
|
||||
}
|
||||
|
||||
private (DateTime startDate, DateTime endDate) GetWeekRange(DateTime date)
|
||||
{
|
||||
// First try to get from repository
|
||||
var weekDef = _weekDefinitionRepository.GetWeekByDate(date);
|
||||
if (weekDef != null)
|
||||
{
|
||||
return (weekDef.StartDate, weekDef.EndDate);
|
||||
}
|
||||
|
||||
// Fallback: manual calculation
|
||||
var dayOfWeek = (int)date.DayOfWeek;
|
||||
// محاسبه تعداد روزهایی که باید عقب برویم تا به شنبه برسیم
|
||||
// شنبه = 6, یکشنبه = 0, دوشنبه = 1, ..., جمعه = 5
|
||||
var daysToSaturday = dayOfWeek == 6 ? 0 : (dayOfWeek + 1) % 7;
|
||||
|
||||
var startDate = date.Date.AddDays(-daysToSaturday);
|
||||
var endDate = startDate.AddDays(6).AddHours(23).AddMinutes(59).AddSeconds(59);
|
||||
|
||||
return (startDate, endDate);
|
||||
}
|
||||
|
||||
private string GetDisplayName(DateTime date)
|
||||
{
|
||||
var weekDef = _weekDefinitionRepository.GetWeekByDate(date);
|
||||
return weekDef?.DisplayName ?? "";
|
||||
}
|
||||
|
||||
// private static string ConvertWeekNumberToPersian(string gregorianWeekNumber, DateTime weekStartDate)
|
||||
// {
|
||||
// // ورودی: "2025-W48"
|
||||
// var persianCalendar = new PersianCalendar();
|
||||
// var persianYear = persianCalendar.GetYear(weekStartDate);
|
||||
//
|
||||
// // محاسبه شماره هفته شمسی
|
||||
// var jan1Persian = persianCalendar.ToDateTime(persianYear, 1, 1, 0, 0, 0, 0);
|
||||
//
|
||||
// // پیدا کردن اولین شنبه سال شمسی
|
||||
// var jan1DayOfWeek = (int)jan1Persian.DayOfWeek;
|
||||
// var daysToFirstSaturday = jan1DayOfWeek == 6 ? 0 : (6 - jan1DayOfWeek + 7) % 7;
|
||||
// var firstSaturday = jan1Persian.AddDays(daysToFirstSaturday);
|
||||
//
|
||||
// // محاسبه شماره هفته
|
||||
// int weekNum;
|
||||
// if (weekStartDate < firstSaturday)
|
||||
// {
|
||||
// weekNum = 1;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// var daysSinceFirstSaturday = (weekStartDate - firstSaturday).Days;
|
||||
// weekNum = (daysSinceFirstSaturday / 7) + 1;
|
||||
// }
|
||||
//
|
||||
// return $"{persianYear}-W{weekNum:D2}";
|
||||
// }
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
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>شناسه یکتای هفته</summary>
|
||||
public required long WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>شماره هفته (YYYY-Www)</summary>
|
||||
public required string DisplayName { 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; }
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@ public record GetCommissionPayoutHistoryQuery : IRequest<GetCommissionPayoutHist
|
||||
/// <summary>
|
||||
/// شماره هفته (اختیاری)
|
||||
/// </summary>
|
||||
public string? WeekNumber { get; init; }
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// مرتبسازی
|
||||
|
||||
+17
-5
@@ -3,15 +3,20 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetCommissionPayoutHi
|
||||
public class GetCommissionPayoutHistoryQueryHandler : IRequestHandler<GetCommissionPayoutHistoryQuery, GetCommissionPayoutHistoryResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
|
||||
public GetCommissionPayoutHistoryQueryHandler(IApplicationDbContext context)
|
||||
public GetCommissionPayoutHistoryQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekDefinitionRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
}
|
||||
|
||||
public async Task<GetCommissionPayoutHistoryResponseDto> Handle(GetCommissionPayoutHistoryQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.CommissionPayoutHistories
|
||||
.Include(i=>i.WeekDefinition)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
@@ -26,12 +31,12 @@ public class GetCommissionPayoutHistoryQueryHandler : IRequestHandler<GetCommiss
|
||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(request.WeekNumber))
|
||||
if (request.WeekDefinitionId!=null)
|
||||
{
|
||||
query = query.Where(x => x.WeekNumber == request.WeekNumber);
|
||||
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId);
|
||||
}
|
||||
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "Created");
|
||||
|
||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||
|
||||
@@ -42,7 +47,8 @@ public class GetCommissionPayoutHistoryQueryHandler : IRequestHandler<GetCommiss
|
||||
Id = x.Id,
|
||||
UserCommissionPayoutId = x.UserCommissionPayoutId,
|
||||
UserId = x.UserId,
|
||||
WeekNumber = x.WeekNumber,
|
||||
WeekDefinitionId = x.WeekDefinitionId,
|
||||
WeekDisplayName=x.WeekDefinition!.DisplayName,
|
||||
AmountBefore = x.AmountBefore,
|
||||
AmountAfter = x.AmountAfter,
|
||||
OldStatus = x.OldStatus,
|
||||
@@ -54,6 +60,12 @@ public class GetCommissionPayoutHistoryQueryHandler : IRequestHandler<GetCommiss
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// // Populate WeekDisplayName from cache
|
||||
// foreach (var model in models)
|
||||
// {
|
||||
// model.WeekDisplayName = _weekDefinitionRepository.GetDisplayNameByGregorianWeekNumber(model.WeekNumber);
|
||||
// }
|
||||
|
||||
return new GetCommissionPayoutHistoryResponseDto
|
||||
{
|
||||
MetaData = meta,
|
||||
|
||||
+4
-5
@@ -13,11 +13,10 @@ public class GetCommissionPayoutHistoryQueryValidator : AbstractValidator<GetCom
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه کاربر معتبر نیست")
|
||||
.When(x => x.UserId.HasValue);
|
||||
|
||||
RuleFor(x => x.WeekNumber)
|
||||
.Matches(@"^\d{4}-W\d{2}$")
|
||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد")
|
||||
.When(x => !string.IsNullOrEmpty(x.WeekNumber));
|
||||
RuleFor(x => x.WeekDefinitionId)
|
||||
.NotNull()
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شماره هفته نمیتواند خالی باشد");
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@ public class GetCommissionPayoutHistoryResponseModel
|
||||
public long Id { get; set; }
|
||||
public long UserCommissionPayoutId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string WeekNumber { get; set; } = string.Empty;
|
||||
public long WeekDefinitionId { get; set; }
|
||||
public string WeekDisplayName { get; set; } = string.Empty;
|
||||
public long AmountBefore { get; set; }
|
||||
public long AmountAfter { get; set; }
|
||||
public CommissionPayoutStatus? OldStatus { get; set; }
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ public record GetUserCommissionPayoutsQuery : IRequest<GetUserCommissionPayoutsR
|
||||
/// <summary>
|
||||
/// شماره هفته (اختیاری)
|
||||
/// </summary>
|
||||
public string? WeekNumber { get; init; }
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// مرتبسازی
|
||||
|
||||
+20
-5
@@ -3,15 +3,21 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayo
|
||||
public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommissionPayoutsQuery, GetUserCommissionPayoutsResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
|
||||
public GetUserCommissionPayoutsQueryHandler(IApplicationDbContext context)
|
||||
public GetUserCommissionPayoutsQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekDefinitionRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
}
|
||||
|
||||
public async Task<GetUserCommissionPayoutsResponseDto> Handle(GetUserCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.UserCommissionPayouts
|
||||
.Include(x => x.WeekDefinition)
|
||||
.Include(x => x.User)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
@@ -26,12 +32,12 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
||||
query = query.Where(x => x.Status == request.Status.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(request.WeekNumber))
|
||||
if (request.WeekDefinitionId!=null)
|
||||
{
|
||||
query = query.Where(x => x.WeekNumber == request.WeekNumber);
|
||||
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId);
|
||||
}
|
||||
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "Created");
|
||||
|
||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||
|
||||
@@ -41,7 +47,10 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
||||
{
|
||||
Id = x.Id,
|
||||
UserId = x.UserId,
|
||||
WeekNumber = x.WeekNumber,
|
||||
FirstName = x.User.FirstName,
|
||||
LastName = x.User.LastName,
|
||||
WeekDefinitionId = x.WeekDefinitionId,
|
||||
WeekDisplayName = x.WeekDefinition != null ? x.WeekDefinition.DisplayName : "",
|
||||
WeeklyPoolId = x.WeeklyPoolId,
|
||||
BalancesEarned = x.BalancesEarned,
|
||||
ValuePerBalance = x.ValuePerBalance,
|
||||
@@ -55,6 +64,12 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// // Populate WeekDisplayName from cache
|
||||
// foreach (var model in models)
|
||||
// {
|
||||
// model.WeekDisplayName = _weekDefinitionRepository.GetDisplayNameByGregorianWeekNumber(model.WeekNumber);
|
||||
// }
|
||||
|
||||
return new GetUserCommissionPayoutsResponseDto
|
||||
{
|
||||
MetaData = meta,
|
||||
|
||||
+4
-4
@@ -14,10 +14,10 @@ public class GetUserCommissionPayoutsQueryValidator : AbstractValidator<GetUserC
|
||||
.WithMessage("وضعیت معتبر نیست")
|
||||
.When(x => x.Status.HasValue);
|
||||
|
||||
RuleFor(x => x.WeekNumber)
|
||||
.Matches(@"^\d{4}-W\d{2}$")
|
||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد")
|
||||
.When(x => !string.IsNullOrEmpty(x.WeekNumber));
|
||||
RuleFor(x => x.WeekDefinitionId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شماره هفته نمیتواند صفر باشد")
|
||||
.When(x => x.WeekDefinitionId.HasValue);
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
|
||||
+4
-1
@@ -10,7 +10,10 @@ public class GetUserCommissionPayoutsResponseModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string WeekNumber { get; set; } = string.Empty;
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
public long WeekDefinitionId { get; set; }
|
||||
public string WeekDisplayName { get; set; } = string.Empty;
|
||||
public long WeeklyPoolId { get; set; }
|
||||
public long BalancesEarned { get; set; }
|
||||
public decimal ValuePerBalance { get; set; }
|
||||
|
||||
+3
-2
@@ -11,9 +11,10 @@ public record GetUserWeeklyBalancesQuery : IRequest<GetUserWeeklyBalancesRespons
|
||||
public long? UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// شماره هفته (اختیاری)
|
||||
/// شناسه تعریف هفته (اختیاری) - روش ترجیحی
|
||||
/// </summary>
|
||||
public string? WeekNumber { get; init; }
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// فقط موارد Expired نشده؟
|
||||
|
||||
+14
-5
@@ -3,15 +3,20 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances
|
||||
public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBalancesQuery, GetUserWeeklyBalancesResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
|
||||
public GetUserWeeklyBalancesQueryHandler(IApplicationDbContext context)
|
||||
public GetUserWeeklyBalancesQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekDefinitionRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
}
|
||||
|
||||
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetUserWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.NetworkWeeklyBalances
|
||||
.Include(x => x.WeekDefinition)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
@@ -21,17 +26,20 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(request.WeekNumber))
|
||||
// فیلتر بر اساس WeekDefinitionId (روش ترجیحی)
|
||||
if (request.WeekDefinitionId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.WeekNumber == request.WeekNumber);
|
||||
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId.Value);
|
||||
}
|
||||
|
||||
|
||||
if (request.OnlyActive.HasValue && request.OnlyActive.Value)
|
||||
{
|
||||
query = query.Where(x => !x.IsExpired);
|
||||
}
|
||||
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "-WeekNumber");
|
||||
// مرتبسازی بر اساس WeekDefinitionId (نزولی = جدیدترین اول)
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "-WeekDefinitionId");
|
||||
|
||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||
|
||||
@@ -41,7 +49,8 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
||||
{
|
||||
Id = x.Id,
|
||||
UserId = x.UserId,
|
||||
WeekNumber = x.WeekNumber,
|
||||
WeekDefinitionId = x.WeekDefinitionId,
|
||||
WeekDisplayName = x.WeekDefinition != null ? x.WeekDefinition.DisplayName : "",
|
||||
LeftLegBalances = x.LeftLegBalances,
|
||||
RightLegBalances = x.RightLegBalances,
|
||||
TotalBalances = x.TotalBalances,
|
||||
|
||||
+4
-4
@@ -9,10 +9,10 @@ public class GetUserWeeklyBalancesQueryValidator : AbstractValidator<GetUserWeek
|
||||
.WithMessage("شناسه کاربر معتبر نیست")
|
||||
.When(x => x.UserId.HasValue);
|
||||
|
||||
RuleFor(x => x.WeekNumber)
|
||||
.Matches(@"^\d{4}-W\d{2}$")
|
||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد")
|
||||
.When(x => !string.IsNullOrEmpty(x.WeekNumber));
|
||||
RuleFor(x => x.WeekDefinitionId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شماره هفته نمیتواند صفر باشد")
|
||||
.When(x => x.WeekDefinitionId.HasValue);
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
|
||||
+3
-1
@@ -10,7 +10,9 @@ public class GetUserWeeklyBalancesResponseModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string WeekNumber { get; set; } = string.Empty;
|
||||
public long WeekDefinitionId { get; set; }
|
||||
|
||||
public string WeekDisplayName { get; set; } = string.Empty;
|
||||
public int LeftLegBalances { get; set; }
|
||||
public int RightLegBalances { get; set; }
|
||||
public int TotalBalances { get; set; }
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions;
|
||||
|
||||
/// <summary>
|
||||
/// Query برای دریافت لیست هفتهها از کش (برای dropdown)
|
||||
/// </summary>
|
||||
public record GetWeekDefinitionsQuery : IRequest<GetWeekDefinitionsResponseDto>
|
||||
{
|
||||
//موقعیت صفحه بندی
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
|
||||
//مرتب سازی بر اساس
|
||||
public string? SortBy { get; init; }
|
||||
|
||||
//فیلتر
|
||||
public GetWeekDefinitionsFilter? Filter { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// فیلترهای جستجوی هفتهها
|
||||
/// </summary>
|
||||
public class GetWeekDefinitionsFilter
|
||||
{
|
||||
//جستجوی متنی روی DisplayName (مثل "هفته یک")
|
||||
public string? SearchText { get; set; }
|
||||
|
||||
//شماره ترتیب هفته
|
||||
public int? WeekOrder { get; set; }
|
||||
|
||||
//شماره هفته میلادی (2025-W46)
|
||||
public string? GregorianWeekNumber { get; set; }
|
||||
|
||||
//شماره هفته شمسی (1404-W35)
|
||||
public string? PersianWeekNumber { get; set; }
|
||||
|
||||
//سال میلادی
|
||||
public int? GregorianYear { get; set; }
|
||||
|
||||
//سال شمسی
|
||||
public int? PersianYear { get; set; }
|
||||
|
||||
//فقط هفتههای فعال
|
||||
public bool? IsActive { get; set; }
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions;
|
||||
|
||||
/// <summary>
|
||||
/// Handler برای دریافت لیست هفتهها از کش
|
||||
/// </summary>
|
||||
public class GetWeekDefinitionsQueryHandler : IRequestHandler<GetWeekDefinitionsQuery, GetWeekDefinitionsResponseDto>
|
||||
{
|
||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
|
||||
public GetWeekDefinitionsQueryHandler(IWeekDefinitionRepository weekDefinitionRepository)
|
||||
{
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
}
|
||||
|
||||
public Task<GetWeekDefinitionsResponseDto> Handle(
|
||||
GetWeekDefinitionsQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// گرفتن همه هفتهها از کش
|
||||
var allWeeks = _weekDefinitionRepository.GetAllWeeks();
|
||||
|
||||
// اعمال فیلترها
|
||||
var filteredWeeks = allWeeks.AsEnumerable();
|
||||
|
||||
if (request.Filter != null)
|
||||
{
|
||||
var filter = request.Filter;
|
||||
|
||||
// جستجوی متنی روی DisplayName
|
||||
if (!string.IsNullOrWhiteSpace(filter.SearchText))
|
||||
{
|
||||
filteredWeeks = filteredWeeks.Where(w =>
|
||||
w.DisplayName.Contains(filter.SearchText, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// فیلتر بر اساس WeekOrder
|
||||
if (filter.WeekOrder.HasValue)
|
||||
{
|
||||
filteredWeeks = filteredWeeks.Where(w => w.WeekOrder == filter.WeekOrder.Value);
|
||||
}
|
||||
|
||||
// فیلتر بر اساس GregorianWeekNumber
|
||||
if (!string.IsNullOrWhiteSpace(filter.GregorianWeekNumber))
|
||||
{
|
||||
filteredWeeks = filteredWeeks.Where(w =>
|
||||
w.GregorianWeekNumber == filter.GregorianWeekNumber);
|
||||
}
|
||||
|
||||
// فیلتر بر اساس PersianWeekNumber
|
||||
if (!string.IsNullOrWhiteSpace(filter.PersianWeekNumber))
|
||||
{
|
||||
filteredWeeks = filteredWeeks.Where(w =>
|
||||
w.PersianWeekNumber == filter.PersianWeekNumber);
|
||||
}
|
||||
|
||||
// فیلتر بر اساس سال میلادی
|
||||
if (filter.GregorianYear.HasValue)
|
||||
{
|
||||
filteredWeeks = filteredWeeks.Where(w => w.GregorianYear == filter.GregorianYear.Value);
|
||||
}
|
||||
|
||||
// فیلتر بر اساس سال شمسی
|
||||
if (filter.PersianYear.HasValue)
|
||||
{
|
||||
filteredWeeks = filteredWeeks.Where(w => w.PersianYear == filter.PersianYear.Value);
|
||||
}
|
||||
|
||||
// فیلتر بر اساس فعال بودن
|
||||
if (filter.IsActive.HasValue)
|
||||
{
|
||||
filteredWeeks = filteredWeeks.Where(w => w.IsActive == filter.IsActive.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// مرتبسازی
|
||||
filteredWeeks = request.SortBy?.ToLower() switch
|
||||
{
|
||||
"weekorder_desc" => filteredWeeks.OrderByDescending(w => w.WeekOrder),
|
||||
"displayname" => filteredWeeks.OrderBy(w => w.DisplayName),
|
||||
"displayname_desc" => filteredWeeks.OrderByDescending(w => w.DisplayName),
|
||||
"startdate" => filteredWeeks.OrderBy(w => w.StartDate),
|
||||
"startdate_desc" => filteredWeeks.OrderByDescending(w => w.StartDate),
|
||||
_ => filteredWeeks.OrderBy(w => w.WeekOrder) // پیشفرض: weekorder
|
||||
};
|
||||
|
||||
// تعداد کل قبل از صفحهبندی
|
||||
var weeksList = filteredWeeks.ToList();
|
||||
var totalCount = weeksList.Count;
|
||||
|
||||
// صفحهبندی
|
||||
if (request.PaginationState != null)
|
||||
{
|
||||
var pageNumber = request.PaginationState.PageNumber > 0 ? request.PaginationState.PageNumber : 1;
|
||||
var pageSize = request.PaginationState.PageSize > 0 ? request.PaginationState.PageSize : 20;
|
||||
|
||||
weeksList = weeksList
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// گرفتن هفته جاری برای مشخص کردن IsCurrentWeek
|
||||
var currentWeek = _weekDefinitionRepository.GetCurrentWeek();
|
||||
|
||||
// تبدیل به DTO
|
||||
var weekDtos = weeksList.Select(w => new WeekDefinitionItemDto
|
||||
{
|
||||
WeekOrder = w.WeekOrder,
|
||||
DisplayName = w.DisplayName,
|
||||
GregorianWeekNumber = w.GregorianWeekNumber,
|
||||
PersianWeekNumber = w.PersianWeekNumber,
|
||||
StartDate = w.StartDate,
|
||||
EndDate = w.EndDate,
|
||||
GregorianYear = w.GregorianYear,
|
||||
PersianYear = w.PersianYear,
|
||||
IsActive = w.IsActive,
|
||||
IsCurrentWeek = currentWeek != null && w.WeekOrder == currentWeek.WeekOrder
|
||||
}).ToList();
|
||||
|
||||
var response = new GetWeekDefinitionsResponseDto
|
||||
{
|
||||
Data = weekDtos,
|
||||
TotalCount = totalCount
|
||||
};
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions;
|
||||
|
||||
/// <summary>
|
||||
/// DTO برای پاسخ لیست هفتهها
|
||||
/// </summary>
|
||||
public class GetWeekDefinitionsResponseDto
|
||||
{
|
||||
//لیست هفتهها
|
||||
public List<WeekDefinitionItemDto> Data { get; set; } = new();
|
||||
|
||||
//تعداد کل
|
||||
public int TotalCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// آیتم هفته برای dropdown
|
||||
/// </summary>
|
||||
public class WeekDefinitionItemDto
|
||||
{
|
||||
//شماره ترتیب هفته (1, 2, 3, ...)
|
||||
public int WeekOrder { get; set; }
|
||||
|
||||
//نام نمایشی هفته (هفته یکم، هفته دوم، ...)
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
//شماره هفته میلادی (2025-W46)
|
||||
public string GregorianWeekNumber { get; set; } = string.Empty;
|
||||
|
||||
//شماره هفته شمسی (1404-W35)
|
||||
public string PersianWeekNumber { get; set; } = string.Empty;
|
||||
|
||||
//تاریخ شروع هفته
|
||||
public DateTime StartDate { get; set; }
|
||||
|
||||
//تاریخ پایان هفته
|
||||
public DateTime EndDate { get; set; }
|
||||
|
||||
//سال میلادی
|
||||
public int GregorianYear { get; set; }
|
||||
|
||||
//سال شمسی
|
||||
public int PersianYear { get; set; }
|
||||
|
||||
//فعال بودن
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
//آیا هفته جاری است؟
|
||||
public bool IsCurrentWeek { get; set; }
|
||||
}
|
||||
+1
-1
@@ -8,5 +8,5 @@ public record GetWeeklyCommissionPoolQuery : IRequest<WeeklyCommissionPoolDto?>
|
||||
/// <summary>
|
||||
/// شماره هفته
|
||||
/// </summary>
|
||||
public string WeekNumber { get; init; } = string.Empty;
|
||||
public long WeekDefinitionId { get; init; }
|
||||
}
|
||||
|
||||
+16
-5
@@ -3,21 +3,27 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWeeklyCommissionPo
|
||||
public class GetWeeklyCommissionPoolQueryHandler : IRequestHandler<GetWeeklyCommissionPoolQuery, WeeklyCommissionPoolDto?>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
|
||||
public GetWeeklyCommissionPoolQueryHandler(IApplicationDbContext context)
|
||||
public GetWeeklyCommissionPoolQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekDefinitionRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
}
|
||||
|
||||
public async Task<WeeklyCommissionPoolDto?> Handle(GetWeeklyCommissionPoolQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var pool = await _context.WeeklyCommissionPools
|
||||
var pool = await _context.WeeklyCommissionPools
|
||||
.Include(x => x.WeekDefinition)
|
||||
.AsNoTracking()
|
||||
.Where(x => x.WeekNumber == request.WeekNumber)
|
||||
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId)
|
||||
.Select(x => new WeeklyCommissionPoolDto
|
||||
{
|
||||
Id = x.Id,
|
||||
WeekNumber = x.WeekNumber,
|
||||
WeekDefinitionId = x.WeekDefinitionId,
|
||||
WeekDisplayName=x.WeekDefinition.DisplayName,
|
||||
TotalPoolAmount = x.TotalPoolAmount,
|
||||
TotalBalances = x.TotalBalances,
|
||||
ValuePerBalance = x.ValuePerBalance,
|
||||
@@ -27,6 +33,11 @@ public class GetWeeklyCommissionPoolQueryHandler : IRequestHandler<GetWeeklyComm
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return pool;
|
||||
// if (pool != null)
|
||||
// {
|
||||
// pool.WeekDisplayName = _weekDefinitionRepository.GetDisplayNameByGregorianWeekNumber(pool.WeekNumber);
|
||||
// }
|
||||
|
||||
return pool ?? new WeeklyCommissionPoolDto();
|
||||
}
|
||||
}
|
||||
|
||||
+3
-5
@@ -4,11 +4,9 @@ public class GetWeeklyCommissionPoolQueryValidator : AbstractValidator<GetWeekly
|
||||
{
|
||||
public GetWeeklyCommissionPoolQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.WeekNumber)
|
||||
.NotEmpty()
|
||||
.WithMessage("شماره هفته نمیتواند خالی باشد")
|
||||
.Matches(@"^\d{4}-W\d{2}$")
|
||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد");
|
||||
RuleFor(x => x.WeekDefinitionId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شماره هفته نمیتواند صفر باشد");
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWeeklyCommissionPo
|
||||
public class WeeklyCommissionPoolDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string WeekNumber { get; set; } = string.Empty;
|
||||
public long WeekDefinitionId { get; set; }
|
||||
public string WeekDisplayName { get; set; } = string.Empty;
|
||||
public long TotalPoolAmount { get; set; }
|
||||
public long TotalBalances { get; set; }
|
||||
public decimal ValuePerBalance { get; set; }
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ public class GetWithdrawalReportsQueryHandler : IRequestHandler<GetWithdrawalRep
|
||||
public async Task<WithdrawalReportsDto> Handle(GetWithdrawalReportsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// تعیین بازه زمانی پیشفرض (30 روز گذشته)
|
||||
var endDate = request.EndDate ?? DateTime.UtcNow;
|
||||
var endDate = request.EndDate ?? DateTime.Now;
|
||||
var startDate = request.StartDate ?? endDate.AddDays(-30);
|
||||
|
||||
// Query پایه
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ public class GetWithdrawalRequestsQuery : IRequest<GetWithdrawalRequestsResponse
|
||||
{
|
||||
public int? Status { get; set; } // CommissionPayoutStatus enum
|
||||
public long? UserId { get; set; }
|
||||
public string? WeekNumber { get; set; }
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
public string? IbanNumber { get; set; }
|
||||
public PaginationState? PaginationState { get; set; }
|
||||
public string? SortBy { get; set; }
|
||||
|
||||
+11
-5
@@ -3,10 +3,14 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalRequests
|
||||
public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRequestsQuery, GetWithdrawalRequestsResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
|
||||
public GetWithdrawalRequestsQueryHandler(IApplicationDbContext context)
|
||||
public GetWithdrawalRequestsQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekDefinitionRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
}
|
||||
|
||||
public async Task<GetWithdrawalRequestsResponseDto> Handle(GetWithdrawalRequestsQuery request, CancellationToken cancellationToken)
|
||||
@@ -14,6 +18,7 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
|
||||
var query = _context.UserCommissionPayouts
|
||||
.AsNoTracking()
|
||||
.Include(x => x.User)
|
||||
.Include(x => x.WeekDefinition)
|
||||
.Where(x => x.WithdrawalMethod != null) // Only requests with withdrawal method
|
||||
.AsQueryable();
|
||||
|
||||
@@ -28,9 +33,9 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
|
||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(request.WeekNumber))
|
||||
if (request.WeekDefinitionId!= null && request.WeekDefinitionId >0)
|
||||
{
|
||||
query = query.Where(x => x.WeekNumber == request.WeekNumber);
|
||||
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.IbanNumber))
|
||||
@@ -38,7 +43,7 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
|
||||
query = query.Where(x => x.IbanNumber != null && x.IbanNumber.Contains(request.IbanNumber));
|
||||
}
|
||||
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "Created");
|
||||
|
||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||
|
||||
@@ -51,7 +56,8 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
|
||||
Id = x.Id,
|
||||
UserId = x.UserId,
|
||||
UserName = x.User != null ? (x.User.FirstName + " " + x.User.LastName).Trim() : x.User?.Mobile ?? "N/A",
|
||||
WeekNumber = x.WeekNumber,
|
||||
WeekDefinitionId = x.WeekDefinitionId,
|
||||
WeekDisplayName =x.WeekDefinition.DisplayName,
|
||||
Amount = x.TotalAmount,
|
||||
Status = (int)x.Status,
|
||||
WithdrawalMethod = x.WithdrawalMethod.HasValue ? (int)x.WithdrawalMethod.Value : 0,
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@ public class WithdrawalRequestModel
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string WeekNumber { get; set; } = string.Empty;
|
||||
public long WeekDefinitionId { get; set; }
|
||||
public string WeekDisplayName { get; set; } = string.Empty;
|
||||
public long Amount { get; set; }
|
||||
public int Status { get; set; } // CommissionPayoutStatus enum
|
||||
public int? WithdrawalMethod { get; set; }
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerExecutionLog
|
||||
|
||||
public record GetWorkerExecutionLogsQuery : IRequest<GetWorkerExecutionLogsResponseDto>
|
||||
{
|
||||
public string? WeekNumber { get; init; }
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
public string? ExecutionId { get; init; }
|
||||
public bool? SuccessOnly { get; init; }
|
||||
public bool? FailedOnly { get; init; }
|
||||
|
||||
+18
-5
@@ -6,10 +6,14 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerExecutionLog
|
||||
public class GetWorkerExecutionLogsQueryHandler : IRequestHandler<GetWorkerExecutionLogsQuery, GetWorkerExecutionLogsResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
|
||||
public GetWorkerExecutionLogsQueryHandler(IApplicationDbContext context)
|
||||
public GetWorkerExecutionLogsQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekDefinitionRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
}
|
||||
|
||||
public async Task<GetWorkerExecutionLogsResponseDto> Handle(
|
||||
@@ -17,12 +21,14 @@ public class GetWorkerExecutionLogsQueryHandler : IRequestHandler<GetWorkerExecu
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Query from database
|
||||
var query = _context.WorkerExecutionLogs.AsQueryable();
|
||||
var query = _context.WorkerExecutionLogs
|
||||
.Include(i=> i.WeekDefinition)
|
||||
.AsQueryable();
|
||||
|
||||
// Apply filters
|
||||
if (!string.IsNullOrEmpty(request.WeekNumber))
|
||||
if (request.WeekDefinitionId!=null && request.WeekDefinitionId > 0)
|
||||
{
|
||||
query = query.Where(x => x.WeekNumber == request.WeekNumber);
|
||||
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId);
|
||||
}
|
||||
|
||||
if (request.SuccessOnly == true)
|
||||
@@ -49,7 +55,8 @@ public class GetWorkerExecutionLogsQueryHandler : IRequestHandler<GetWorkerExecu
|
||||
.Select(x => new WorkerExecutionLogModel
|
||||
{
|
||||
ExecutionId = x.ExecutionId.ToString(),
|
||||
WeekNumber = x.WeekNumber,
|
||||
WeekDefinitionId = x.WeekDefinitionId,
|
||||
WeekDisplayName = x.WeekDefinition.DisplayName,
|
||||
Step = "Full", // We only have full execution now
|
||||
Success = x.Status == Domain.Entities.Commission.WorkerExecutionStatus.Success ||
|
||||
x.Status == Domain.Entities.Commission.WorkerExecutionStatus.SuccessWithWarnings,
|
||||
@@ -62,6 +69,12 @@ public class GetWorkerExecutionLogsQueryHandler : IRequestHandler<GetWorkerExecu
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// // Populate WeekDisplayName from cache
|
||||
// foreach (var log in logs)
|
||||
// {
|
||||
// log.WeekDisplayName = _weekDefinitionRepository.GetDisplayNameByGregorianWeekNumber(log.WeekNumber);
|
||||
// }
|
||||
|
||||
return new GetWorkerExecutionLogsResponseDto
|
||||
{
|
||||
MetaData = new MetaData
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@ public class GetWorkerExecutionLogsResponseDto
|
||||
public class WorkerExecutionLogModel
|
||||
{
|
||||
public string ExecutionId { get; set; } = string.Empty;
|
||||
public string WeekNumber { get; set; } = string.Empty;
|
||||
public long WeekDefinitionId { get; set; }
|
||||
public string WeekDisplayName { get; set; } = string.Empty;
|
||||
public string Step { get; set; } = string.Empty; // "Balances" | "Pool" | "Payouts" | "Full"
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
+14
-4
@@ -5,10 +5,14 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerStatus;
|
||||
public class GetWorkerStatusQueryHandler : IRequestHandler<GetWorkerStatusQuery, GetWorkerStatusResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
|
||||
public GetWorkerStatusQueryHandler(IApplicationDbContext context)
|
||||
public GetWorkerStatusQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekDefinitionRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
}
|
||||
|
||||
public async Task<GetWorkerStatusResponseDto> Handle(
|
||||
@@ -19,16 +23,22 @@ public class GetWorkerStatusQueryHandler : IRequestHandler<GetWorkerStatusQuery,
|
||||
// فعلاً mock data برمیگرداند
|
||||
|
||||
await Task.CompletedTask;
|
||||
|
||||
var currentWeek = _weekDefinitionRepository.GetCurrentWeek();
|
||||
var currentWeekNumber = currentWeek != null
|
||||
? $"{currentWeek.GregorianYear}-W{currentWeek.GregorianWeekNumber:D2}"
|
||||
: null;
|
||||
|
||||
return new GetWorkerStatusResponseDto
|
||||
{
|
||||
IsRunning = false,
|
||||
IsEnabled = true,
|
||||
CurrentExecutionId = null,
|
||||
CurrentWeekNumber = null,
|
||||
CurrentWeekNumber = currentWeekNumber,
|
||||
CurrentWeekDisplayName = currentWeek?.DisplayName,
|
||||
CurrentStep = "Idle",
|
||||
LastRunAt = DateTime.UtcNow.AddHours(-24),
|
||||
NextScheduledRun = DateTime.UtcNow.AddDays(7),
|
||||
LastRunAt = DateTime.Now.AddHours(-24),
|
||||
NextScheduledRun = DateTime.Now.AddDays(7),
|
||||
TotalExecutions = 48,
|
||||
SuccessfulExecutions = 47,
|
||||
FailedExecutions = 1
|
||||
|
||||
+1
@@ -6,6 +6,7 @@ public class GetWorkerStatusResponseDto
|
||||
public bool IsEnabled { get; set; }
|
||||
public string? CurrentExecutionId { get; set; }
|
||||
public string? CurrentWeekNumber { get; set; }
|
||||
public string? CurrentWeekDisplayName { get; set; }
|
||||
public string? CurrentStep { get; set; } // "Balances" | "Pool" | "Payouts" | "Idle"
|
||||
public DateTime? LastRunAt { get; set; }
|
||||
public DateTime? NextScheduledRun { get; set; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using CMSMicroservice.Domain.Entities.Payment;
|
||||
using CMSMicroservice.Domain.Entities.Order;
|
||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||
using CMSMicroservice.Domain.Entities.Geography;
|
||||
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
@@ -39,6 +40,7 @@ public interface IApplicationDbContext
|
||||
DbSet<UserClubFeature> UserClubFeatures { get; }
|
||||
DbSet<NetworkWeeklyBalance> NetworkWeeklyBalances { get; }
|
||||
DbSet<NetworkMembershipHistory> NetworkMembershipHistories { get; }
|
||||
DbSet<WeekDefinition> WeekDefinitions { get; }
|
||||
DbSet<WeeklyCommissionPool> WeeklyCommissionPools { get; }
|
||||
DbSet<UserCommissionPayout> UserCommissionPayouts { get; }
|
||||
DbSet<CommissionPayoutHistory> CommissionPayoutHistories { get; }
|
||||
@@ -53,5 +55,10 @@ public interface IApplicationDbContext
|
||||
DbSet<DiscountOrder> DiscountOrders { get; }
|
||||
DbSet<DiscountOrderDetail> DiscountOrderDetails { get; }
|
||||
|
||||
// ============= Geography =============
|
||||
DbSet<Country> Countries { get; }
|
||||
DbSet<State> States { get; }
|
||||
DbSet<City> Cities { get; }
|
||||
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// استراتژی محاسبه کمیسیون هفتگی
|
||||
/// دو پیادهسازی دارد: ORM و Stored Procedure
|
||||
/// </summary>
|
||||
public interface ICommissionCalculationStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// محاسبه تعادلهای هفتگی (مرحله 1)
|
||||
/// </summary>
|
||||
/// <param name="weekDefinitionId">شناسه هفته</param>
|
||||
/// <param name="forceRecalculate">محاسبه مجدد حتی اگر قبلاً محاسبه شده</param>
|
||||
/// <param name="cancellationToken">توکن لغو</param>
|
||||
/// <returns>تعداد رکوردهای محاسبه شده</returns>
|
||||
Task<int> CalculateWeeklyBalancesAsync(
|
||||
long weekDefinitionId,
|
||||
bool forceRecalculate,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه استخر کمیسیون و پرداختها (مرحله 2)
|
||||
/// </summary>
|
||||
/// <param name="weekDefinitionId">شناسه هفته</param>
|
||||
/// <param name="forceRecalculate">محاسبه مجدد حتی اگر قبلاً محاسبه شده</param>
|
||||
/// <param name="cancellationToken">توکن لغو</param>
|
||||
/// <returns>شناسه Pool ایجاد شده</returns>
|
||||
Task<long> CalculateWeeklyCommissionPoolAsync(
|
||||
long weekDefinitionId,
|
||||
bool forceRecalculate,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// نوع استراتژی محاسبه کمیسیون
|
||||
/// </summary>
|
||||
public enum CommissionCalculationStrategyType
|
||||
{
|
||||
/// <summary>
|
||||
/// استفاده از Entity Framework (ORM) - پیشفرض
|
||||
/// </summary>
|
||||
Orm = 0,
|
||||
|
||||
/// <summary>
|
||||
/// استفاده از Stored Procedure - بهینهتر برای دادههای زیاد
|
||||
/// </summary>
|
||||
StoredProcedure = 1
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Factory برای ایجاد Strategy مناسب براساس Config
|
||||
/// </summary>
|
||||
public interface ICommissionCalculationStrategyFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// ایجاد Strategy براساس تنظیمات سیستم
|
||||
/// </summary>
|
||||
/// <returns>استراتژی مناسب (ORM یا SP)</returns>
|
||||
Task<ICommissionCalculationStrategy> CreateStrategyAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// ایجاد Strategy مشخص
|
||||
/// </summary>
|
||||
/// <param name="strategyType">نوع استراتژی</param>
|
||||
/// <returns>استراتژی درخواستی</returns>
|
||||
ICommissionCalculationStrategy CreateStrategy(CommissionCalculationStrategyType strategyType);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for sending token-related notifications via SignalR
|
||||
/// </summary>
|
||||
public interface ITokenNotificationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Notify that a user's token has been revoked and they should refresh their token
|
||||
/// </summary>
|
||||
/// <param name="userId">The user ID whose token was revoked</param>
|
||||
/// <param name="reason">The reason for revocation</param>
|
||||
Task NotifyTokenRevokedAsync(long userId, string reason);
|
||||
|
||||
/// <summary>
|
||||
/// Notify that a user should refresh their token due to profile changes
|
||||
/// </summary>
|
||||
/// <param name="userId">The user ID whose profile changed</param>
|
||||
Task NotifyForceRefreshAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast a message to all connected clients
|
||||
/// </summary>
|
||||
/// <param name="message">The message to broadcast</param>
|
||||
Task BroadcastMessageAsync(string message);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// اینترفیس ریپازیتوری هفتهها با کش در حافظه
|
||||
/// این سرویس موقع استارت برنامه از دیتابیس لود میشود و در حافظه نگهداری میشود
|
||||
/// </summary>
|
||||
public interface IWeekDefinitionRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// گرفتن هفته جاری
|
||||
/// </summary>
|
||||
WeekDefinition? GetCurrentWeek();
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن هفته جاری (async برای سازگاری)
|
||||
/// </summary>
|
||||
Task<WeekDefinition?> GetCurrentWeekAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن هفته بعد
|
||||
/// </summary>
|
||||
WeekDefinition? GetNextWeek();
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن هفته بعد از هفته مشخص
|
||||
/// </summary>
|
||||
WeekDefinition? GetNextWeek(WeekDefinition currentWeek);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن هفته قبل
|
||||
/// </summary>
|
||||
WeekDefinition? GetPreviousWeek();
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن هفته قبل از هفته مشخص
|
||||
/// </summary>
|
||||
WeekDefinition? GetPreviousWeek(WeekDefinition currentWeek);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن هفته بر اساس تاریخ میلادی
|
||||
/// </summary>
|
||||
WeekDefinition? GetWeekByDate(DateTime date);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن هفته بر اساس شماره هفته میلادی (مثال: "2025-W46")
|
||||
/// </summary>
|
||||
WeekDefinition? GetWeekByGregorianWeekNumber(string gregorianWeekNumber);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن هفته بر اساس شماره هفته شمسی (مثال: "1404-W35")
|
||||
/// </summary>
|
||||
WeekDefinition? GetWeekByPersianWeekNumber(string persianWeekNumber);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن هفته بر اساس شماره ترتیبی
|
||||
/// </summary>
|
||||
WeekDefinition? GetWeekByOrder(int weekOrder);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن لیست همه هفتهها
|
||||
/// </summary>
|
||||
IReadOnlyList<WeekDefinition> GetAllWeeks();
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن هفتههای یک سال میلادی خاص
|
||||
/// </summary>
|
||||
IReadOnlyList<WeekDefinition> GetWeeksByGregorianYear(int year);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن هفتههای یک سال شمسی خاص
|
||||
/// </summary>
|
||||
IReadOnlyList<WeekDefinition> GetWeeksByPersianYear(int year);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن شماره هفته میلادی جاری (مثال: "2025-W51")
|
||||
/// </summary>
|
||||
string GetCurrentGregorianWeekNumber();
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن شماره هفته شمسی جاری (مثال: "1404-W39")
|
||||
/// </summary>
|
||||
string GetCurrentPersianWeekNumber();
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن نام نمایشی هفته جاری (مثال: "هفته ششم")
|
||||
/// </summary>
|
||||
string GetCurrentWeekDisplayName();
|
||||
|
||||
/// <summary>
|
||||
/// آیا تاریخ مشخص در هفته جاری است؟
|
||||
/// </summary>
|
||||
bool IsDateInCurrentWeek(DateTime date);
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه شماره هفته میلادی برای یک تاریخ (حتی اگر در کش نباشد)
|
||||
/// </summary>
|
||||
string CalculateGregorianWeekNumber(DateTime date);
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه شماره هفته شمسی برای یک تاریخ (حتی اگر در کش نباشد)
|
||||
/// </summary>
|
||||
string CalculatePersianWeekNumber(DateTime date);
|
||||
|
||||
/// <summary>
|
||||
/// بارگذاری مجدد کش از دیتابیس
|
||||
/// </summary>
|
||||
Task ReloadCacheAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن بازه تاریخی یک هفته بر اساس شماره هفته میلادی
|
||||
/// </summary>
|
||||
(DateTime startDate, DateTime endDate)? GetWeekDateRange(string gregorianWeekNumber);
|
||||
|
||||
(DateTime startDate, DateTime endDate)? GetWeekDateRange(long WeekDefinitionId);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن شماره هفته قبل از هفته مشخص
|
||||
/// </summary>
|
||||
string? GetPreviousWeekNumber(string gregorianWeekNumber);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن شماره هفته بعد از هفته مشخص
|
||||
/// </summary>
|
||||
string? GetNextWeekNumber(string gregorianWeekNumber);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن DisplayName یک هفته بر اساس شماره هفته میلادی (مثل "هفته یکم")
|
||||
/// </summary>
|
||||
string GetDisplayNameByGregorianWeekNumber(string gregorianWeekNumber);
|
||||
|
||||
/// <summary>
|
||||
/// جستجوی هفتهها بر اساس DisplayName (برای dropdown)
|
||||
/// اگر فیلتر خالی باشد همه هفتهها برگردانده میشود
|
||||
/// </summary>
|
||||
IEnumerable<WeekDefinition> SearchWeeksByDisplayName(string? filter = null);
|
||||
|
||||
/// <summary>
|
||||
/// گرفتن WeekDefinitionId از شماره هفته میلادی
|
||||
/// </summary>
|
||||
/// <param name="gregorianWeekNumber">شماره هفته میلادی (مثال: "2025-W46")</param>
|
||||
/// <returns>شناسه WeekDefinition یا null اگر پیدا نشد</returns>
|
||||
long? GetWeekDefinitionId(string gregorianWeekNumber);
|
||||
|
||||
WeekDefinition? GetWeekById(long weekDefinitionId);
|
||||
/// <summary>
|
||||
/// گرفتن شماره هفته میلادی از WeekDefinitionId
|
||||
/// </summary>
|
||||
/// <param name="weekDefinitionId">شناسه WeekDefinition</param>
|
||||
/// <returns>شماره هفته میلادی یا null اگر پیدا نشد</returns>
|
||||
string? GetGregorianWeekNumber(long weekDefinitionId);
|
||||
|
||||
/// <summary>
|
||||
/// آیا کش لود شده؟
|
||||
/// </summary>
|
||||
bool IsCacheLoaded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// تعداد هفتههای موجود در کش
|
||||
/// </summary>
|
||||
int CachedWeeksCount { get; }
|
||||
}
|
||||
+1
-1
@@ -22,7 +22,7 @@ public class GetConfigurationHistoryQueryHandler : IRequestHandler<GetConfigurat
|
||||
|
||||
var query = _context.SystemConfigurationHistories
|
||||
.Where(x => x.ConfigurationId == request.ConfigurationId)
|
||||
.ApplyOrder(sortBy: request.SortBy ?? "-Created") // پیشفرض: جدیدترین اول
|
||||
.ApplyOrder(sortBy: request.SortBy ?? "Created") // پیشفرض: جدیدترین اول
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
|
||||
+134
-3
@@ -10,6 +10,11 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IDayaLoanApiService _dayaApiService;
|
||||
private readonly ILogger<CheckDayaLoanStatusCommandHandler> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// مبلغ وام دایا - 56 میلیون ریال
|
||||
/// </summary>
|
||||
private const long DAYA_LOAN_AMOUNT = 56_000_000;
|
||||
|
||||
public CheckDayaLoanStatusCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
@@ -48,9 +53,24 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
|
||||
|
||||
if (existingContract != null)
|
||||
{
|
||||
existingContract.LastCheckDate = DateTime.UtcNow;
|
||||
var previousStatus = existingContract.Status;
|
||||
existingContract.LastCheckDate = DateTime.Now;
|
||||
existingContract.Status = dayaResult.Status;
|
||||
existingContract.ContractNumber = dayaResult.ContractNumber;
|
||||
|
||||
// بررسی تغییر وضعیت به PendingReceive یا Received
|
||||
// فقط اگر قبلاً پردازش نشده باشد
|
||||
if (!existingContract.IsProcessed &&
|
||||
!string.IsNullOrEmpty(dayaResult.ContractNumber) &&
|
||||
(dayaResult.Status == DayaLoanStatus.PendingReceive || dayaResult.Status == DayaLoanStatus.Received))
|
||||
{
|
||||
await ProcessDayaLoanReceivedAsync(existingContract.UserId, dayaResult.ContractNumber, cancellationToken);
|
||||
existingContract.IsProcessed = true;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Daya loan processed for User {UserId}, ContractNumber: {ContractNumber}, Amount: {Amount}",
|
||||
existingContract.UserId, dayaResult.ContractNumber, DAYA_LOAN_AMOUNT);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -59,14 +79,28 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
var isProcessed = false;
|
||||
|
||||
// اگر وضعیت PendingReceive یا Received بود و قرارداد دارد، فوری پردازش کن
|
||||
if (!string.IsNullOrEmpty(dayaResult.ContractNumber) &&
|
||||
(dayaResult.Status == DayaLoanStatus.PendingReceive || dayaResult.Status == DayaLoanStatus.Received))
|
||||
{
|
||||
await ProcessDayaLoanReceivedAsync(user.Id, dayaResult.ContractNumber, cancellationToken);
|
||||
isProcessed = true;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Daya loan processed for new contract - User {UserId}, ContractNumber: {ContractNumber}, Amount: {Amount}",
|
||||
user.Id, dayaResult.ContractNumber, DAYA_LOAN_AMOUNT);
|
||||
}
|
||||
|
||||
var newContract = new DayaLoanContract
|
||||
{
|
||||
UserId = user.Id,
|
||||
NationalCode = dayaResult.NationalCode,
|
||||
Status = dayaResult.Status,
|
||||
ContractNumber = dayaResult.ContractNumber,
|
||||
LastCheckDate = DateTime.UtcNow,
|
||||
IsProcessed = false
|
||||
LastCheckDate = DateTime.Now,
|
||||
IsProcessed = isProcessed
|
||||
};
|
||||
|
||||
await _context.DayaLoanContracts.AddAsync(newContract, cancellationToken);
|
||||
@@ -105,4 +139,101 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
|
||||
SuccessCount = results.Count(r => r.ContractNumber != null)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// پردازش دریافت وام دایا برای کاربر
|
||||
/// 1. شارژ کیف پول (Balance + DiscountBalance)
|
||||
/// 2. ثبت تراکنش
|
||||
/// 3. ثبت لاگ کیف پول
|
||||
/// 4. بهروزرسانی وضعیت کاربر
|
||||
/// </summary>
|
||||
private async Task ProcessDayaLoanReceivedAsync(long userId, string contractNumber, CancellationToken cancellationToken)
|
||||
{
|
||||
// پیدا کردن کاربر
|
||||
var user = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User {UserId} not found for Daya loan processing", userId);
|
||||
return;
|
||||
}
|
||||
|
||||
// بررسی اینکه قبلاً دریافت نکرده باشد
|
||||
if (user.HasReceivedDayaCredit)
|
||||
{
|
||||
_logger.LogWarning("User {UserId} has already received Daya credit", userId);
|
||||
return;
|
||||
}
|
||||
|
||||
// پیدا کردن یا ایجاد کیف پول کاربر
|
||||
var wallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == userId, cancellationToken);
|
||||
|
||||
if (wallet == null)
|
||||
{
|
||||
wallet = new UserWallet
|
||||
{
|
||||
UserId = userId,
|
||||
Balance = 0,
|
||||
NetworkBalance = 0,
|
||||
DiscountBalance = 0
|
||||
};
|
||||
await _context.UserWallets.AddAsync(wallet, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// 1. شارژ کیف پول - هم موجودی اصلی و هم موجودی تخفیف
|
||||
var previousBalance = wallet.Balance;
|
||||
var previousDiscountBalance = wallet.DiscountBalance;
|
||||
|
||||
wallet.Balance += DAYA_LOAN_AMOUNT;
|
||||
wallet.DiscountBalance += DAYA_LOAN_AMOUNT;
|
||||
|
||||
// 2. ثبت تراکنش - RefId = شماره قرارداد، Status = 0 (Success)، Type = 2 (DepositExternal1)
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Amount = DAYA_LOAN_AMOUNT,
|
||||
Description = $"شارژ کیف پول از وام دایا - قرارداد {contractNumber}",
|
||||
PaymentStatus = PaymentStatus.Success, // 0
|
||||
PaymentDate = DateTime.Now,
|
||||
RefId = contractNumber,
|
||||
Type = TransactionType.DepositExternal1 // 2
|
||||
};
|
||||
|
||||
await _context.Transactions.AddAsync(transaction, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 3. ثبت لاگ کیف پول با شناسه تراکنش
|
||||
var walletLog = new UserWalletChangeLog
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
ChangeValue = DAYA_LOAN_AMOUNT,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeDiscountValue = DAYA_LOAN_AMOUNT,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
};
|
||||
|
||||
await _context.UserWalletChangeLogs.AddAsync(walletLog, cancellationToken);
|
||||
|
||||
// 4. بهروزرسانی وضعیت کاربر
|
||||
user.HasReceivedDayaCredit = true;
|
||||
user.DayaCreditReceivedAt = DateTime.Now;
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan; // 1
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Daya loan fully processed for User {UserId}: " +
|
||||
"Wallet Balance {PreviousBalance} -> {NewBalance}, " +
|
||||
"DiscountBalance {PreviousDiscountBalance} -> {NewDiscountBalance}, " +
|
||||
"Transaction Id: {TransactionId}",
|
||||
userId, previousBalance, wallet.Balance,
|
||||
previousDiscountBalance, wallet.DiscountBalance,
|
||||
transaction.Id);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ public class ProcessDayaLoanApprovalCommandHandler : IRequestHandler<ProcessDaya
|
||||
Amount = request.WalletAmount + request.LockedWalletAmount + request.DiscountWalletAmount, // 168 میلیون
|
||||
Description = $"دریافت اعتبار دایا - قرارداد {request.ContractNumber}",
|
||||
PaymentStatus = PaymentStatus.Success,
|
||||
PaymentDate = DateTime.UtcNow,
|
||||
PaymentDate = DateTime.Now,
|
||||
RefId = request.ContractNumber, // شماره قرارداد دایا
|
||||
Type = TransactionType.DepositExternal1
|
||||
};
|
||||
@@ -114,7 +114,7 @@ public class ProcessDayaLoanApprovalCommandHandler : IRequestHandler<ProcessDaya
|
||||
|
||||
// بهروزرسانی وضعیت کاربر
|
||||
user.HasReceivedDayaCredit = true;
|
||||
user.DayaCreditReceivedAt = DateTime.UtcNow;
|
||||
user.DayaCreditReceivedAt = DateTime.Now;
|
||||
|
||||
// تنظیم نحوه خرید پکیج به DayaLoan
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan;
|
||||
@@ -139,7 +139,7 @@ public class ProcessDayaLoanApprovalCommandHandler : IRequestHandler<ProcessDaya
|
||||
PackageId = goldenPackage.Id,
|
||||
Amount = request.WalletAmount, // 56 میلیون
|
||||
PaymentStatus = PaymentStatus.Success,
|
||||
PaymentDate = DateTime.UtcNow,
|
||||
PaymentDate = DateTime.Now,
|
||||
DeliveryStatus = DeliveryStatus.None,
|
||||
UserAddressId = defaultAddress.Id,
|
||||
TransactionId = transaction.Id,
|
||||
|
||||
+2
-2
@@ -46,12 +46,12 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
|
||||
{
|
||||
// Update transaction
|
||||
transaction.PaymentStatus = PaymentStatus.Success;
|
||||
transaction.PaymentDate = DateTime.UtcNow;
|
||||
transaction.PaymentDate = DateTime.Now;
|
||||
transaction.RefId = request.RefId;
|
||||
|
||||
// Update order
|
||||
order.PaymentStatus = PaymentStatus.Success;
|
||||
order.PaymentDate = DateTime.UtcNow;
|
||||
order.PaymentDate = DateTime.Now;
|
||||
order.DeliveryStatus = DeliveryStatus.InTransit;
|
||||
|
||||
// Deduct discount balance from user wallet
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ public class CreateDiscountCategoryCommandHandler : IRequestHandler<CreateDiscou
|
||||
ParentCategoryId = request.ParentCategoryId,
|
||||
SortOrder = request.SortOrder,
|
||||
IsActive = request.IsActive,
|
||||
Created = DateTime.UtcNow
|
||||
Created = DateTime.Now
|
||||
};
|
||||
|
||||
_context.DiscountCategories.Add(category);
|
||||
|
||||
+2
-2
@@ -86,7 +86,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
|
||||
Amount = manualPayment.Amount,
|
||||
Description = $"پرداخت دستی - {manualPayment.Type} - {manualPayment.Description}",
|
||||
PaymentStatus = PaymentStatus.Success,
|
||||
PaymentDate = DateTime.UtcNow,
|
||||
PaymentDate = DateTime.Now,
|
||||
RefId = manualPayment.ReferenceNumber,
|
||||
Type = MapToTransactionType(manualPayment.Type)
|
||||
};
|
||||
@@ -219,7 +219,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
|
||||
// 7. بهروزرسانی ManualPayment
|
||||
manualPayment.Status = ManualPaymentStatus.Approved;
|
||||
manualPayment.ApprovedBy = approvedById;
|
||||
manualPayment.ApprovedAt = DateTime.UtcNow;
|
||||
manualPayment.ApprovedAt = DateTime.Now;
|
||||
manualPayment.TransactionId = transaction.Id;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
namespace CMSMicroservice.Application.ManualPaymentCQ.Commands.ProcessManualMembershipPayment;
|
||||
|
||||
public record ProcessManualMembershipPaymentCommand : IRequest<ProcessManualMembershipPaymentResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه کاربر
|
||||
/// </summary>
|
||||
public long UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// مبلغ پرداختی (ریال)
|
||||
/// </summary>
|
||||
public long Amount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// شماره مرجع تراکنش
|
||||
/// </summary>
|
||||
public string ReferenceNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// توضیحات (اختیاری)
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Entities.Payment;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.ManualPaymentCQ.Commands.ProcessManualMembershipPayment;
|
||||
|
||||
public class ProcessManualMembershipPaymentCommandHandler : IRequestHandler<ProcessManualMembershipPaymentCommand, ProcessManualMembershipPaymentResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
private readonly ILogger<ProcessManualMembershipPaymentCommandHandler> _logger;
|
||||
|
||||
public ProcessManualMembershipPaymentCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUser,
|
||||
ILogger<ProcessManualMembershipPaymentCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_currentUser = currentUser;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ProcessManualMembershipPaymentResponseDto> Handle(
|
||||
ProcessManualMembershipPaymentCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Processing manual membership payment for UserId: {UserId}, Amount: {Amount}",
|
||||
request.UserId, request.Amount);
|
||||
|
||||
// 1. دریافت شناسه ادمین از کاربر جاری
|
||||
var currentUserId = _currentUser.UserId;
|
||||
if (string.IsNullOrEmpty(currentUserId))
|
||||
{
|
||||
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
|
||||
}
|
||||
|
||||
if (!long.TryParse(currentUserId, out var adminUserId))
|
||||
{
|
||||
throw new UnauthorizedAccessException("شناسه کاربر نامعتبر است");
|
||||
}
|
||||
|
||||
// 2. بررسی وجود کاربر
|
||||
var user = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogError("User not found: {UserId}", request.UserId);
|
||||
throw new NotFoundException($"کاربر با شناسه {request.UserId} یافت نشد");
|
||||
}
|
||||
|
||||
// 3. ایجاد ManualPayment با وضعیت Approved
|
||||
var manualPayment = new ManualPayment
|
||||
{
|
||||
UserId = request.UserId,
|
||||
Amount = request.Amount,
|
||||
Type = ManualPaymentType.CashDeposit,
|
||||
Description = request.Description ?? "پرداخت دستی عضویت",
|
||||
ReferenceNumber = request.ReferenceNumber,
|
||||
Status = ManualPaymentStatus.Approved,
|
||||
RequestedBy = adminUserId,
|
||||
ApprovedBy = adminUserId,
|
||||
ApprovedAt = DateTime.Now
|
||||
};
|
||||
|
||||
_context.ManualPayments.Add(manualPayment);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 4. پیدا کردن یا ایجاد کیف پول
|
||||
var wallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == request.UserId, cancellationToken);
|
||||
|
||||
if (wallet == null)
|
||||
{
|
||||
_logger.LogError("Wallet not found for UserId: {UserId}", request.UserId);
|
||||
throw new NotFoundException($"کیف پول کاربر {request.UserId} یافت نشد");
|
||||
}
|
||||
|
||||
// 6. ثبت تراکنش
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Amount = request.Amount,
|
||||
Description = $"پرداخت دستی عضویت - {manualPayment.Description} - مرجع: {request.ReferenceNumber}",
|
||||
PaymentStatus = PaymentStatus.Success,
|
||||
PaymentDate = DateTime.Now,
|
||||
RefId = request.ReferenceNumber,
|
||||
Type = TransactionType.DepositExternal1
|
||||
};
|
||||
|
||||
_context.Transactions.Add(transaction);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 7. اعمال تغییرات بر کیف پول
|
||||
var oldBalance = wallet.Balance;
|
||||
var oldDiscountBalance = wallet.DiscountBalance;
|
||||
|
||||
wallet.Balance += request.Amount;
|
||||
wallet.DiscountBalance += request.Amount;
|
||||
|
||||
// 8. ثبت لاگ Balance
|
||||
var balanceLog = new UserWalletChangeLog
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
ChangeValue = request.Amount,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeDiscountValue = request.Amount,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
};
|
||||
await _context.UserWalletChangeLogs.AddAsync(balanceLog, cancellationToken);
|
||||
|
||||
|
||||
// 10. بهروزرسانی ManualPayment با TransactionId
|
||||
manualPayment.TransactionId = transaction.Id;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 11. پیدا کردن یا ایجاد آدرس پیشفرض کاربر
|
||||
var userAddress = await _context.UserAddresses
|
||||
.Where(a => a.UserId == request.UserId)
|
||||
.OrderByDescending(a => a.IsDefault)
|
||||
.ThenBy(a => a.Id)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (userAddress == null)
|
||||
{
|
||||
userAddress = new UserAddress
|
||||
{
|
||||
UserId = request.UserId,
|
||||
Title = "آدرس پیشفرض",
|
||||
Address = "پرداخت دستی عضویت - آدرس موقت",
|
||||
PostalCode = "0000000000",
|
||||
IsDefault = true,
|
||||
CityId = 1
|
||||
};
|
||||
await _context.UserAddresses.AddAsync(userAddress, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// 12. ثبت سفارش
|
||||
var order = new UserOrder
|
||||
{
|
||||
UserId = request.UserId,
|
||||
Amount = request.Amount,
|
||||
TransactionId = transaction.Id,
|
||||
PaymentStatus = PaymentStatus.Success,
|
||||
PaymentDate = DateTime.Now,
|
||||
PaymentMethod = PaymentMethod.Deposit,
|
||||
DeliveryStatus = DeliveryStatus.None,
|
||||
UserAddressId = userAddress.Id,
|
||||
DeliveryDescription = $"پرداخت دستی عضویت - مرجع: {request.ReferenceNumber}"
|
||||
};
|
||||
|
||||
_context.UserOrders.Add(order);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Manual membership payment processed successfully. UserId: {UserId}, Amount: {Amount}, ManualPaymentId: {ManualPaymentId}, TransactionId: {TransactionId}, OrderId: {OrderId}, AdminUserId: {AdminUserId}",
|
||||
request.UserId, request.Amount, manualPayment.Id, transaction.Id, order.Id, adminUserId);
|
||||
|
||||
return new ProcessManualMembershipPaymentResponseDto
|
||||
{
|
||||
TransactionId = transaction.Id,
|
||||
OrderId = order.Id,
|
||||
NewWalletBalance = wallet.Balance,
|
||||
Message = "پرداخت دستی با موفقیت ثبت شد"
|
||||
};
|
||||
}
|
||||
catch (Exception ex) when (ex is not NotFoundException)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"Error processing manual membership payment for UserId: {UserId}, Amount: {Amount}",
|
||||
request.UserId, request.Amount);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
namespace CMSMicroservice.Application.ManualPaymentCQ.Commands.ProcessManualMembershipPayment;
|
||||
|
||||
public class ProcessManualMembershipPaymentCommandValidator : AbstractValidator<ProcessManualMembershipPaymentCommand>
|
||||
{
|
||||
public ProcessManualMembershipPaymentCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.UserId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه کاربر معتبر نیست");
|
||||
|
||||
RuleFor(x => x.Amount)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("مبلغ باید بزرگتر از صفر باشد");
|
||||
|
||||
RuleFor(x => x.ReferenceNumber)
|
||||
.NotEmpty()
|
||||
.WithMessage("شماره مرجع الزامی است")
|
||||
.MaximumLength(50)
|
||||
.WithMessage("شماره مرجع نباید بیشتر از 50 کاراکتر باشد");
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
namespace CMSMicroservice.Application.ManualPaymentCQ.Commands.ProcessManualMembershipPayment;
|
||||
|
||||
public class ProcessManualMembershipPaymentResponseDto
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه تراکنش ثبت شده
|
||||
/// </summary>
|
||||
public long TransactionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه سفارش ثبت شده
|
||||
/// </summary>
|
||||
public long OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// موجودی جدید کیف پول
|
||||
/// </summary>
|
||||
public long NewWalletBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// پیام موفقیت
|
||||
/// </summary>
|
||||
public string Message { get; set; } = "پرداخت دستی با موفقیت ثبت شد";
|
||||
}
|
||||
+1
-1
@@ -71,7 +71,7 @@ public class RejectManualPaymentCommandHandler : IRequestHandler<RejectManualPay
|
||||
// 4. رد درخواست
|
||||
manualPayment.Status = ManualPaymentStatus.Rejected;
|
||||
manualPayment.ApprovedBy = rejectedById;
|
||||
manualPayment.ApprovedAt = DateTime.UtcNow;
|
||||
manualPayment.ApprovedAt = DateTime.Now;
|
||||
manualPayment.RejectionReason = request.RejectionReason;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ public record GetNetworkMembershipHistoryQuery : IRequest<GetNetworkMembershipHi
|
||||
public long? UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// مرتبسازی (پیشفرض: -Created)
|
||||
/// مرتبسازی (پیشفرض: Created)
|
||||
/// </summary>
|
||||
public string? SortBy { get; init; }
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ public class GetNetworkMembershipHistoryQueryHandler : IRequestHandler<GetNetwor
|
||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
||||
}
|
||||
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "Created");
|
||||
|
||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||
|
||||
|
||||
+8
-4
@@ -55,17 +55,21 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
|
||||
}
|
||||
|
||||
// Monthly growth (last 6 months) - using Created date
|
||||
var sixMonthsAgo = DateTime.UtcNow.AddMonths(-6);
|
||||
var monthlyGrowth = await _context.Users
|
||||
var sixMonthsAgo = DateTime.Now.AddMonths(-6);
|
||||
var monthlyGrowthRaw = await _context.Users
|
||||
.Where(x => x.NetworkParentId != null && x.Created >= sixMonthsAgo)
|
||||
.GroupBy(x => new { x.Created.Year, x.Created.Month })
|
||||
.Select(x => new { x.Created.Year, x.Created.Month })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var monthlyGrowth = monthlyGrowthRaw
|
||||
.GroupBy(x => new { x.Year, x.Month })
|
||||
.Select(g => new MonthlyGrowthModel
|
||||
{
|
||||
Month = $"{g.Key.Year}-{g.Key.Month:D2}",
|
||||
NewMembers = g.Count()
|
||||
})
|
||||
.OrderBy(x => x.Month)
|
||||
.ToListAsync(cancellationToken);
|
||||
.ToList();
|
||||
|
||||
// Top users by total children count
|
||||
var topUsers = await _context.Users
|
||||
|
||||
+10
@@ -14,4 +14,14 @@ public record GetNetworkTreeQuery : IRequest<NetworkTreeDto?>
|
||||
/// تعداد سطوح (Depth) که میخواهیم نمایش دهیم (پیشفرض: 3)
|
||||
/// </summary>
|
||||
public int MaxDepth { get; init; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// فیلتر براساس وضعیت فعال بودن باشگاه مشتریان
|
||||
/// </summary>
|
||||
public bool? IsClubActive { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فیلتر براساس شماره هفته فعالسازی (مثال: 2025-W05)
|
||||
/// </summary>
|
||||
public long? ActivationWeekDefinitionId { get; init; }
|
||||
}
|
||||
|
||||
+83
-17
@@ -3,10 +3,14 @@ namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree
|
||||
public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, NetworkTreeDto?>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
|
||||
public GetNetworkTreeQueryHandler(IApplicationDbContext context)
|
||||
public GetNetworkTreeQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekDefinitionRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
}
|
||||
|
||||
public async Task<NetworkTreeDto?> Handle(GetNetworkTreeQuery request, CancellationToken cancellationToken)
|
||||
@@ -20,13 +24,15 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
||||
return null;
|
||||
}
|
||||
|
||||
var tree = await BuildTree(rootUser.Id, request.MaxDepth, 0, cancellationToken);
|
||||
var tree = await BuildTree(rootUser.Id, request.MaxDepth, 0, cancellationToken, request);
|
||||
return tree;
|
||||
}
|
||||
|
||||
private async Task<NetworkTreeDto> BuildTree(long userId, int maxDepth, int currentDepth, CancellationToken cancellationToken)
|
||||
private async Task<NetworkTreeDto> BuildTree(long userId, int maxDepth, int currentDepth, CancellationToken cancellationToken, GetNetworkTreeQuery request)
|
||||
{
|
||||
// دریافت کاربر با اطلاعات باشگاه مشتریان
|
||||
var user = await _context.Users
|
||||
.Include(u => u.ClubMembership)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
||||
|
||||
@@ -35,6 +41,25 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
||||
throw new NotFoundException(nameof(User), userId);
|
||||
}
|
||||
|
||||
// محاسبه شماره هفته فعالسازی
|
||||
long? activationWeekDefinitionId = null;
|
||||
string? activationWeekDisplayName = null;
|
||||
bool isActivatedInTargetWeek = false;
|
||||
|
||||
if (user.ClubMembership?.ActivatedAt != null)
|
||||
{
|
||||
// activationWeekDefinitionId = CalculateWeekNumber(user.ClubMembership.ActivatedAt.Value);
|
||||
var week = _weekDefinitionRepository.GetWeekByDate(user.ClubMembership.ActivatedAt.Value);
|
||||
activationWeekDefinitionId = week.Id;
|
||||
activationWeekDisplayName = week.DisplayName;
|
||||
|
||||
// بررسی آیا در هفته هدف فعال شده است
|
||||
if (request.ActivationWeekDefinitionId!=null)
|
||||
{
|
||||
isActivatedInTargetWeek = activationWeekDefinitionId == request.ActivationWeekDefinitionId;
|
||||
}
|
||||
}
|
||||
|
||||
var node = new NetworkTreeDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
@@ -42,7 +67,13 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
LegPosition = user.LegPosition,
|
||||
CurrentDepth = currentDepth
|
||||
CurrentDepth = currentDepth,
|
||||
ClubActivatedAt = user.ClubMembership?.ActivatedAt,
|
||||
IsClubActive = user.ClubMembership?.IsActive ?? false,
|
||||
ActivationWeekDefinitionId = activationWeekDefinitionId,
|
||||
ActivationWeekDisplayName = activationWeekDisplayName,
|
||||
IsActivatedInTargetWeek = isActivatedInTargetWeek,
|
||||
UserCreated = user.Created
|
||||
};
|
||||
|
||||
// اگر به حداکثر عمق رسیدیم، دیگر فرزندان را نمیخوانیم
|
||||
@@ -51,28 +82,63 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
||||
return node;
|
||||
}
|
||||
|
||||
// پیدا کردن فرزند چپ
|
||||
var leftChild = await _context.Users
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Left,
|
||||
cancellationToken);
|
||||
// پیدا کردن فرزندان (چپ و راست)
|
||||
var children = await GetFilteredChildren(userId, request, cancellationToken);
|
||||
|
||||
var leftChild = children.FirstOrDefault(c => c.LegPosition == NetworkLeg.Left);
|
||||
if (leftChild != null)
|
||||
{
|
||||
node.LeftChild = await BuildTree(leftChild.Id, maxDepth, currentDepth + 1, cancellationToken);
|
||||
node.LeftChild = await BuildTree(leftChild.Id, maxDepth, currentDepth + 1, cancellationToken, request);
|
||||
}
|
||||
|
||||
// پیدا کردن فرزند راست
|
||||
var rightChild = await _context.Users
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Right,
|
||||
cancellationToken);
|
||||
|
||||
var rightChild = children.FirstOrDefault(c => c.LegPosition == NetworkLeg.Right);
|
||||
if (rightChild != null)
|
||||
{
|
||||
node.RightChild = await BuildTree(rightChild.Id, maxDepth, currentDepth + 1, cancellationToken);
|
||||
node.RightChild = await BuildTree(rightChild.Id, maxDepth, currentDepth + 1, cancellationToken, request);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت فرزندان با اعمال فیلترها
|
||||
/// </summary>
|
||||
private async Task<List<User>> GetFilteredChildren(long parentId, GetNetworkTreeQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.Users
|
||||
.Include(u => u.ClubMembership)
|
||||
.AsNoTracking()
|
||||
.Where(x => x.NetworkParentId == parentId);
|
||||
|
||||
// اعمال فیلتر IsClubActive
|
||||
if (request.IsClubActive.HasValue)
|
||||
{
|
||||
query = query.Where(u =>
|
||||
u.ClubMembership != null &&
|
||||
u.ClubMembership.IsDeleted == false &&
|
||||
u.ClubMembership.IsActive == request.IsClubActive.Value);
|
||||
}
|
||||
|
||||
return await query.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه شماره هفته از تاریخ
|
||||
/// </summary>
|
||||
// private long CalculateWeekNumber(DateTime date)
|
||||
// {
|
||||
// // First try to get from repository cache
|
||||
// var weekDef = _weekDefinitionRepository.GetWeekByDate(date);
|
||||
// if (weekDef != null)
|
||||
// {
|
||||
// return weekDef.Id;
|
||||
// }
|
||||
//
|
||||
// // Fallback: use repository's calculation method
|
||||
// // return _weekDefinitionRepository.CalculateGregorianWeekNumber(date);
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ public class GetNetworkTreeQueryValidator : AbstractValidator<GetNetworkTreeQuer
|
||||
.WithMessage("شناسه کاربر معتبر نیست");
|
||||
|
||||
RuleFor(x => x.MaxDepth)
|
||||
.InclusiveBetween(1, 10)
|
||||
.WithMessage("عمق درخت باید بین 1 تا 10 باشد");
|
||||
.InclusiveBetween(1, 20)
|
||||
.WithMessage("عمق درخت باید بین 1 تا 20 باشد");
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
|
||||
+31
@@ -11,6 +11,37 @@ public class NetworkTreeDto
|
||||
public string? LastName { get; set; }
|
||||
public NetworkLeg? LegPosition { get; set; }
|
||||
public int CurrentDepth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// تاریخ فعالسازی در باشگاه مشتریان
|
||||
/// </summary>
|
||||
public DateTime? ClubActivatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// وضعیت فعال بودن در باشگاه مشتریان
|
||||
/// </summary>
|
||||
public bool IsClubActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شماره هفته فعالسازی
|
||||
/// </summary>
|
||||
public long? ActivationWeekDefinitionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نام نمایشی هفته فعالسازی (مثل: هفته یکم)
|
||||
/// </summary>
|
||||
public string? ActivationWeekDisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// آیا کاربر در هفته هدف فعال شده است؟
|
||||
/// </summary>
|
||||
public bool IsActivatedInTargetWeek { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// تاریخ ایجاد کاربر
|
||||
/// </summary>
|
||||
public DateTimeOffset UserCreated { get; set; }
|
||||
|
||||
public NetworkTreeDto? LeftChild { get; set; }
|
||||
public NetworkTreeDto? RightChild { get; set; }
|
||||
}
|
||||
|
||||
+184
-19
@@ -11,6 +11,7 @@ public class GetUserNetworkPositionQueryHandler : IRequestHandler<GetUserNetwork
|
||||
|
||||
public async Task<UserNetworkPositionDto?> Handle(GetUserNetworkPositionQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// واکشی اطلاعات اصلی کاربر
|
||||
var user = await _context.Users
|
||||
.AsNoTracking()
|
||||
.Where(x => x.Id == request.UserId)
|
||||
@@ -20,8 +21,17 @@ public class GetUserNetworkPositionQueryHandler : IRequestHandler<GetUserNetwork
|
||||
x.Mobile,
|
||||
x.FirstName,
|
||||
x.LastName,
|
||||
x.Email,
|
||||
x.NationalCode,
|
||||
x.ReferralCode,
|
||||
x.IsMobileVerified,
|
||||
x.BirthDate,
|
||||
x.NetworkParentId,
|
||||
x.LegPosition
|
||||
x.LegPosition,
|
||||
x.HasReceivedDayaCredit,
|
||||
x.DayaCreditReceivedAt,
|
||||
x.PackagePurchaseMethod,
|
||||
x.Created
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
@@ -30,41 +40,196 @@ public class GetUserNetworkPositionQueryHandler : IRequestHandler<GetUserNetwork
|
||||
return null;
|
||||
}
|
||||
|
||||
// شمارش فرزندان
|
||||
var childrenCount = await _context.Users
|
||||
.CountAsync(x => x.NetworkParentId == request.UserId, cancellationToken);
|
||||
|
||||
var leftChildCount = await _context.Users
|
||||
.CountAsync(x => x.NetworkParentId == request.UserId && x.LegPosition == NetworkLeg.Left,
|
||||
cancellationToken);
|
||||
|
||||
var rightChildCount = await _context.Users
|
||||
.CountAsync(x => x.NetworkParentId == request.UserId && x.LegPosition == NetworkLeg.Right,
|
||||
cancellationToken);
|
||||
|
||||
// اطلاعات والد
|
||||
string? parentMobile = null;
|
||||
string? parentFullName = null;
|
||||
if (user.NetworkParentId.HasValue)
|
||||
{
|
||||
parentMobile = await _context.Users
|
||||
var parent = await _context.Users
|
||||
.Where(x => x.Id == user.NetworkParentId)
|
||||
.Select(x => x.Mobile)
|
||||
.Select(x => new { x.Mobile, x.FirstName, x.LastName })
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (parent != null)
|
||||
{
|
||||
parentMobile = parent.Mobile;
|
||||
parentFullName = $"{parent.FirstName} {parent.LastName}".Trim();
|
||||
}
|
||||
}
|
||||
|
||||
// شمارش فرزندان مستقیم
|
||||
var directChildren = await _context.Users
|
||||
.Where(x => x.NetworkParentId == request.UserId)
|
||||
.Select(x => new { x.Id, x.LegPosition, x.FirstName, x.LastName, x.Mobile, x.Created })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var leftChild = directChildren.FirstOrDefault(x => x.LegPosition == NetworkLeg.Left);
|
||||
var rightChild = directChildren.FirstOrDefault(x => x.LegPosition == NetworkLeg.Right);
|
||||
|
||||
// محاسبه تعداد کل اعضای هر شاخه (با استفاده از CTE برای عملکرد بهتر)
|
||||
var leftLegCount = await GetLegMemberCountAsync(request.UserId, NetworkLeg.Left, cancellationToken);
|
||||
var rightLegCount = await GetLegMemberCountAsync(request.UserId, NetworkLeg.Right, cancellationToken);
|
||||
var totalNetworkSize = leftLegCount + rightLegCount;
|
||||
|
||||
// محاسبه حداکثر عمق شبکه
|
||||
var maxDepth = await GetMaxNetworkDepthAsync(request.UserId, cancellationToken);
|
||||
|
||||
// آمار اعضای فعال/غیرفعال (کسانی که پکیج خریدهاند)
|
||||
var allDescendantIds = await GetAllDescendantIdsAsync(request.UserId, cancellationToken);
|
||||
var activeCount = await _context.Users
|
||||
.CountAsync(x => allDescendantIds.Contains(x.Id) &&
|
||||
x.PackagePurchaseMethod != PackagePurchaseMethod.None,
|
||||
cancellationToken);
|
||||
var inactiveCount = allDescendantIds.Count - activeCount;
|
||||
|
||||
// آمار کمیسیونهای کاربر
|
||||
var commissionStats = await _context.UserCommissionPayouts
|
||||
.Where(x => x.UserId == request.UserId)
|
||||
.GroupBy(x => x.UserId)
|
||||
.Select(g => new
|
||||
{
|
||||
TotalBalances = g.Sum(x => x.BalancesEarned),
|
||||
TotalAmount = g.Sum(x => x.TotalAmount),
|
||||
PaidAmount = g.Where(x => x.Status == CommissionPayoutStatus.Paid).Sum(x => x.TotalAmount),
|
||||
PendingAmount = g.Where(x => x.Status == CommissionPayoutStatus.Pending).Sum(x => x.TotalAmount)
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return new UserNetworkPositionDto
|
||||
{
|
||||
// اطلاعات اصلی
|
||||
UserId = user.Id,
|
||||
Mobile = user.Mobile,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
Email = user.Email,
|
||||
NationalCode = user.NationalCode,
|
||||
ReferralCode = user.ReferralCode,
|
||||
IsMobileVerified = user.IsMobileVerified,
|
||||
BirthDate = user.BirthDate,
|
||||
JoinedAt = user.Created,
|
||||
|
||||
// اطلاعات شبکه
|
||||
NetworkParentId = user.NetworkParentId,
|
||||
ParentMobile = parentMobile,
|
||||
ParentFullName = parentFullName,
|
||||
LegPosition = user.LegPosition,
|
||||
TotalChildren = childrenCount,
|
||||
LeftChildCount = leftChildCount,
|
||||
RightChildCount = rightChildCount,
|
||||
IsInNetwork = user.NetworkParentId.HasValue
|
||||
IsInNetwork = user.NetworkParentId.HasValue,
|
||||
|
||||
// آمار فرزندان مستقیم
|
||||
TotalChildren = directChildren.Count,
|
||||
LeftChildCount = leftChild != null ? 1 : 0,
|
||||
RightChildCount = rightChild != null ? 1 : 0,
|
||||
|
||||
// اطلاعات فرزند چپ
|
||||
LeftChildId = leftChild?.Id,
|
||||
LeftChildFullName = leftChild != null ? $"{leftChild.FirstName} {leftChild.LastName}".Trim() : null,
|
||||
LeftChildMobile = leftChild?.Mobile,
|
||||
LeftChildJoinedAt = leftChild?.Created,
|
||||
|
||||
// اطلاعات فرزند راست
|
||||
RightChildId = rightChild?.Id,
|
||||
RightChildFullName = rightChild != null ? $"{rightChild.FirstName} {rightChild.LastName}".Trim() : null,
|
||||
RightChildMobile = rightChild?.Mobile,
|
||||
RightChildJoinedAt = rightChild?.Created,
|
||||
|
||||
// آمار کل شبکه
|
||||
TotalLeftLegMembers = leftLegCount,
|
||||
TotalRightLegMembers = rightLegCount,
|
||||
TotalNetworkSize = totalNetworkSize,
|
||||
MaxNetworkDepth = maxDepth,
|
||||
|
||||
// اطلاعات پکیج و دایا
|
||||
HasReceivedDayaCredit = user.HasReceivedDayaCredit,
|
||||
DayaCreditReceivedAt = user.DayaCreditReceivedAt,
|
||||
PackagePurchaseMethod = user.PackagePurchaseMethod,
|
||||
HasPurchasedGoldenPackage = user.PackagePurchaseMethod != PackagePurchaseMethod.None,
|
||||
|
||||
// آمار مالی
|
||||
TotalEarnedCommission = commissionStats?.TotalAmount ?? 0,
|
||||
TotalPaidCommission = commissionStats?.PaidAmount ?? 0,
|
||||
PendingCommission = commissionStats?.PendingAmount ?? 0,
|
||||
TotalBalancesEarned = commissionStats?.TotalBalances ?? 0,
|
||||
|
||||
// آمار فعالیت
|
||||
ActiveMembersInNetwork = activeCount,
|
||||
InactiveMembersInNetwork = inactiveCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه تعداد اعضای یک شاخه (چپ یا راست) به صورت بازگشتی
|
||||
/// </summary>
|
||||
private async Task<int> GetLegMemberCountAsync(long userId, NetworkLeg leg, CancellationToken cancellationToken)
|
||||
{
|
||||
var directChild = await _context.Users
|
||||
.Where(x => x.NetworkParentId == userId && x.LegPosition == leg)
|
||||
.Select(x => x.Id)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (directChild == 0)
|
||||
return 0;
|
||||
|
||||
// تعداد کل زیرمجموعه این فرزند + خود فرزند
|
||||
var descendants = await GetAllDescendantIdsAsync(directChild, cancellationToken);
|
||||
return descendants.Count + 1; // +1 برای خود فرزند
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه حداکثر عمق شبکه
|
||||
/// </summary>
|
||||
private async Task<int> GetMaxNetworkDepthAsync(long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var maxDepth = 0;
|
||||
var currentLevelIds = new List<long> { userId };
|
||||
|
||||
while (currentLevelIds.Any())
|
||||
{
|
||||
var nextLevelIds = await _context.Users
|
||||
.Where(x => currentLevelIds.Contains(x.NetworkParentId ?? 0))
|
||||
.Select(x => x.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (nextLevelIds.Any())
|
||||
{
|
||||
maxDepth++;
|
||||
currentLevelIds = nextLevelIds;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return maxDepth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تمام ID های زیرمجموعه یک کاربر
|
||||
/// </summary>
|
||||
private async Task<List<long>> GetAllDescendantIdsAsync(long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var allDescendants = new List<long>();
|
||||
var currentLevelIds = new List<long> { userId };
|
||||
|
||||
while (currentLevelIds.Any())
|
||||
{
|
||||
var children = await _context.Users
|
||||
.Where(x => currentLevelIds.Contains(x.NetworkParentId ?? 0))
|
||||
.Select(x => x.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (children.Any())
|
||||
{
|
||||
allDescendants.AddRange(children);
|
||||
currentLevelIds = children;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allDescendants;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user