Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b42d9e141d | |||
| f64b6be7da | |||
| 9185aa227d | |||
| b2d676b555 | |||
| b41342dcad | |||
| c3eeb16856 | |||
| 794dd01ac0 | |||
| 658d076bdf | |||
| 96daf899c7 | |||
| eddc82dfce | |||
| 0c95b03f0b | |||
| 45c01fc157 | |||
| 55a775f51c | |||
| 5b0558ee24 | |||
| cabf3886de | |||
| 0baed05fd4 | |||
| 7c4c978d3f | |||
| e327eb23ba | |||
| 20280cf07f | |||
| c67caca0ae | |||
| e14a653fa8 | |||
| 4968878773 | |||
| 8ffb53996b | |||
| 1a01fd9057 | |||
| 52af1bfaad | |||
| 2fd5300c1e | |||
| c76af47a4c | |||
| 77be841221 | |||
| 4743b52cae | |||
| 0b0fa3aa61 | |||
| 839250e132 | |||
| 8894e72a8e | |||
| 1c130b14e1 | |||
| 793d81f1dd | |||
| f6704eaa99 | |||
| d03fbe0a98 | |||
| f9c90bb871 | |||
| 70c4b15a23 | |||
| 5e662b5984 | |||
| 16ba2fa6fb | |||
| 389568af2b | |||
| 98f3036bd0 | |||
| 631646776e | |||
| d8031a0d17 | |||
| ca9b02a902 | |||
| dde4c68b2f | |||
| 8c3d710253 | |||
| a81fb39a32 | |||
| f8731bc7fc | |||
| d3d021c007 | |||
| bea43a0569 | |||
| 1cf501711f | |||
| f0117eb1d5 | |||
| 500e169141 | |||
| ed45a1759b | |||
| 7087df23ef | |||
| 6374d5c343 | |||
| 1dbb3ee502 | |||
| cd2549775a | |||
| 322242fbab | |||
| 3d42c91d21 | |||
| 9448f3fff0 | |||
| 02e2f8111f | |||
| 9d2b5ad2d4 | |||
| 8940362575 | |||
| bca3b7fe62 | |||
| c03b3705c3 | |||
| a9140bec04 | |||
| 17c2958b9c | |||
| 2e54aa7aca | |||
| 7b26073c63 | |||
| 28e94d6137 | |||
| c6ee356cbd | |||
| 2d09a69be9 | |||
| 7cebb27171 | |||
| 68489a8374 | |||
| 330f0cae5a | |||
| b16f01b6e4 | |||
| 13f24d523b | |||
| 41308e189a | |||
| 8953d86755 | |||
| ef8d5d4d97 | |||
| 288b85a59b | |||
| 30144a07c8 | |||
| 5bcf6c9840 | |||
| 9d51e70a7f | |||
| a31c88219f | |||
| ceeb29d843 | |||
| 0ba6af9049 | |||
| 5ad69cb3fa | |||
| 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 |
@@ -4,7 +4,7 @@ name: Push nuget and docker image Actions Workflow
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- stage
|
- stage_new
|
||||||
jobs:
|
jobs:
|
||||||
Deploy:
|
Deploy:
|
||||||
runs-on: windows
|
runs-on: windows
|
||||||
|
|||||||
@@ -6,81 +6,84 @@ on:
|
|||||||
- kub-stage
|
- kub-stage
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: git.foursat.afrino.co
|
REGISTRY: 194.5.195.53:30080
|
||||||
IMAGE_NAME: admin/cms
|
IMAGE_NAME: admin/cms
|
||||||
|
K8S_SERVER: 194.5.195.53
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: docker:latest
|
image: 194.5.195.53:32082/docker-sshpass:latest
|
||||||
options: --privileged
|
options: --privileged
|
||||||
env:
|
|
||||||
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:
|
steps:
|
||||||
- name: Install dependencies
|
- name: Start Docker daemon
|
||||||
run: |
|
|
||||||
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 insecure registry
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /etc/docker
|
mkdir -p /etc/docker
|
||||||
cat > /etc/docker/daemon.json << 'DAEMON'
|
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||||
{
|
{
|
||||||
"insecure-registries": ["git.foursat.afrino.co", "gitea-svc:3000"]
|
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
|
||||||
}
|
}
|
||||||
DAEMON
|
DAEMON
|
||||||
mkdir -p ~/.docker
|
echo "🚀 Starting Docker daemon..."
|
||||||
cat > ~/.docker/config.json << 'CONF'
|
|
||||||
{
|
|
||||||
"proxies": {
|
|
||||||
"default": {
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
dockerd &
|
dockerd &
|
||||||
for i in $(seq 1 30); do
|
|
||||||
docker info >/dev/null 2>&1 && break || sleep 2
|
# Wait up to 3 minutes for Docker to be ready
|
||||||
|
for i in $(seq 1 90); do
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
echo "✅ Docker daemon is ready (attempt $i)"
|
||||||
|
docker version
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
docker info
|
|
||||||
|
# Final check
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
echo "❌ Docker daemon failed to start after 3 minutes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/CMS.git .
|
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/CMS.git .
|
||||||
git log -1 --format="%H %s"
|
|
||||||
|
- name: Publish Protobuf packages
|
||||||
|
run: |
|
||||||
|
echo "📦 Publishing Protobuf packages..."
|
||||||
|
docker run --rm -v $(pwd):/src -w /src \
|
||||||
|
194.5.195.53:32082/dotnet/sdk:9.0 sh -c '
|
||||||
|
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
|
||||||
|
echo "📦 $proj"
|
||||||
|
dotnet restore "$proj"
|
||||||
|
dotnet build "$proj" -c Release --no-restore
|
||||||
|
dotnet pack "$proj" -c Release --no-build -o "$(dirname $proj)/nupkg"
|
||||||
|
for nupkg in $(dirname $proj)/nupkg/*.nupkg; do
|
||||||
|
[ -f "$nupkg" ] && dotnet nuget push "$nupkg" \
|
||||||
|
--source "http://194.5.195.53:32081/repository/foursat-nuget-hosted/index.json" \
|
||||||
|
--api-key "admin:87zH26nbqT" \
|
||||||
|
--skip-duplicate --allow-insecure-connections || true
|
||||||
|
done
|
||||||
|
done
|
||||||
|
'
|
||||||
|
echo "✅ Protobuf packages done!"
|
||||||
|
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
run: |
|
run: |
|
||||||
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest .
|
||||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
|
|
||||||
--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
|
- name: Push to Registry
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
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
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||||
|
|
||||||
- name: Deploy to Kubernetes
|
- name: Deploy to Kubernetes
|
||||||
run: |
|
run: |
|
||||||
# Setup kubeconfig
|
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
|
||||||
mkdir -p ~/.kube
|
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||||
echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config
|
kubectl rollout restart deployment/cms
|
||||||
|
kubectl rollout status deployment/cms --timeout=180s
|
||||||
# Restart deployment to pull new image
|
"
|
||||||
kubectl rollout restart deployment/cms || echo "Deployment doesn't exist yet"
|
echo "✅ Deployed!"
|
||||||
|
|
||||||
# Wait for rollout to complete
|
|
||||||
kubectl rollout status deployment/cms --timeout=5m || echo "Deployment rollout pending"
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
name: Build and Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- production
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: 194.5.195.53:30080
|
||||||
|
IMAGE_NAME: admin/cms
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: docker:latest
|
||||||
|
options: --privileged
|
||||||
|
env:
|
||||||
|
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,45.149.79.127,10.0.0.0/8
|
||||||
|
steps:
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
apk add --no-cache git curl
|
||||||
|
|
||||||
|
# Install kubectl with fixed version
|
||||||
|
KUBECTL_VERSION="v1.31.0"
|
||||||
|
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
|
||||||
|
chmod +x kubectl
|
||||||
|
mv kubectl /usr/local/bin/
|
||||||
|
|
||||||
|
- name: Start Docker daemon with insecure registry
|
||||||
|
run: |
|
||||||
|
mkdir -p /etc/docker
|
||||||
|
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||||
|
{
|
||||||
|
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32082", "gitea-svc:3000"]
|
||||||
|
}
|
||||||
|
DAEMON
|
||||||
|
mkdir -p ~/.docker
|
||||||
|
cat > ~/.docker/config.json << 'CONF'
|
||||||
|
{
|
||||||
|
"proxies": {
|
||||||
|
"default": {
|
||||||
|
"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,45.149.79.127,10.0.0.0/8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
dockerd &
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
docker info >/dev/null 2>&1 && break || sleep 2
|
||||||
|
done
|
||||||
|
docker info
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
run: |
|
||||||
|
git clone --depth 1 --branch production 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 }}:prod \
|
||||||
|
--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
|
||||||
|
run: |
|
||||||
|
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 }}:prod
|
||||||
|
|
||||||
|
- name: Deploy to Production
|
||||||
|
run: |
|
||||||
|
# Setup kubeconfig for PRODUCTION
|
||||||
|
mkdir -p ~/.kube
|
||||||
|
echo "${{ secrets.KUBECONFIG_PROD }}" | 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"
|
||||||
@@ -492,3 +492,8 @@ fabric.properties
|
|||||||
.idea/caches/build_file_checksums.ser
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
/src/.idea
|
/src/.idea
|
||||||
|
|
||||||
|
# Environment-specific configuration files with sensitive data
|
||||||
|
**/appsettings.Staging.json
|
||||||
|
**/appsettings.Production.json
|
||||||
|
**/appsettings.*.local.json
|
||||||
|
|||||||
+7
-4
@@ -1,14 +1,17 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copy NuGet config first
|
||||||
|
COPY src/NuGet.config ./
|
||||||
|
|
||||||
# Copy solution and project files
|
# Copy solution and project files
|
||||||
COPY src/ ./
|
COPY src/ ./
|
||||||
|
|
||||||
# Restore and publish
|
# Restore with Nexus config and publish
|
||||||
RUN dotnet restore "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj"
|
RUN dotnet restore "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" --configfile NuGet.config
|
||||||
RUN dotnet publish "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" -c Release -o /app/publish --no-restore
|
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
|
FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /app/publish .
|
COPY --from=build /app/publish .
|
||||||
ENV ASPNETCORE_URLS=http://+:8080
|
ENV ASPNETCORE_URLS=http://+:8080
|
||||||
|
|||||||
@@ -1,38 +1,92 @@
|
|||||||
# CMS Microservice - Network & Club Commission System
|
# CMS Microservice - Network & Club Commission + Inventory Management System
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[]()
|
[]()
|
||||||
[]()
|
[]()
|
||||||
|
|
||||||
## 📊 Project Status (2025-12-01)
|
## 📊 Project Status (January 2026)
|
||||||
|
|
||||||
**Overall Progress**: 85% Complete (7/10 phases)
|
### 🏪 Inventory Management System - NEW!
|
||||||
**Production Readiness**: 95%
|
**Progress**: Phase 2 Complete (50%)
|
||||||
|
**Architecture**: Clean Architecture + CQRS + Repository Pattern
|
||||||
|
|
||||||
|
#### ✅ Completed Phases
|
||||||
|
1. ✅ **Phase 1: Infrastructure & Domain Layer**
|
||||||
|
- Domain Entities: `InventoryItem`, `StockMovement`, `Warehouse`
|
||||||
|
- Domain Enums: `StockMovementType`
|
||||||
|
- EF Core Configurations with proper indexing
|
||||||
|
- Database migration applied
|
||||||
|
|
||||||
|
2. ✅ **Phase 2: Repository Pattern & CQRS**
|
||||||
|
- Repository Interfaces & Implementations
|
||||||
|
- CQRS Commands (17 commands)
|
||||||
|
- CQRS Queries (35 queries)
|
||||||
|
- MediatR Handlers (52 handlers)
|
||||||
|
|
||||||
|
#### 🔄 In Progress
|
||||||
|
3. 🔄 **Phase 3: Business Services Layer**
|
||||||
|
4. ⏳ **Phase 4: DTOs & AutoMapper**
|
||||||
|
5. ⏳ **Phase 5: API Controllers**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💼 Commission System - Production Ready
|
||||||
|
**Progress**: 85% Complete
|
||||||
**MVP Status**: ✅ 100% Complete
|
**MVP Status**: ✅ 100% Complete
|
||||||
|
|
||||||
### ✅ Completed Phases (7)
|
#### ✅ Completed Features
|
||||||
1. ✅ Domain Layer (Entities, Enums, Value Objects)
|
- ✅ Binary network tree with automatic placement
|
||||||
2. ✅ Club Membership System
|
- ✅ Club membership (Member/Trial) with commission rates
|
||||||
3. ✅ Binary Network Tree
|
- ✅ Weekly commission calculation (Lesser Leg algorithm)
|
||||||
4. ✅ **Commission Calculation & Background Worker** (MVP)
|
- ✅ Background worker with Hangfire
|
||||||
5. ✅ Protobuf gRPC Services
|
- ✅ Email + SMS notifications (MailKit + Kavenegar)
|
||||||
6. ✅ History & Configuration Management
|
- ✅ Health check endpoints (Kubernetes-ready)
|
||||||
7. ✅ Database Migration & Seed Data
|
|
||||||
|
|
||||||
### 🟡 Partially Complete (1)
|
### 🟡 Partially Complete
|
||||||
- Phase 10: Withdrawal & Settlement (40%)
|
- Phase 10: Withdrawal & Settlement (40%)
|
||||||
- ✅ Commands & Database
|
- ✅ Commands & Database
|
||||||
- ❌ Payment Gateway Integration
|
- ❌ Payment Gateway Integration
|
||||||
|
|
||||||
### ❌ Not Started (1)
|
### ❌ Not Started
|
||||||
- Phase 9: Club Shop & Product Integration (0%)
|
- Phase 9: Club Shop & Product Integration (0%)
|
||||||
|
|
||||||
### ⏸️ Postponed (1)
|
|
||||||
- Phase 7: Testing (Unit, Integration, Load tests)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Recent Updates (2025-12-01)
|
## 🚀 Recent Updates (January 2026)
|
||||||
|
|
||||||
|
### 🏪 Inventory Management System - NEW! ✅
|
||||||
|
**Complete CQRS-based inventory management with:**
|
||||||
|
|
||||||
|
#### Domain Layer:
|
||||||
|
- ✅ `InventoryItem` - Multi-warehouse product tracking with min/max thresholds
|
||||||
|
- ✅ `StockMovement` - Complete audit trail with 8 movement types
|
||||||
|
- ✅ `Warehouse` - Multi-location support with default warehouse
|
||||||
|
|
||||||
|
#### Repository Pattern:
|
||||||
|
- ✅ `IInventoryItemRepository` - 25+ methods for inventory operations
|
||||||
|
- ✅ `IStockMovementRepository` - Movement tracking & analytics
|
||||||
|
- ✅ `IWarehouseRepository` - Warehouse management & statistics
|
||||||
|
|
||||||
|
#### CQRS Commands (17 total):
|
||||||
|
- **Inventory:** Create, Update, Delete, Reserve, Release, Reduce, Increase
|
||||||
|
- **Movement:** Create, BulkCreate, Delete
|
||||||
|
- **Warehouse:** Create, Update, Delete, SetDefault, Activate, BulkCreate
|
||||||
|
|
||||||
|
#### CQRS Queries (35 total):
|
||||||
|
- **Inventory:** GetById, Search, LowStock, OutOfStock, CheckAvailability
|
||||||
|
- **Movement:** GetHistory, GetByOrder, Search, Analytics, DailyVolume, TopMoving
|
||||||
|
- **Warehouse:** GetById, Search, GetStats, GetLowStock, GetAllStats
|
||||||
|
|
||||||
|
#### Business Features:
|
||||||
|
- ✅ Multi-warehouse inventory management
|
||||||
|
- ✅ Stock reservation system for orders
|
||||||
|
- ✅ Automatic movement tracking
|
||||||
|
- ✅ Low stock & out-of-stock alerts
|
||||||
|
- ✅ Advanced analytics & reporting
|
||||||
|
- ✅ Bulk operations support
|
||||||
|
- ✅ Transaction-safe operations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Email & SMS Notifications - COMPLETED ✅
|
### Email & SMS Notifications - COMPLETED ✅
|
||||||
- ✅ **MailKit 4.14.1** for Email (SMTP with HTML templates)
|
- ✅ **MailKit 4.14.1** for Email (SMTP with HTML templates)
|
||||||
@@ -63,8 +117,38 @@
|
|||||||
**Clean Architecture** with 4 layers:
|
**Clean Architecture** with 4 layers:
|
||||||
```
|
```
|
||||||
CMSMicroservice.Domain/ # Entities, Enums, Interfaces
|
CMSMicroservice.Domain/ # Entities, Enums, Interfaces
|
||||||
|
├── Entities/
|
||||||
|
│ ├── InventoryItem.cs # NEW: Inventory tracking
|
||||||
|
│ ├── StockMovement.cs # NEW: Movement audit
|
||||||
|
│ └── Warehouse.cs # NEW: Multi-warehouse
|
||||||
|
├── Enums/
|
||||||
|
│ └── StockMovementType.cs # NEW: Movement types
|
||||||
|
|
||||||
CMSMicroservice.Application/ # CQRS (Commands, Queries, MediatR)
|
CMSMicroservice.Application/ # CQRS (Commands, Queries, MediatR)
|
||||||
|
├── Features/
|
||||||
|
│ ├── InventoryItems/ # NEW: Inventory CQRS
|
||||||
|
│ │ ├── Commands/
|
||||||
|
│ │ ├── Queries/
|
||||||
|
│ │ └── Handlers/
|
||||||
|
│ ├── StockMovements/ # NEW: Movement CQRS
|
||||||
|
│ │ ├── Commands/
|
||||||
|
│ │ ├── Queries/
|
||||||
|
│ │ └── Handlers/
|
||||||
|
│ └── Warehouses/ # NEW: Warehouse CQRS
|
||||||
|
│ ├── Commands/
|
||||||
|
│ ├── Queries/
|
||||||
|
│ └── Handlers/
|
||||||
|
└── Common/Interfaces/
|
||||||
|
└── Repositories/ # NEW: Repository interfaces
|
||||||
|
|
||||||
CMSMicroservice.Infrastructure/ # DbContext, Services, Background Jobs
|
CMSMicroservice.Infrastructure/ # DbContext, Services, Background Jobs
|
||||||
|
├── Persistence/
|
||||||
|
│ ├── Context/
|
||||||
|
│ ├── Configurations/ # NEW: EF Core configs
|
||||||
|
│ ├── Repositories/ # NEW: Repository implementations
|
||||||
|
│ └── Migrations/
|
||||||
|
└── DependencyInjection.cs # NEW: DI setup
|
||||||
|
|
||||||
CMSMicroservice.WebApi/ # gRPC Services, Controllers
|
CMSMicroservice.WebApi/ # gRPC Services, Controllers
|
||||||
CMSMicroservice.Protobuf/ # Protocol Buffers definitions
|
CMSMicroservice.Protobuf/ # Protocol Buffers definitions
|
||||||
```
|
```
|
||||||
@@ -84,6 +168,7 @@ CMSMicroservice.Protobuf/ # Protocol Buffers definitions
|
|||||||
|
|
||||||
## 📖 Documentation
|
## 📖 Documentation
|
||||||
|
|
||||||
|
- **[Development Plan](docs/development-plan.md)** - NEW: Inventory system roadmap
|
||||||
- **[Implementation Progress](docs/implementation-progress.md)** - Detailed phase-by-phase progress
|
- **[Implementation Progress](docs/implementation-progress.md)** - Detailed phase-by-phase progress
|
||||||
- **[Email/SMS Configuration Guide](docs/email-sms-configuration-guide.md)** - Production setup instructions
|
- **[Email/SMS Configuration Guide](docs/email-sms-configuration-guide.md)** - Production setup instructions
|
||||||
- **[Balance Calculation Logic](docs/balance-calculation-carryover-logic.md)** - Commission algorithm details
|
- **[Balance Calculation Logic](docs/balance-calculation-carryover-logic.md)** - Commission algorithm details
|
||||||
@@ -92,6 +177,74 @@ CMSMicroservice.Protobuf/ # Protocol Buffers definitions
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🏪 Inventory System Usage
|
||||||
|
|
||||||
|
### Create Warehouse
|
||||||
|
```csharp
|
||||||
|
await mediator.Send(new CreateWarehouseCommand
|
||||||
|
{
|
||||||
|
Name = "Main Warehouse",
|
||||||
|
Code = "WH-001",
|
||||||
|
IsDefault = true,
|
||||||
|
IsActive = true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Inventory Item
|
||||||
|
```csharp
|
||||||
|
await mediator.Send(new CreateInventoryItemCommand
|
||||||
|
{
|
||||||
|
ProductId = 1,
|
||||||
|
WarehouseId = 1,
|
||||||
|
Quantity = 100,
|
||||||
|
MinQuantity = 10,
|
||||||
|
MaxQuantity = 1000
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reserve Stock for Order
|
||||||
|
```csharp
|
||||||
|
await mediator.Send(new ReserveInventoryCommand
|
||||||
|
{
|
||||||
|
Id = inventoryId,
|
||||||
|
Quantity = 5,
|
||||||
|
OrderId = 12345
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Availability
|
||||||
|
```csharp
|
||||||
|
bool available = await mediator.Send(
|
||||||
|
new CheckInventoryAvailabilityQuery(inventoryId, 10));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Low Stock Alerts
|
||||||
|
```csharp
|
||||||
|
var lowStock = await mediator.Send(new GetLowStockItemsQuery
|
||||||
|
{
|
||||||
|
WarehouseId = 1,
|
||||||
|
Count = 50
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Movement Analytics
|
||||||
|
```csharp
|
||||||
|
var summary = await mediator.Send(new GetMovementSummaryQuery
|
||||||
|
{
|
||||||
|
FromDate = DateTime.Now.AddDays(-7),
|
||||||
|
ToDate = DateTime.Now
|
||||||
|
});
|
||||||
|
|
||||||
|
var topProducts = await mediator.Send(new GetTopMovingProductsQuery
|
||||||
|
{
|
||||||
|
FromDate = DateTime.Now.AddDays(-30),
|
||||||
|
ToDate = DateTime.Now,
|
||||||
|
Count = 10
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
@@ -197,7 +350,25 @@ curl http://localhost:5133/health/live # Liveness probe (K8s)
|
|||||||
|
|
||||||
## 📊 What's Remaining?
|
## 📊 What's Remaining?
|
||||||
|
|
||||||
### High Priority
|
### 🏪 Inventory System (Current Focus)
|
||||||
|
1. **Phase 3: Business Services** (In Progress)
|
||||||
|
- `IInventoryManagementService` - High-level operations
|
||||||
|
- `IStockMovementService` - Movement orchestration
|
||||||
|
- `IWarehouseService` - Warehouse business logic
|
||||||
|
- `IInventoryReportingService` - Advanced reporting
|
||||||
|
|
||||||
|
2. **Phase 4: DTOs & AutoMapper** (Next)
|
||||||
|
- Request/Response DTOs
|
||||||
|
- AutoMapper profiles
|
||||||
|
- Validation rules
|
||||||
|
|
||||||
|
3. **Phase 5: API Controllers** (Planned)
|
||||||
|
- `InventoryController` - REST API
|
||||||
|
- `WarehouseController` - Warehouse management
|
||||||
|
- `StockMovementController` - Movement tracking
|
||||||
|
- Swagger documentation
|
||||||
|
|
||||||
|
### 💼 Commission System
|
||||||
1. **Payment Gateway Integration** (Phase 10 - 1 week)
|
1. **Payment Gateway Integration** (Phase 10 - 1 week)
|
||||||
- Daya or Bank Mellat API integration
|
- Daya or Bank Mellat API integration
|
||||||
- IBAN transfer automation
|
- IBAN transfer automation
|
||||||
@@ -230,6 +401,7 @@ curl http://localhost:5133/health/live # Liveness probe (K8s)
|
|||||||
|
|
||||||
## 🎯 MVP Features (100% Complete)
|
## 🎯 MVP Features (100% Complete)
|
||||||
|
|
||||||
|
### 💼 Commission System:
|
||||||
✅ Binary network tree with automatic placement
|
✅ Binary network tree with automatic placement
|
||||||
✅ Club membership (Member/Trial) with different commission rates
|
✅ Club membership (Member/Trial) with different commission rates
|
||||||
✅ Weekly commission calculation (Lesser Leg algorithm)
|
✅ Weekly commission calculation (Lesser Leg algorithm)
|
||||||
@@ -244,12 +416,26 @@ curl http://localhost:5133/health/live # Liveness probe (K8s)
|
|||||||
✅ Structured logging (AlertService for Sentry/Slack)
|
✅ Structured logging (AlertService for Sentry/Slack)
|
||||||
✅ JWT authentication context (CurrentUserService)
|
✅ JWT authentication context (CurrentUserService)
|
||||||
|
|
||||||
|
### 🏪 Inventory System (Phase 2 Complete):
|
||||||
|
✅ Domain entities (InventoryItem, StockMovement, Warehouse)
|
||||||
|
✅ Multi-warehouse inventory management
|
||||||
|
✅ Stock reservation system for orders
|
||||||
|
✅ 8 movement types with complete audit trail
|
||||||
|
✅ Repository pattern with 25+ methods per repository
|
||||||
|
✅ CQRS with 17 commands and 35 queries
|
||||||
|
✅ 52 MediatR handlers with business logic
|
||||||
|
✅ Low stock and out-of-stock alerts
|
||||||
|
✅ Advanced analytics (top products, daily volume)
|
||||||
|
✅ Bulk operations support
|
||||||
|
✅ Transaction-safe operations with rollback
|
||||||
|
✅ DI container configuration
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 👥 Team
|
## 👥 Team
|
||||||
|
|
||||||
**Development**: FourSat Team
|
**Development**: FourSat Team
|
||||||
**Last Updated**: 2025-12-01
|
**Last Updated**: January 2026
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Docs moved to /totalDoc — see totalDoc/INDEX.md
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "a5404a90-7d5a-4e9f-a26b-bb2bc93f19a2",
|
||||||
|
"name": "hushyar",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||||
|
"_exporter_id": "12336342",
|
||||||
|
"_collection_link": "https://abbas-rahimzadeh-postman.postman.co/workspace/public-workspace~57c702f9-fed1-4b27-b507-0b0252812bf7/collection/12336342-a5404a90-7d5a-4e9f-a26b-bb2bc93f19a2?action=share&source=collection_link&creator=12336342"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "v1",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "organization",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "register-user",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{organizationApiKey}}",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Register a new user or get existing user via organization API key. If user is new or has no organization, they will be assigned to this organization and their wallet will be charged with the organization's user_credit."
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "success - new user",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{organizationApiKey}}",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"id\": 1,\n \"mobile_number\": \"09123456789\",\n \"organization_id\": 1,\n \"organization_title\": \"Test Organization\",\n \"wallet_balance\": 100.0,\n \"is_new_user\": true,\n \"credit_charged\": 100.0\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "success - existing user",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{organizationApiKey}}",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"id\": 1,\n \"mobile_number\": \"09123456789\",\n \"organization_id\": 1,\n \"organization_title\": \"Test Organization\",\n \"wallet_balance\": 100.0,\n \"is_new_user\": false,\n \"credit_charged\": 0.0\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "invalid api key",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "invalid-api-key",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Unauthorized",
|
||||||
|
"code": 401,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"error_code\": \"INVALID_API_KEY\",\n \"message\": \"Invalid API key\",\n \"context\": {\n \"api_key_prefix\": \"invalid-\"\n }\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "organization disabled",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{organizationApiKey}}",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Forbidden",
|
||||||
|
"code": 403,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"error_code\": \"ORGANIZATION_DISABLED\",\n \"message\": \"Organization is disabled\",\n \"context\": {\n \"organization_id\": 1\n }\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "organization expired",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{organizationApiKey}}",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Forbidden",
|
||||||
|
"code": 403,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"error_code\": \"ORGANIZATION_EXPIRED\",\n \"message\": \"Organization has expired\",\n \"context\": {\n \"organization_id\": 1,\n \"expires_at\": \"2024-01-01T00:00:00\"\n }\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "invalid mobile format",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{organizationApiKey}}",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"01234567890\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Unprocessable Entity",
|
||||||
|
"code": 422,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"mobile_number\": \"String should match pattern '^09\\\\d{9}$'\"\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "missing api key header",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Unprocessable Entity",
|
||||||
|
"code": 422,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"detail\": [\n {\n \"type\": \"missing\",\n \"loc\": [\"header\", \"x-api-key\"],\n \"msg\": \"Field required\",\n \"input\": null\n }\n ]\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"packages": {},
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"packages": {},
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "baseURL",
|
||||||
|
"value": "http://127.0.0.0:8000",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "access_token",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "accessToken",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "refreshToken",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "organizationApiKey",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
namespace CMSMicroservice.Application.AppVersionCQ.Commands.UpdateAppVersion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command برای آپدیت یا ایجاد نسخه جدید اپلیکیشن
|
||||||
|
/// </summary>
|
||||||
|
public record UpdateAppVersionCommand : IRequest<Unit>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// نام اپلیکیشن (FrontOffice, BackOffice, MobileApp)
|
||||||
|
/// </summary>
|
||||||
|
public string AppName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شماره نسخه جدید (مثلاً 1.2.3)
|
||||||
|
/// </summary>
|
||||||
|
public string CurrentVersion { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حداقل نسخه مورد نیاز
|
||||||
|
/// </summary>
|
||||||
|
public string? MinRequiredVersion { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیا کش کامل باید پاک بشه؟
|
||||||
|
/// </summary>
|
||||||
|
public bool RequiresFullCacheClear { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیام آپدیت
|
||||||
|
/// </summary>
|
||||||
|
public string? UpdateMessage { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// توضیحات تغییرات این نسخه
|
||||||
|
/// </summary>
|
||||||
|
public string? ReleaseNotes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دلیل آپدیت (برای لاگ)
|
||||||
|
/// </summary>
|
||||||
|
public string? UpdateReason { get; init; }
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
using CMSMicroservice.Domain.Entities.Configuration;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.AppVersionCQ.Commands.UpdateAppVersion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler برای آپدیت یا ایجاد نسخه جدید اپلیکیشن
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateAppVersionCommandHandler : IRequestHandler<UpdateAppVersionCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ILogger<UpdateAppVersionCommandHandler> _logger;
|
||||||
|
|
||||||
|
public UpdateAppVersionCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
ILogger<UpdateAppVersionCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(UpdateAppVersionCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// پیدا کردن نسخه موجود برای این اپ
|
||||||
|
var existingVersion = await _context.AppVersions
|
||||||
|
.Where(v => v.AppName == request.AppName && !v.IsDeleted)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (existingVersion != null)
|
||||||
|
{
|
||||||
|
// آپدیت نسخه موجود
|
||||||
|
existingVersion.CurrentVersion = request.CurrentVersion;
|
||||||
|
existingVersion.MinRequiredVersion = request.MinRequiredVersion ?? request.CurrentVersion;
|
||||||
|
existingVersion.RequiresFullCacheClear = request.RequiresFullCacheClear;
|
||||||
|
existingVersion.UpdateMessage = request.UpdateMessage;
|
||||||
|
existingVersion.ReleaseNotes = request.ReleaseNotes;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"App version updated: {AppName} v{Version}, CacheClear={CacheClear}, Reason={Reason}",
|
||||||
|
request.AppName, request.CurrentVersion, request.RequiresFullCacheClear, request.UpdateReason);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ایجاد نسخه جدید
|
||||||
|
var newVersion = new AppVersion
|
||||||
|
{
|
||||||
|
AppName = request.AppName,
|
||||||
|
CurrentVersion = request.CurrentVersion,
|
||||||
|
MinRequiredVersion = request.MinRequiredVersion ?? request.CurrentVersion,
|
||||||
|
RequiresFullCacheClear = request.RequiresFullCacheClear,
|
||||||
|
UpdateMessage = request.UpdateMessage,
|
||||||
|
ReleaseNotes = request.ReleaseNotes,
|
||||||
|
IsActive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.AppVersions.Add(newVersion);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"New app version created: {AppName} v{Version}, CacheClear={CacheClear}, Reason={Reason}",
|
||||||
|
request.AppName, request.CurrentVersion, request.RequiresFullCacheClear, request.UpdateReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
namespace CMSMicroservice.Application.AppVersionCQ.Queries.GetAllAppVersions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query برای دریافت همه نسخههای اپلیکیشنها
|
||||||
|
/// </summary>
|
||||||
|
public record GetAllAppVersionsQuery(bool IncludeInactive = false) : IRequest<List<AppVersionItemDto>>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO برای هر آیتم نسخه اپلیکیشن
|
||||||
|
/// </summary>
|
||||||
|
public record AppVersionItemDto
|
||||||
|
{
|
||||||
|
public long Id { get; init; }
|
||||||
|
public string AppName { get; init; } = string.Empty;
|
||||||
|
public string CurrentVersion { get; init; } = string.Empty;
|
||||||
|
public string MinRequiredVersion { get; init; } = string.Empty;
|
||||||
|
public bool RequiresFullCacheClear { get; init; }
|
||||||
|
public string? UpdateMessage { get; init; }
|
||||||
|
public string? ReleaseNotes { get; init; }
|
||||||
|
public bool IsActive { get; init; }
|
||||||
|
public DateTime Created { get; init; }
|
||||||
|
public DateTime? LastModified { get; init; }
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
namespace CMSMicroservice.Application.AppVersionCQ.Queries.GetAllAppVersions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler برای دریافت همه نسخههای اپلیکیشنها
|
||||||
|
/// </summary>
|
||||||
|
public class GetAllAppVersionsQueryHandler : IRequestHandler<GetAllAppVersionsQuery, List<AppVersionItemDto>>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetAllAppVersionsQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<AppVersionItemDto>> Handle(GetAllAppVersionsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var query = _context.AppVersions
|
||||||
|
.Where(v => !v.IsDeleted);
|
||||||
|
|
||||||
|
if (!request.IncludeInactive)
|
||||||
|
{
|
||||||
|
query = query.Where(v => v.IsActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
var versions = await query
|
||||||
|
.OrderBy(v => v.AppName)
|
||||||
|
.Select(v => new AppVersionItemDto
|
||||||
|
{
|
||||||
|
Id = v.Id,
|
||||||
|
AppName = v.AppName,
|
||||||
|
CurrentVersion = v.CurrentVersion,
|
||||||
|
MinRequiredVersion = v.MinRequiredVersion,
|
||||||
|
RequiresFullCacheClear = v.RequiresFullCacheClear,
|
||||||
|
UpdateMessage = v.UpdateMessage,
|
||||||
|
ReleaseNotes = v.ReleaseNotes,
|
||||||
|
IsActive = v.IsActive,
|
||||||
|
Created = v.Created,
|
||||||
|
LastModified = v.LastModified
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
namespace CMSMicroservice.Application.AppVersionCQ.Queries.GetAppVersion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query برای دریافت آخرین نسخه یک اپلیکیشن
|
||||||
|
/// </summary>
|
||||||
|
public record GetAppVersionQuery(string AppName, string? CurrentClientVersion = null) : IRequest<AppVersionDto?>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO برای اطلاعات نسخه اپلیکیشن
|
||||||
|
/// </summary>
|
||||||
|
public record AppVersionDto
|
||||||
|
{
|
||||||
|
public bool Found { get; init; }
|
||||||
|
public string AppName { get; init; } = string.Empty;
|
||||||
|
public string CurrentVersion { get; init; } = string.Empty;
|
||||||
|
public string MinRequiredVersion { get; init; } = string.Empty;
|
||||||
|
public bool RequiresFullCacheClear { get; init; }
|
||||||
|
public bool RequiresUpdate { get; init; }
|
||||||
|
public string? UpdateMessage { get; init; }
|
||||||
|
public string? ReleaseNotes { get; init; }
|
||||||
|
public DateTime? LastUpdated { get; init; }
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
using CMSMicroservice.Domain.Entities.Configuration;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.AppVersionCQ.Queries.GetAppVersion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler برای دریافت آخرین نسخه اپلیکیشن
|
||||||
|
/// </summary>
|
||||||
|
public class GetAppVersionQueryHandler : IRequestHandler<GetAppVersionQuery, AppVersionDto?>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetAppVersionQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AppVersionDto?> Handle(GetAppVersionQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var appVersion = await _context.AppVersions
|
||||||
|
.Where(v => v.AppName == request.AppName && v.IsActive && !v.IsDeleted)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (appVersion == null)
|
||||||
|
{
|
||||||
|
return new AppVersionDto
|
||||||
|
{
|
||||||
|
Found = false,
|
||||||
|
AppName = request.AppName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// مقایسه ورژن کلاینت با حداقل نسخه مورد نیاز
|
||||||
|
bool requiresUpdate = false;
|
||||||
|
if (!string.IsNullOrEmpty(request.CurrentClientVersion) && !string.IsNullOrEmpty(appVersion.MinRequiredVersion))
|
||||||
|
{
|
||||||
|
requiresUpdate = CompareVersions(request.CurrentClientVersion, appVersion.MinRequiredVersion) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AppVersionDto
|
||||||
|
{
|
||||||
|
Found = true,
|
||||||
|
AppName = appVersion.AppName,
|
||||||
|
CurrentVersion = appVersion.CurrentVersion,
|
||||||
|
MinRequiredVersion = appVersion.MinRequiredVersion,
|
||||||
|
RequiresFullCacheClear = appVersion.RequiresFullCacheClear,
|
||||||
|
RequiresUpdate = requiresUpdate,
|
||||||
|
UpdateMessage = appVersion.UpdateMessage,
|
||||||
|
ReleaseNotes = appVersion.ReleaseNotes,
|
||||||
|
LastUpdated = appVersion.LastModified ?? appVersion.Created
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// مقایسه دو نسخه (مثلاً 1.2.3 با 1.3.0)
|
||||||
|
/// برگشت: منفی = اولی کوچکتر، مثبت = اولی بزرگتر، صفر = برابر
|
||||||
|
/// </summary>
|
||||||
|
private static int CompareVersions(string version1, string version2)
|
||||||
|
{
|
||||||
|
var v1Parts = version1.Split('.').Select(s => int.TryParse(s, out var n) ? n : 0).ToArray();
|
||||||
|
var v2Parts = version2.Split('.').Select(s => int.TryParse(s, out var n) ? n : 0).ToArray();
|
||||||
|
|
||||||
|
var maxLen = Math.Max(v1Parts.Length, v2Parts.Length);
|
||||||
|
|
||||||
|
for (int i = 0; i < maxLen; i++)
|
||||||
|
{
|
||||||
|
var v1 = i < v1Parts.Length ? v1Parts[i] : 0;
|
||||||
|
var v2 = i < v2Parts.Length ? v2Parts[i] : 0;
|
||||||
|
|
||||||
|
if (v1 != v2)
|
||||||
|
return v1.CompareTo(v2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,12 +7,18 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
|
|
||||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2" />
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2" />
|
||||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" 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="System.Linq.Dynamic.Core" Version="1.6.10" />
|
||||||
|
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CMSMicroservice.Domain\CMSMicroservice.Domain.csproj" />
|
<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; }
|
||||||
|
}
|
||||||
+339
@@ -0,0 +1,339 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Common;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using CMSMicroservice.Domain.Entities.Club;
|
||||||
|
using CMSMicroservice.Domain.Entities.Commission;
|
||||||
|
using CMSMicroservice.Domain.Entities.History;
|
||||||
|
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)
|
||||||
|
/// 4. مبلغ را به Pool هفته جاری اضافه میکند
|
||||||
|
/// 5. ویژگیهای باشگاه را به کاربر اعطا میکند
|
||||||
|
/// </summary>
|
||||||
|
public class AcceptClubMembershipContractCommandHandler
|
||||||
|
: IRequestHandler<AcceptClubMembershipContractCommand, AcceptClubMembershipContractResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IConfiguration _cfg;
|
||||||
|
private readonly IHashService _hashService;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
private readonly IWeekDefinitionRepository _weekRepository;
|
||||||
|
private readonly ILogger<AcceptClubMembershipContractCommandHandler> _logger;
|
||||||
|
|
||||||
|
private const int MaxAttempts = 5;
|
||||||
|
private const string OtpPurpose = "signClubContract";
|
||||||
|
|
||||||
|
public AcceptClubMembershipContractCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IConfiguration cfg,
|
||||||
|
IHashService hashService,
|
||||||
|
ICurrentUserService currentUser,
|
||||||
|
IWeekDefinitionRepository weekRepository,
|
||||||
|
ILogger<AcceptClubMembershipContractCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_cfg = cfg;
|
||||||
|
_hashService = hashService;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
_weekRepository = weekRepository;
|
||||||
|
_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. دریافت مقادیر از SystemConstants (استاتیک)
|
||||||
|
long giftValue = SystemConstants.ClubMembershipGiftValue;
|
||||||
|
long activationFeeValue = SystemConstants.ClubActivationFee;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Using Club.MembershipGiftValue: {GiftValue}, Club.ActivationFee: {ActivationFee}",
|
||||||
|
giftValue, activationFeeValue
|
||||||
|
);
|
||||||
|
|
||||||
|
// 7. فعالسازی باشگاه مشتریان
|
||||||
|
ClubMembership clubMembership;
|
||||||
|
bool isNewMembership = user.ClubMembership == null;
|
||||||
|
var activationDate = DateTime.Now;
|
||||||
|
|
||||||
|
if (isNewMembership)
|
||||||
|
{
|
||||||
|
clubMembership = new ClubMembership
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
IsActive = true,
|
||||||
|
ActivatedAt = activationDate,
|
||||||
|
InitialContribution = activationFeeValue,
|
||||||
|
GiftValue = giftValue,
|
||||||
|
TotalEarned = 0,
|
||||||
|
PurchaseMethod = user.PackagePurchaseMethod
|
||||||
|
};
|
||||||
|
await _context.ClubMemberships.AddAsync(clubMembership, cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Created new club membership for UserId {UserId} via {Method}, GiftValue: {GiftValue}",
|
||||||
|
user.Id,
|
||||||
|
user.PackagePurchaseMethod,
|
||||||
|
giftValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
clubMembership = user.ClubMembership!;
|
||||||
|
clubMembership.IsActive = true;
|
||||||
|
clubMembership.ActivatedAt = activationDate;
|
||||||
|
clubMembership.PurchaseMethod = user.PackagePurchaseMethod;
|
||||||
|
_context.ClubMemberships.Update(clubMembership);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Reactivated club membership for UserId {UserId}",
|
||||||
|
user.Id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 8. ثبت تاریخچه
|
||||||
|
var history = new ClubMembershipHistory
|
||||||
|
{
|
||||||
|
ClubMembershipId = clubMembership.Id,
|
||||||
|
UserId = clubMembership.UserId,
|
||||||
|
OldIsActive = !isNewMembership && !user.ClubMembership!.IsActive,
|
||||||
|
NewIsActive = true,
|
||||||
|
Action = ClubMembershipAction.Activated,
|
||||||
|
Reason = isNewMembership
|
||||||
|
? $"Initial activation via contract signing - {user.PackagePurchaseMethod}"
|
||||||
|
: $"Reactivated via contract signing - {user.PackagePurchaseMethod}",
|
||||||
|
PerformedBy = _currentUser.GetPerformedBy()
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.ClubMembershipHistories.Add(history);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 9. اضافه کردن مبلغ به Pool هفته جاری
|
||||||
|
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
|
||||||
|
var weeklyPool = await _context.WeeklyCommissionPools
|
||||||
|
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId, cancellationToken);
|
||||||
|
|
||||||
|
if (weeklyPool == null)
|
||||||
|
{
|
||||||
|
weeklyPool = new WeeklyCommissionPool
|
||||||
|
{
|
||||||
|
WeekDefinitionId = currentWeekDefinitionId,
|
||||||
|
TotalPoolAmount = activationFeeValue,
|
||||||
|
TotalBalances = 0,
|
||||||
|
ValuePerBalance = 0,
|
||||||
|
IsCalculated = false,
|
||||||
|
CalculatedAt = null
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.WeeklyCommissionPools.AddAsync(weeklyPool, cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Created new WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId} with initial amount: {Amount}",
|
||||||
|
currentWeekDefinitionId,
|
||||||
|
activationFeeValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 10. اعطای ویژگیهای باشگاه برای کاربر (فقط برای عضویت جدید)
|
||||||
|
if (isNewMembership)
|
||||||
|
{
|
||||||
|
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
|
||||||
|
var clubFeatures = await _context.ClubFeatures
|
||||||
|
.Where(f => !f.IsDeleted && featureIds.Contains(f.Id))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (clubFeatures.Any())
|
||||||
|
{
|
||||||
|
var userClubFeatures = clubFeatures.Select(feature => new UserClubFeature
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
ClubMembershipId = clubMembership.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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Club membership contract accepted and activated for UserId: {UserId}, ContractId: {ContractId}, MembershipId: {MembershipId}",
|
||||||
|
request.UserId,
|
||||||
|
userContract.Id,
|
||||||
|
clubMembership.Id
|
||||||
|
);
|
||||||
|
|
||||||
|
return new AcceptClubMembershipContractResponseDto
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "قرارداد باشگاه مشتریان با موفقیت ثبت شد و عضویت شما فعال گردید",
|
||||||
|
ContractId = userContract.Id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت شناسه تعریف هفته جاری
|
||||||
|
/// </summary>
|
||||||
|
private long GetCurrentWeekDefinitionId()
|
||||||
|
{
|
||||||
|
var week = _weekRepository.GetCurrentWeek();
|
||||||
|
if (week == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("هفته جاری در سیستم تعریف نشده است");
|
||||||
|
}
|
||||||
|
return week.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("محتوای قرارداد الزامی است");
|
||||||
|
}
|
||||||
|
}
|
||||||
+106
-24
@@ -1,13 +1,16 @@
|
|||||||
using CMSMicroservice.Application.Common.Exceptions;
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.Common.Models;
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using CMSMicroservice.Domain.Common;
|
||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
using CMSMicroservice.Domain.Entities.Club;
|
using CMSMicroservice.Domain.Entities.Club;
|
||||||
|
using CMSMicroservice.Domain.Entities.Commission;
|
||||||
using CMSMicroservice.Domain.Entities.History;
|
using CMSMicroservice.Domain.Entities.History;
|
||||||
using CMSMicroservice.Domain.Enums;
|
using CMSMicroservice.Domain.Enums;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
|
namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
|
||||||
|
|
||||||
@@ -15,15 +18,18 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly ICurrentUserService _currentUser;
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
private readonly IWeekDefinitionRepository _weekRepository;
|
||||||
private readonly ILogger<ActivateClubMembershipCommandHandler> _logger;
|
private readonly ILogger<ActivateClubMembershipCommandHandler> _logger;
|
||||||
|
|
||||||
public ActivateClubMembershipCommandHandler(
|
public ActivateClubMembershipCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
ICurrentUserService currentUser,
|
ICurrentUserService currentUser,
|
||||||
|
IWeekDefinitionRepository weekRepository,
|
||||||
ILogger<ActivateClubMembershipCommandHandler> logger)
|
ILogger<ActivateClubMembershipCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_currentUser = currentUser;
|
_currentUser = currentUser;
|
||||||
|
_weekRepository = weekRepository;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +76,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
throw new NotFoundException("کیف پول کاربر یافت نشد");
|
throw new NotFoundException("کیف پول کاربر یافت نشد");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wallet.Balance < 56_000_000)
|
if (wallet.Balance < SystemConstants.BasePackageAmount)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(
|
_logger.LogWarning(
|
||||||
"User {UserId} has insufficient balance: {Balance}",
|
"User {UserId} has insufficient balance: {Balance}",
|
||||||
@@ -78,7 +84,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
wallet.Balance
|
wallet.Balance
|
||||||
);
|
);
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"برای فعالسازی باشگاه مشتریان باید حداقل 56 میلیون تومان موجودی اصلی داشته باشید"
|
$"برای فعالسازی باشگاه مشتریان باید حداقل {SystemConstants.BasePackageAmount:N0} ریال موجودی اصلی داشته باشید"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,33 +136,18 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
var existingMembership = await _context.ClubMemberships
|
var existingMembership = await _context.ClubMemberships
|
||||||
.FirstOrDefaultAsync(c => c.UserId == user.Id, cancellationToken);
|
.FirstOrDefaultAsync(c => c.UserId == user.Id, cancellationToken);
|
||||||
|
|
||||||
// 6.1. دریافت مبلغ هدیه از تنظیمات
|
// 6.1. دریافت مبلغ هدیه و هزینه فعالسازی از SystemConstants
|
||||||
var giftValueConfig = await _context.SystemConfigurations
|
long giftValue = SystemConstants.ClubMembershipGiftValue;
|
||||||
.FirstOrDefaultAsync(
|
long activationFeeValue = SystemConstants.ClubActivationFee;
|
||||||
c => c.Key == "Club.MembershipGiftValue" && c.IsActive,
|
|
||||||
cancellationToken
|
|
||||||
);
|
|
||||||
|
|
||||||
long giftValue = 25_200_000; // مقدار پیشفرض
|
|
||||||
if (giftValueConfig != null && long.TryParse(giftValueConfig.Value, out var configValue))
|
|
||||||
{
|
|
||||||
giftValue = configValue;
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Using Club.MembershipGiftValue from configuration: {GiftValue}",
|
"Using Club.MembershipGiftValue: {GiftValue}, Club.ActivationFee: {ActivationFee}",
|
||||||
giftValue
|
giftValue, activationFeeValue
|
||||||
);
|
);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"Club.MembershipGiftValue not found in configuration, using default: {GiftValue}",
|
|
||||||
giftValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ClubMembership entity;
|
ClubMembership entity;
|
||||||
bool isNewMembership = existingMembership == null;
|
bool isNewMembership = existingMembership == null;
|
||||||
var activationDate = DateTime.UtcNow;
|
var activationDate = DateTime.Now;
|
||||||
|
|
||||||
if (isNewMembership)
|
if (isNewMembership)
|
||||||
{
|
{
|
||||||
@@ -166,7 +157,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
ActivatedAt = activationDate,
|
ActivatedAt = activationDate,
|
||||||
InitialContribution = 56_000_000,
|
InitialContribution =activationFeeValue,
|
||||||
GiftValue = giftValue, // مقدار از تنظیمات
|
GiftValue = giftValue, // مقدار از تنظیمات
|
||||||
TotalEarned = 0,
|
TotalEarned = 0,
|
||||||
PurchaseMethod = user.PackagePurchaseMethod
|
PurchaseMethod = user.PackagePurchaseMethod
|
||||||
@@ -225,6 +216,84 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
_context.ClubMembershipHistories.Add(history);
|
_context.ClubMembershipHistories.Add(history);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
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 featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
|
||||||
|
var clubFeatures = await _context.ClubFeatures
|
||||||
|
.Where(f => !f.IsDeleted && featureIds.Contains(f.Id))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (clubFeatures.Any())
|
||||||
|
{
|
||||||
|
var userClubFeatures = clubFeatures.Select(feature => new UserClubFeature
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
ClubMembershipId = entity.Id,
|
||||||
|
ClubFeatureId = feature.Id,
|
||||||
|
GrantedAt = activationDate,
|
||||||
|
IsActive = true,
|
||||||
|
Notes = "اعطا شده بهطور خودکار هنگام فعالسازی"
|
||||||
|
}).ToList(); _context.UserClubFeatures.AddRange(userClubFeatures);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Granted {Count} club features to UserId {UserId}",
|
||||||
|
clubFeatures.Count,
|
||||||
|
user.Id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"No club features found to grant to UserId {UserId}",
|
||||||
|
user.Id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Club membership activated successfully. UserId: {UserId}, MembershipId: {MembershipId}",
|
"Club membership activated successfully. UserId: {UserId}, MembershipId: {MembershipId}",
|
||||||
user.Id,
|
user.Id,
|
||||||
@@ -243,4 +312,17 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
throw;
|
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,
|
UserId = request.UserId,
|
||||||
ClubMembershipId = membership.Id,
|
ClubMembershipId = membership.Id,
|
||||||
ClubFeatureId = request.FeatureId,
|
ClubFeatureId = request.FeatureId,
|
||||||
GrantedAt = request.GrantedAt ?? DateTime.UtcNow,
|
GrantedAt = request.GrantedAt ?? DateTime.Now,
|
||||||
Notes = request.Notes
|
Notes = request.Notes
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2
@@ -12,6 +12,7 @@ public class GetAllClubMembershipsQueryHandler : IRequestHandler<GetAllClubMembe
|
|||||||
public async Task<GetAllClubMembershipsResponseDto> Handle(GetAllClubMembershipsQuery request, CancellationToken cancellationToken)
|
public async Task<GetAllClubMembershipsResponseDto> Handle(GetAllClubMembershipsQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var query = _context.ClubMemberships
|
var query = _context.ClubMemberships
|
||||||
|
.Include(x => x.User)
|
||||||
.ApplyOrder(sortBy: request.SortBy)
|
.ApplyOrder(sortBy: request.SortBy)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
@@ -33,6 +34,7 @@ public class GetAllClubMembershipsQueryHandler : IRequestHandler<GetAllClubMembe
|
|||||||
{
|
{
|
||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
UserId = x.UserId,
|
UserId = x.UserId,
|
||||||
|
UserName = x.User != null ? $"{x.User.FirstName} {x.User.LastName}" : string.Empty,
|
||||||
IsActive = x.IsActive,
|
IsActive = x.IsActive,
|
||||||
ActivatedAt = x.ActivatedAt,
|
ActivatedAt = x.ActivatedAt,
|
||||||
InitialContribution = x.InitialContribution,
|
InitialContribution = x.InitialContribution,
|
||||||
|
|||||||
+1
@@ -10,6 +10,7 @@ public class GetAllClubMembershipsResponseModel
|
|||||||
{
|
{
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
public long UserId { get; set; }
|
public long UserId { get; set; }
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
public DateTime? ActivatedAt { get; set; }
|
public DateTime? ActivatedAt { get; set; }
|
||||||
public long InitialContribution { get; set; }
|
public long InitialContribution { get; set; }
|
||||||
|
|||||||
+25
-1
@@ -3,14 +3,18 @@ namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership
|
|||||||
public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQuery, ClubMembershipDto?>
|
public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQuery, ClubMembershipDto?>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ILogger<GetClubMembershipQueryHandler> _logger;
|
||||||
|
|
||||||
public GetClubMembershipQueryHandler(IApplicationDbContext context)
|
public GetClubMembershipQueryHandler(IApplicationDbContext context, ILogger<GetClubMembershipQueryHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ClubMembershipDto?> Handle(GetClubMembershipQuery request, CancellationToken cancellationToken)
|
public async Task<ClubMembershipDto?> Handle(GetClubMembershipQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("GetClubMembership called for UserId: {UserId}", request.UserId);
|
||||||
|
|
||||||
var membership = await _context.ClubMemberships
|
var membership = await _context.ClubMemberships
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(x => x.UserId == request.UserId)
|
.Where(x => x.UserId == request.UserId)
|
||||||
@@ -27,6 +31,26 @@ public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQu
|
|||||||
})
|
})
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
// اگر کاربر عضویت نداره، یک DTO با وضعیت غیرفعال برگردون
|
||||||
|
if (membership == null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No membership found for UserId: {UserId}, returning inactive status", request.UserId);
|
||||||
|
return new ClubMembershipDto
|
||||||
|
{
|
||||||
|
Id = 0,
|
||||||
|
UserId = request.UserId,
|
||||||
|
IsActive = false,
|
||||||
|
ActivatedAt = null,
|
||||||
|
InitialContribution = 0,
|
||||||
|
TotalEarned = 0,
|
||||||
|
Created = DateTimeOffset.UtcNow,
|
||||||
|
LastModified = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Membership found for UserId: {UserId}, IsActive: {IsActive}, Id: {Id}",
|
||||||
|
request.UserId, membership.IsActive, membership.Id);
|
||||||
|
|
||||||
return membership;
|
return membership;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ public class GetClubMembershipHistoryQueryHandler : IRequestHandler<GetClubMembe
|
|||||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
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);
|
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)
|
public async Task<GetClubStatisticsResponseDto> Handle(GetClubStatisticsQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.Now;
|
||||||
|
|
||||||
// Basic statistics
|
// Basic statistics
|
||||||
var totalMembers = await _context.ClubMemberships.CountAsync(cancellationToken);
|
var totalMembers = await _context.ClubMemberships.CountAsync(cancellationToken);
|
||||||
|
|||||||
+4
-4
@@ -33,10 +33,10 @@ public class ApproveWithdrawalCommandHandler : IRequestHandler<ApproveWithdrawal
|
|||||||
|
|
||||||
// Update status to Withdrawn (approved)
|
// Update status to Withdrawn (approved)
|
||||||
payout.Status = CommissionPayoutStatus.Withdrawn;
|
payout.Status = CommissionPayoutStatus.Withdrawn;
|
||||||
payout.WithdrawnAt = DateTime.UtcNow;
|
payout.WithdrawnAt = DateTime.Now;
|
||||||
payout.ProcessedBy = _currentUser.GetPerformedBy();
|
payout.ProcessedBy = _currentUser.GetPerformedBy();
|
||||||
payout.ProcessedAt = DateTime.UtcNow;
|
payout.ProcessedAt = DateTime.Now;
|
||||||
payout.LastModified = DateTime.UtcNow;
|
payout.LastModified = DateTime.Now;
|
||||||
|
|
||||||
// TODO: Add PayoutHistory record
|
// TODO: Add PayoutHistory record
|
||||||
// var history = new CommissionPayoutHistory
|
// var history = new CommissionPayoutHistory
|
||||||
@@ -51,7 +51,7 @@ public class ApproveWithdrawalCommandHandler : IRequestHandler<ApproveWithdrawal
|
|||||||
// Action = (int)CommissionPayoutAction.Approved,
|
// Action = (int)CommissionPayoutAction.Approved,
|
||||||
// PerformedBy = "Admin", // TODO: Get from authenticated user
|
// PerformedBy = "Admin", // TODO: Get from authenticated user
|
||||||
// Reason = request.Notes,
|
// Reason = request.Notes,
|
||||||
// Created = DateTime.UtcNow
|
// Created = DateTime.Now
|
||||||
// };
|
// };
|
||||||
// _context.CommissionPayoutHistories.Add(history);
|
// _context.CommissionPayoutHistories.Add(history);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ public record CalculateWeeklyBalancesCommand : IRequest<int>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// شماره هفته (فرمت: YYYY-Www مثل 2025-W01)
|
/// شماره هفته (فرمت: YYYY-Www مثل 2025-W01)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string WeekNumber { get; init; } = string.Empty;
|
public long WeekDefinitionId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// آیا محاسبه مجدد انجام شود؟ (پیشفرض: false)
|
/// آیا محاسبه مجدد انجام شود؟ (پیشفرض: false)
|
||||||
|
|||||||
+194
-84
@@ -1,24 +1,37 @@
|
|||||||
|
using CMSMicroservice.Domain.Common;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
|
namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
|
||||||
|
|
||||||
public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWeeklyBalancesCommand, int>
|
public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWeeklyBalancesCommand, int>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IWeekDefinitionRepository _weekRepository;
|
||||||
|
|
||||||
public CalculateWeeklyBalancesCommandHandler(IApplicationDbContext context)
|
public CalculateWeeklyBalancesCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IWeekDefinitionRepository weekRepository)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_weekRepository = weekRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> Handle(CalculateWeeklyBalancesCommand request, CancellationToken cancellationToken)
|
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
|
var existingBalances = await _context.NetworkWeeklyBalances
|
||||||
.Where(x => x.WeekNumber == request.WeekNumber)
|
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
if (existingBalances.Any() && !request.ForceRecalculate)
|
if (existingBalances.Any() && !request.ForceRecalculate)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"تعادلهای هفته {request.WeekNumber} قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید");
|
throw new InvalidOperationException($"تعادلهای هفته {request.WeekDefinitionId} قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید");
|
||||||
}
|
}
|
||||||
|
|
||||||
// حذف محاسبات قبلی در صورت ForceRecalculate
|
// حذف محاسبات قبلی در صورت ForceRecalculate
|
||||||
@@ -28,44 +41,48 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
|||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// دریافت کاربران فعال در شبکه
|
// ⭐ دریافت همه کاربرانی که عضو فعال باشگاه هستند
|
||||||
|
// بدون محدودیت زمانی - همه اعضای فعال کلاب باید کمیسیون بگیرند
|
||||||
|
var activeClubMemberUserIds = await _context.ClubMemberships
|
||||||
|
.Where(c => c.IsActive)
|
||||||
|
.Select(c => c.UserId)
|
||||||
|
.ToHashSetAsync(cancellationToken);
|
||||||
|
|
||||||
|
// دریافت کاربران فعال در شبکه که عضو باشگاه هستند
|
||||||
|
// نکته: شرط NetworkParentId.HasValue نداریم چون ریشه شبکه (اولین نفر) هم باید حساب بشه
|
||||||
var usersInNetwork = await _context.Users
|
var usersInNetwork = await _context.Users
|
||||||
.Where(x => x.NetworkParentId.HasValue)
|
.Where(x => activeClubMemberUserIds.Contains(x.Id))
|
||||||
.Select(x => new { x.Id })
|
.Select(x => new { x.Id })
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
// دریافت باقیماندههای هفته قبل
|
// دریافت باقیماندههای هفته قبل
|
||||||
var previousWeekNumber = GetPreviousWeekNumber(request.WeekNumber);
|
var previousWeekDefinitionId = GetPreviousWeekDefinitionId(request.WeekDefinitionId);
|
||||||
var previousWeekCarryovers = await _context.NetworkWeeklyBalances
|
Dictionary<long, (int LeftLegRemainder, int RightLegRemainder)> previousWeekCarryovers;
|
||||||
.Where(x => x.WeekNumber == previousWeekNumber)
|
|
||||||
.Select(x => new
|
if (previousWeekDefinitionId.HasValue)
|
||||||
{
|
{
|
||||||
x.UserId,
|
previousWeekCarryovers = await _context.NetworkWeeklyBalances
|
||||||
x.LeftLegRemainder,
|
.Where(x => x.WeekDefinitionId == previousWeekDefinitionId.Value)
|
||||||
x.RightLegRemainder
|
.ToDictionaryAsync(
|
||||||
})
|
x => x.UserId,
|
||||||
.ToDictionaryAsync(x => x.UserId, cancellationToken);
|
x => (x.LeftLegRemainder, x.RightLegRemainder),
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
previousWeekCarryovers = new Dictionary<long, (int, int)>();
|
||||||
|
}
|
||||||
|
|
||||||
var balancesList = new List<NetworkWeeklyBalance>();
|
var balancesList = new List<NetworkWeeklyBalance>();
|
||||||
var calculatedAt = DateTime.UtcNow;
|
var calculatedAt = DateTime.Now;
|
||||||
|
|
||||||
// خواندن یکباره Configuration ها (بهینهسازی - به جای N query)
|
// استفاده از SystemConstants (استاتیک - بدون کوئری به دیتابیس)
|
||||||
var configs = await _context.SystemConfigurations
|
|
||||||
.Where(x => x.IsActive && (
|
|
||||||
x.Key == "Club.ActivationFee" ||
|
|
||||||
x.Key == "Commission.WeeklyPoolContributionPercent" ||
|
|
||||||
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;
|
|
||||||
// سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل
|
// سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل
|
||||||
var maxBalancesPerLeg = int.Parse(configs.GetValueOrDefault("Commission.MaxWeeklyBalancesPerLeg", "300"));
|
var maxBalancesPerLeg = SystemConstants.CommissionMaxWeeklyBalancesPerLeg;
|
||||||
// حداکثر عمق شبکه برای شمارش اعضا (15 لول)
|
// حداکثر عمق شبکه برای شمارش اعضا (15 لول)
|
||||||
var maxNetworkLevel = int.Parse(configs.GetValueOrDefault("Commission.MaxNetworkLevel", "15"));
|
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
|
||||||
|
|
||||||
foreach (var user in usersInNetwork)
|
foreach (var user in usersInNetwork.OrderBy(o=>o.Id))
|
||||||
{
|
{
|
||||||
// دریافت باقیمانده هفته قبل
|
// دریافت باقیمانده هفته قبل
|
||||||
var leftCarryover = 0;
|
var leftCarryover = 0;
|
||||||
@@ -77,41 +94,46 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// محاسبه تعداد اعضای جدید در این هفته (تا maxNetworkLevel لول پایینتر)
|
// محاسبه تعداد اعضای جدید در این هفته (تا maxNetworkLevel لول پایینتر)
|
||||||
var leftNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Left, 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.WeekNumber, maxNetworkLevel, cancellationToken);
|
var rightNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Right, request.WeekDefinitionId, maxNetworkLevel, cancellationToken);
|
||||||
|
|
||||||
// محاسبه مجموع هر پا (جدید + باقیمانده)
|
// محاسبه مجموع هر پا (جدید + باقیمانده)
|
||||||
var leftTotal = leftNewMembers + leftCarryover;
|
var leftTotal = leftNewMembers + leftCarryover;
|
||||||
var rightTotal = rightNewMembers + rightCarryover;
|
var rightTotal = rightNewMembers + rightCarryover;
|
||||||
|
|
||||||
// ✅ اصلاح شده: اعمال سقف روی هر دست جداگانه (نه روی کل)
|
// ✅ مرحله 1: محاسبه تعادل اولیه (قبل از اعمال سقف)
|
||||||
// سقف 300 برای دست چپ + 300 برای دست راست = حداکثر 600 تعادل در هفته
|
// تعادل = کمترین مقدار بین چپ و راست
|
||||||
var cappedLeftTotal = Math.Min(leftTotal, maxBalancesPerLeg);
|
// مثال: چپ=500، راست=600 → تعادل=500
|
||||||
var cappedRightTotal = Math.Min(rightTotal, maxBalancesPerLeg);
|
var totalBalances = Math.Min(leftTotal, rightTotal);
|
||||||
|
|
||||||
// محاسبه تعادل (کمترین مقدار بعد از اعمال سقف)
|
// ✅ مرحله 2: محاسبه باقیمانده (قبل از سقف)
|
||||||
var totalBalances = Math.Min(cappedLeftTotal, cappedRightTotal);
|
// باقیمانده = اضافهای که یک طرف دارد
|
||||||
|
// مثال: چپ=500، راست=600، تعادل=500
|
||||||
|
// → باقی چپ = 500-500 = 0
|
||||||
|
// → باقی راست = 600-500 = 100 (میرود برای هفته بعد)
|
||||||
|
var leftRemainder = leftTotal - totalBalances;
|
||||||
|
var rightRemainder = rightTotal - totalBalances;
|
||||||
|
|
||||||
// محاسبه باقیمانده برای هفته بعد
|
// ✅ مرحله 3: اعمال سقف 300 (برای امتیاز نهایی)
|
||||||
// باقیمانده = مقداری که از سقف هر دست رد شده
|
// از تعادل، فقط 300 از هر طرف حساب میشود
|
||||||
// مثال: چپ=350، راست=450، سقف=300
|
// مثال: تعادل=500 → امتیاز=300
|
||||||
// cappedLeft = MIN(350, 300) = 300
|
// از چپ: 300 حساب میشود، 200 فلش میشود
|
||||||
// cappedRight = MIN(450, 300) = 300
|
// از راست: 300 حساب میشود، 200 فلش میشود
|
||||||
// totalBalances = MIN(300, 300) = 300
|
// جمع فلش = 400 (از بین میرود)
|
||||||
// leftRemainder = 350 - 300 = 50 (مازاد سقف)
|
var cappedBalances = Math.Min(totalBalances, maxBalancesPerLeg);
|
||||||
// rightRemainder = 450 - 300 = 150 (مازاد سقف)
|
|
||||||
var leftRemainder = leftTotal - cappedLeftTotal;
|
|
||||||
var rightRemainder = rightTotal - cappedRightTotal;
|
|
||||||
|
|
||||||
// محاسبه سهم استخر (20% از مجموع فعالسازیهای جدید کل شبکه)
|
// ✅ مرحله 4: محاسبه فلش (از هر دو طرف)
|
||||||
// طبق گفته دکتر: کل افراد جدید در شبکه × هزینه فعالسازی × 20%
|
var flushedPerSide = totalBalances - cappedBalances; // 500-300=200
|
||||||
var totalNewMembers = leftNewMembers + rightNewMembers;
|
var totalFlushed = flushedPerSide * 2; // 200×2=400 (از بین میرود)
|
||||||
var weeklyPoolContribution = (long)(totalNewMembers * activationFee * poolPercent);
|
|
||||||
|
// ⚠️ توجه: تعادل زیرمجموعه در این مرحله محاسبه نمیشه
|
||||||
|
// چون هنوز تمام تعادلها محاسبه نشدن
|
||||||
|
// بعد از ذخیره همه تعادلها، در یک حلقه دوم محاسبه خواهد شد
|
||||||
|
|
||||||
var balance = new NetworkWeeklyBalance
|
var balance = new NetworkWeeklyBalance
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
WeekNumber = request.WeekNumber,
|
WeekDefinitionId = request.WeekDefinitionId,
|
||||||
|
|
||||||
// اطلاعات جدید
|
// اطلاعات جدید
|
||||||
LeftLegNewMembers = leftNewMembers,
|
LeftLegNewMembers = leftNewMembers,
|
||||||
@@ -122,19 +144,26 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
|||||||
// مجموع
|
// مجموع
|
||||||
LeftLegTotal = leftTotal,
|
LeftLegTotal = leftTotal,
|
||||||
RightLegTotal = rightTotal,
|
RightLegTotal = rightTotal,
|
||||||
TotalBalances = totalBalances, // تعادل واقعی بعد از اعمال سقف روی هر دست
|
TotalBalances = cappedBalances, // امتیاز نهایی بعد از اعمال سقف 300
|
||||||
|
|
||||||
// باقیمانده برای هفته بعد (مازاد سقف هر دست)
|
// باقیمانده برای هفته بعد (اضافهای که یک طرف دارد)
|
||||||
LeftLegRemainder = leftRemainder,
|
LeftLegRemainder = leftRemainder,
|
||||||
RightLegRemainder = rightRemainder,
|
RightLegRemainder = rightRemainder,
|
||||||
|
|
||||||
|
// فلش (از دست رفته)
|
||||||
|
FlushedPerSide = flushedPerSide,
|
||||||
|
TotalFlushed = totalFlushed,
|
||||||
|
|
||||||
|
// تعادل زیرمجموعه - فعلاً 0 (بعد از ذخیره همه تعادلها محاسبه میشه)
|
||||||
|
SubordinateBalances = 0,
|
||||||
|
|
||||||
// فیلدهای قدیمی (deprecated) - برای سازگاری با کدهای قبلی
|
// فیلدهای قدیمی (deprecated) - برای سازگاری با کدهای قبلی
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
LeftLegBalances = leftTotal,
|
LeftLegBalances = leftTotal,
|
||||||
RightLegBalances = rightTotal,
|
RightLegBalances = rightTotal,
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
WeeklyPoolContribution = weeklyPoolContribution,
|
WeeklyPoolContribution = 0, // Pool در مرحله بعد محاسبه میشه
|
||||||
CalculatedAt = calculatedAt,
|
CalculatedAt = calculatedAt,
|
||||||
IsExpired = false
|
IsExpired = false
|
||||||
};
|
};
|
||||||
@@ -145,40 +174,54 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
|||||||
await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, cancellationToken);
|
await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, cancellationToken);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// ⭐ مرحله 2: محاسبه تعادل زیرمجموعه برای هر کاربر (تا 15 لول)
|
||||||
|
// حالا که همه تعادلها ذخیره شدن، میتونیم تعادل زیرمجموعه رو حساب کنیم
|
||||||
|
var balancesDictionary = balancesList.ToDictionary(x => x.UserId);
|
||||||
|
|
||||||
|
foreach (var balance in balancesList)
|
||||||
|
{
|
||||||
|
var subordinateBalances = await CalculateSubordinateBalancesAsync(
|
||||||
|
balance.UserId,
|
||||||
|
balancesDictionary,
|
||||||
|
maxNetworkLevel,
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
|
||||||
|
balance.SubordinateBalances = subordinateBalances;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ذخیره تعادلهای زیرمجموعه
|
||||||
|
_context.NetworkWeeklyBalances.UpdateRange(balancesList);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
return balancesList.Count;
|
return balancesList.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// شماره هفته قبل را محاسبه میکند
|
/// شناسه هفته قبل را از WeekDefinitionRepository میگیرد
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string GetPreviousWeekNumber(string currentWeekNumber)
|
private long? GetPreviousWeekDefinitionId(long currentWeekDefinitionId)
|
||||||
{
|
{
|
||||||
// مثال: "2025-W48" -> "2025-W47"
|
var currentWeekNumber = _weekRepository.GetGregorianWeekNumber(currentWeekDefinitionId);
|
||||||
var parts = currentWeekNumber.Split('-');
|
if (currentWeekNumber == null) return null;
|
||||||
var year = int.Parse(parts[0]);
|
|
||||||
var week = int.Parse(parts[1].Replace("W", ""));
|
|
||||||
|
|
||||||
week--;
|
var previousWeekNumber = _weekRepository.GetPreviousWeekNumber(currentWeekNumber);
|
||||||
if (week < 1)
|
if (previousWeekNumber == null) return null;
|
||||||
{
|
|
||||||
year--;
|
|
||||||
week = 52; // یا 53 بسته به سال
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{year}-W{week:D2}";
|
return _weekRepository.GetWeekDefinitionId(previousWeekNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// شمارش اعضای جدیدی که در این هفته به یک پا اضافه شدند
|
/// شمارش اعضای جدیدی که در این هفته به یک پا اضافه شدند
|
||||||
/// تا maxLevel لول پایینتر شمارش میشود
|
/// تا maxLevel لول پایینتر شمارش میشود
|
||||||
/// </summary>
|
/// </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 به بازه تاریخی
|
// تبدیل WeekNumber به بازه تاریخی
|
||||||
var (startDate, endDate) = GetWeekDateRange(weekNumber);
|
var startDateEndDate =_weekRepository.GetWeekDateRange(WeekDefinitionId);
|
||||||
|
|
||||||
// شمارش تمام اعضای زیرمجموعه که در این هفته فعال شدند (تا maxLevel لول)
|
// شمارش تمام اعضای زیرمجموعه که در این هفته فعال شدند (تا 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;
|
return count;
|
||||||
}
|
}
|
||||||
@@ -230,22 +273,89 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// تبدیل شماره هفته به بازه تاریخی
|
/// تبدیل شماره هفته به بازه تاریخی از WeekDefinitionRepository
|
||||||
/// </summary>
|
/// </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"
|
// پیدا کردن همه زیرمجموعهها تا maxLevel لول
|
||||||
var parts = weekNumber.Split('-');
|
var subordinates = await GetSubordinatesRecursive(userId, 1, maxLevel, cancellationToken);
|
||||||
var year = int.Parse(parts[0]);
|
|
||||||
var week = int.Parse(parts[1].Replace("W", ""));
|
|
||||||
|
|
||||||
// محاسبه اولین روز هفته (شنبه)
|
// جمع تعادلهای آنها
|
||||||
var jan1 = new DateTime(year, 1, 1);
|
var totalSubordinateBalances = 0;
|
||||||
var daysOffset = DayOfWeek.Saturday - jan1.DayOfWeek;
|
foreach (var subordinateId in subordinates)
|
||||||
var firstSaturday = jan1.AddDays(daysOffset);
|
{
|
||||||
var weekStart = firstSaturday.AddDays((week - 1) * 7);
|
if (allBalances.ContainsKey(subordinateId))
|
||||||
var weekEnd = weekStart.AddDays(6).AddHours(23).AddMinutes(59).AddSeconds(59);
|
{
|
||||||
|
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()
|
public CalculateWeeklyBalancesCommandValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.WeekNumber)
|
RuleFor(x => x.WeekDefinitionId)
|
||||||
.NotEmpty()
|
.NotNull()
|
||||||
|
.GreaterThan(0)
|
||||||
.WithMessage("شماره هفته نمیتواند خالی باشد")
|
.WithMessage("شماره هفته نمیتواند خالی باشد")
|
||||||
.Matches(@"^\d{4}-W\d{2}$")
|
;
|
||||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد (مثل 2025-W01)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ public record CalculateWeeklyCommissionPoolCommand : IRequest<long>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// شماره هفته (فرمت: YYYY-Www)
|
/// شماره هفته (فرمت: YYYY-Www)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string WeekNumber { get; init; } = string.Empty;
|
public long WeekDefinitionId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// آیا محاسبه مجدد انجام شود؟
|
/// آیا محاسبه مجدد انجام شود؟
|
||||||
|
|||||||
+261
-32
@@ -3,76 +3,305 @@ namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommi
|
|||||||
public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<CalculateWeeklyCommissionPoolCommand, long>
|
public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<CalculateWeeklyCommissionPoolCommand, long>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IWeekDefinitionRepository _weekRepository;
|
||||||
|
|
||||||
public CalculateWeeklyCommissionPoolCommandHandler(IApplicationDbContext context)
|
public CalculateWeeklyCommissionPoolCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IWeekDefinitionRepository weekRepository)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_weekRepository = weekRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<long> Handle(CalculateWeeklyCommissionPoolCommand request, CancellationToken cancellationToken)
|
public async Task<long> Handle(CalculateWeeklyCommissionPoolCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// بررسی وجود استخر قبلی
|
// تبدیل WeekNumber به WeekDefinitionId
|
||||||
var existingPool = await _context.WeeklyCommissionPools
|
var weekDefinitionId = _weekRepository.GetGregorianWeekNumber(request.WeekDefinitionId);
|
||||||
.FirstOrDefaultAsync(x => x.WeekNumber == request.WeekNumber, cancellationToken);
|
if (weekDefinitionId == null)
|
||||||
|
|
||||||
if (existingPool != null && existingPool.IsCalculated && !request.ForceRecalculate)
|
|
||||||
{
|
{
|
||||||
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
|
var weeklyBalances = await _context.NetworkWeeklyBalances
|
||||||
.Where(x => x.WeekNumber == request.WeekNumber)
|
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
if (!weeklyBalances.Any())
|
if (!weeklyBalances.Any())
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"تعادلهای هفته {request.WeekNumber} هنوز محاسبه نشده است. ابتدا CalculateWeeklyBalances را اجرا کنید");
|
throw new InvalidOperationException($"تعادلهای هفته {request.WeekDefinitionId} هنوز محاسبه نشده است. ابتدا CalculateWeeklyBalances را اجرا کنید");
|
||||||
}
|
}
|
||||||
|
|
||||||
// محاسبه مجموع مشارکتها در استخر
|
// ⭐ Pool از قبل پُر شده (توسط ActivateClubMembership)
|
||||||
var totalPoolAmount = weeklyBalances.Sum(x => x.WeeklyPoolContribution);
|
var totalPoolAmount = existingPool.TotalPoolAmount;
|
||||||
|
|
||||||
// محاسبه مجموع Balances
|
// محاسبه مجموع تعادلهای کل شبکه
|
||||||
var totalBalances = weeklyBalances.Sum(x => x.TotalBalances);
|
// نکته: SubordinateBalances اضافه نمیکنیم چون وقتی همه TotalBalances رو جمع میزنیم،
|
||||||
|
// خودش شامل بالانسهای زیرمجموعهها هم هست (تکراری نشه)
|
||||||
|
var totalBalancesInNetwork = weeklyBalances.Sum(x => x.TotalBalances);
|
||||||
|
|
||||||
// محاسبه ارزش هر Balance (تقسیم صحیح برای ریال)
|
// محاسبه ارزش هر امتیاز
|
||||||
long valuePerBalance = 0;
|
long valuePerBalance = 0;
|
||||||
if (totalBalances > 0)
|
if (totalBalancesInNetwork > 0)
|
||||||
{
|
{
|
||||||
valuePerBalance = totalPoolAmount / totalBalances;
|
valuePerBalance = totalPoolAmount / totalBalancesInNetwork;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingPool != null)
|
// بهروزرسانی Pool
|
||||||
{
|
existingPool.TotalBalances = totalBalancesInNetwork;
|
||||||
// بهروزرسانی
|
|
||||||
existingPool.TotalPoolAmount = totalPoolAmount;
|
|
||||||
existingPool.TotalBalances = totalBalances;
|
|
||||||
existingPool.ValuePerBalance = valuePerBalance;
|
existingPool.ValuePerBalance = valuePerBalance;
|
||||||
existingPool.IsCalculated = true;
|
existingPool.IsCalculated = true;
|
||||||
existingPool.CalculatedAt = DateTime.UtcNow;
|
existingPool.CalculatedAt = DateTime.Now;
|
||||||
|
|
||||||
_context.WeeklyCommissionPools.Update(existingPool);
|
_context.WeeklyCommissionPools.Update(existingPool);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// حذف پرداختهای قبلی در صورت ForceRecalculate
|
||||||
|
if (request.ForceRecalculate)
|
||||||
|
{
|
||||||
|
var oldPayouts = await _context.UserCommissionPayouts
|
||||||
|
.Where(p => p.WeekDefinitionId == request.WeekDefinitionId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (oldPayouts.Any())
|
||||||
|
{
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// ⭐ ۳. حذف پرداختها
|
||||||
|
_context.UserCommissionPayouts.RemoveRange(oldPayouts);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⭐ ثبت پرداخت برای کاربرانی که تعادل دارند
|
||||||
|
// تعادل شخصی + زیرمجموعه قبلاً در CalculateWeeklyBalances محاسبه شده
|
||||||
|
var payouts = new List<UserCommissionPayout>();
|
||||||
|
|
||||||
|
foreach (var balance in weeklyBalances)
|
||||||
{
|
{
|
||||||
// ایجاد جدید
|
// فقط تعادل شخصی (SubordinateBalances اضافه نمیشه چون در SUM کل شبکه خودش حساب میشه)
|
||||||
var pool = new WeeklyCommissionPool
|
var userBalance = balance.TotalBalances;
|
||||||
|
|
||||||
|
// اگر تعادل صفر است، نیازی به ثبت نیست
|
||||||
|
if (userBalance <= 0)
|
||||||
{
|
{
|
||||||
WeekNumber = request.WeekNumber,
|
continue;
|
||||||
TotalPoolAmount = totalPoolAmount,
|
}
|
||||||
TotalBalances = totalBalances,
|
|
||||||
|
// محاسبه مبلغ کمیسیون
|
||||||
|
var totalAmount = (long)(userBalance * valuePerBalance);
|
||||||
|
|
||||||
|
var payout = new UserCommissionPayout
|
||||||
|
{
|
||||||
|
UserId = balance.UserId,
|
||||||
|
WeekDefinitionId = request.WeekDefinitionId,
|
||||||
|
WeeklyPoolId = existingPool.Id,
|
||||||
|
BalancesEarned = userBalance,
|
||||||
ValuePerBalance = valuePerBalance,
|
ValuePerBalance = valuePerBalance,
|
||||||
IsCalculated = true,
|
TotalAmount = totalAmount,
|
||||||
CalculatedAt = DateTime.UtcNow
|
Status = CommissionPayoutStatus.Pending,
|
||||||
|
PaidAt = null,
|
||||||
|
WithdrawalMethod = null,
|
||||||
|
IbanNumber = null,
|
||||||
|
WithdrawnAt = null
|
||||||
};
|
};
|
||||||
|
|
||||||
await _context.WeeklyCommissionPools.AddAsync(pool, cancellationToken);
|
payouts.Add(payout);
|
||||||
existingPool = pool;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (payouts.Any())
|
||||||
|
{
|
||||||
|
await _context.UserCommissionPayouts.AddRangeAsync(payouts, cancellationToken);
|
||||||
await _context.SaveChangesAsync(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;
|
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()
|
public CalculateWeeklyCommissionPoolCommandValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.WeekNumber)
|
RuleFor(x => x.WeekDefinitionId)
|
||||||
.NotEmpty()
|
.NotNull()
|
||||||
.WithMessage("شماره هفته نمیتواند خالی باشد")
|
.GreaterThan(0)
|
||||||
.Matches(@"^\d{4}-W\d{2}$")
|
.WithMessage("شماره هفته نمیتواند خالی باشد");
|
||||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ public record ProcessUserPayoutsCommand : IRequest<int>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// شماره هفته
|
/// شماره هفته
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string WeekNumber { get; init; } = string.Empty;
|
public long WeekDefinitionId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// آیا پرداخت مجدد انجام شود؟
|
/// آیا پرداخت مجدد انجام شود؟
|
||||||
|
|||||||
+25
-17
@@ -1,33 +1,45 @@
|
|||||||
|
using CMSMicroservice.Domain.Common;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
|
namespace CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
|
||||||
|
|
||||||
public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayoutsCommand, int>
|
public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayoutsCommand, int>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IWeekDefinitionRepository _weekRepository;
|
||||||
|
|
||||||
public ProcessUserPayoutsCommandHandler(IApplicationDbContext context)
|
public ProcessUserPayoutsCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IWeekDefinitionRepository weekRepository)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_weekRepository = weekRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> Handle(ProcessUserPayoutsCommand request, CancellationToken cancellationToken)
|
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
|
var pool = await _context.WeeklyCommissionPools
|
||||||
.FirstOrDefaultAsync(x => x.WeekNumber == request.WeekNumber, cancellationToken);
|
.FirstOrDefaultAsync(x => x.WeekDefinitionId == request.WeekDefinitionId, cancellationToken);
|
||||||
|
|
||||||
if (pool == null || !pool.IsCalculated)
|
if (pool == null || !pool.IsCalculated)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"استخر کمیسیون هفته {request.WeekNumber} هنوز محاسبه نشده است");
|
throw new InvalidOperationException($"استخر کمیسیون هفته {request.WeekDefinitionId} هنوز محاسبه نشده است");
|
||||||
}
|
}
|
||||||
|
|
||||||
// بررسی پرداخت قبلی
|
// بررسی پرداخت قبلی
|
||||||
var existingPayouts = await _context.UserCommissionPayouts
|
var existingPayouts = await _context.UserCommissionPayouts
|
||||||
.Where(x => x.WeekNumber == request.WeekNumber)
|
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
if (existingPayouts.Any() && !request.ForceReprocess)
|
if (existingPayouts.Any() && !request.ForceReprocess)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"پرداختهای هفته {request.WeekNumber} قبلاً انجام شده است");
|
throw new InvalidOperationException($"پرداختهای هفته {request.WeekDefinitionId} قبلاً انجام شده است");
|
||||||
}
|
}
|
||||||
|
|
||||||
// حذف پرداختهای قبلی در صورت ForceReprocess
|
// حذف پرداختهای قبلی در صورت ForceReprocess
|
||||||
@@ -37,21 +49,17 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
|||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐ خواندن MaxNetworkLevel از Config
|
// ⭐ خواندن MaxNetworkLevel از SystemConstants (استاتیک)
|
||||||
var maxNetworkLevelConfig = await _context.SystemConfigurations
|
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
|
||||||
.Where(x => x.Key == "Commission.MaxNetworkLevel" && x.IsActive)
|
|
||||||
.Select(x => x.Value)
|
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
|
||||||
var maxNetworkLevel = int.Parse(maxNetworkLevelConfig ?? "15");
|
|
||||||
|
|
||||||
// دریافت همه تعادلهای هفتگی (شامل صفرها هم برای محاسبه زیرمجموعه)
|
// دریافت همه تعادلهای هفتگی (شامل صفرها هم برای محاسبه زیرمجموعه)
|
||||||
var allWeeklyBalances = await _context.NetworkWeeklyBalances
|
var allWeeklyBalances = await _context.NetworkWeeklyBalances
|
||||||
.Where(x => x.WeekNumber == request.WeekNumber)
|
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId)
|
||||||
.ToDictionaryAsync(x => x.UserId, cancellationToken);
|
.ToDictionaryAsync(x => x.UserId, cancellationToken);
|
||||||
|
|
||||||
// دریافت کاربرانی که تعادل > 0 دارند (یا زیرمجموعهشان دارد)
|
// دریافت کاربرانی که تعادل > 0 دارند (یا زیرمجموعهشان دارد)
|
||||||
var usersWithBalances = await _context.NetworkWeeklyBalances
|
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)
|
.Select(x => x.UserId)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -82,7 +90,7 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
|||||||
// ⭐ محاسبه مجموع تعادلهای زیرمجموعه تا maxNetworkLevel لول
|
// ⭐ محاسبه مجموع تعادلهای زیرمجموعه تا maxNetworkLevel لول
|
||||||
var subordinateBalances = await CalculateSubordinateBalancesAsync(
|
var subordinateBalances = await CalculateSubordinateBalancesAsync(
|
||||||
userId,
|
userId,
|
||||||
request.WeekNumber,
|
request.WeekDefinitionId,
|
||||||
allWeeklyBalances,
|
allWeeklyBalances,
|
||||||
maxNetworkLevel,
|
maxNetworkLevel,
|
||||||
cancellationToken
|
cancellationToken
|
||||||
@@ -103,7 +111,7 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
|||||||
var payout = new UserCommissionPayout
|
var payout = new UserCommissionPayout
|
||||||
{
|
{
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
WeekNumber = request.WeekNumber,
|
WeekDefinitionId = request.WeekDefinitionId,
|
||||||
WeeklyPoolId = pool.Id,
|
WeeklyPoolId = pool.Id,
|
||||||
BalancesEarned = totalBalancesWithSubordinates, // ⭐ شامل زیرمجموعه
|
BalancesEarned = totalBalancesWithSubordinates, // ⭐ شامل زیرمجموعه
|
||||||
ValuePerBalance = pool.ValuePerBalance,
|
ValuePerBalance = pool.ValuePerBalance,
|
||||||
@@ -129,7 +137,7 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
|||||||
{
|
{
|
||||||
UserCommissionPayoutId = payout.Id,
|
UserCommissionPayoutId = payout.Id,
|
||||||
UserId = payout.UserId,
|
UserId = payout.UserId,
|
||||||
WeekNumber = request.WeekNumber,
|
WeekDefinitionId = request.WeekDefinitionId,
|
||||||
AmountBefore = 0,
|
AmountBefore = 0,
|
||||||
AmountAfter = payout.TotalAmount,
|
AmountAfter = payout.TotalAmount,
|
||||||
OldStatus = default(CommissionPayoutStatus),
|
OldStatus = default(CommissionPayoutStatus),
|
||||||
@@ -180,7 +188,7 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<int> CalculateSubordinateBalancesAsync(
|
private async Task<int> CalculateSubordinateBalancesAsync(
|
||||||
long userId,
|
long userId,
|
||||||
string weekNumber,
|
long WeekDefinitionId,
|
||||||
Dictionary<long, NetworkWeeklyBalance> allBalances,
|
Dictionary<long, NetworkWeeklyBalance> allBalances,
|
||||||
int maxLevel,
|
int maxLevel,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
|||||||
+4
-5
@@ -4,11 +4,10 @@ public class ProcessUserPayoutsCommandValidator : AbstractValidator<ProcessUserP
|
|||||||
{
|
{
|
||||||
public ProcessUserPayoutsCommandValidator()
|
public ProcessUserPayoutsCommandValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.WeekNumber)
|
RuleFor(x => x.WeekDefinitionId)
|
||||||
.NotEmpty()
|
.NotNull()
|
||||||
.WithMessage("شماره هفته نمیتواند خالی باشد")
|
.GreaterThan(0)
|
||||||
.Matches(@"^\d{4}-W\d{2}$")
|
.WithMessage("شماره هفته نمیتواند خالی باشد");
|
||||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
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 oldStatus = payout.Status;
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.Now;
|
||||||
|
|
||||||
if (request.IsApproved)
|
if (request.IsApproved)
|
||||||
{
|
{
|
||||||
@@ -73,7 +73,7 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
|
|||||||
UserId = payout.UserId,
|
UserId = payout.UserId,
|
||||||
Iban = payout.IbanNumber ?? throw new InvalidOperationException("شماره شبا یافت نشد"),
|
Iban = payout.IbanNumber ?? throw new InvalidOperationException("شماره شبا یافت نشد"),
|
||||||
AccountHolderName = $"{payout.User.FirstName} {payout.User.LastName}",
|
AccountHolderName = $"{payout.User.FirstName} {payout.User.LastName}",
|
||||||
Description = $"برداشت کمیسیون هفته {payout.WeekNumber}",
|
Description = $"برداشت کمیسیون هفته {payout.WeekDefinitionId}",
|
||||||
InternalRefId = $"PAYOUT-{payout.Id}"
|
InternalRefId = $"PAYOUT-{payout.Id}"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
|
|||||||
{
|
{
|
||||||
UserCommissionPayoutId = payout.Id,
|
UserCommissionPayoutId = payout.Id,
|
||||||
UserId = payout.UserId,
|
UserId = payout.UserId,
|
||||||
WeekNumber = payout.WeekNumber,
|
WeekDefinitionId = payout.WeekDefinitionId,
|
||||||
AmountBefore = payout.TotalAmount,
|
AmountBefore = payout.TotalAmount,
|
||||||
AmountAfter = payout.TotalAmount,
|
AmountAfter = payout.TotalAmount,
|
||||||
OldStatus = oldStatus,
|
OldStatus = oldStatus,
|
||||||
@@ -147,7 +147,7 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
|
|||||||
{
|
{
|
||||||
UserCommissionPayoutId = payout.Id,
|
UserCommissionPayoutId = payout.Id,
|
||||||
UserId = payout.UserId,
|
UserId = payout.UserId,
|
||||||
WeekNumber = payout.WeekNumber,
|
WeekDefinitionId = payout.WeekDefinitionId,
|
||||||
AmountBefore = payout.TotalAmount,
|
AmountBefore = payout.TotalAmount,
|
||||||
AmountAfter = payout.TotalAmount,
|
AmountAfter = payout.TotalAmount,
|
||||||
OldStatus = oldStatus,
|
OldStatus = oldStatus,
|
||||||
|
|||||||
+3
-3
@@ -34,9 +34,9 @@ public class RejectWithdrawalCommandHandler : IRequestHandler<RejectWithdrawalCo
|
|||||||
// Update status to Cancelled (rejected)
|
// Update status to Cancelled (rejected)
|
||||||
payout.Status = CommissionPayoutStatus.Cancelled;
|
payout.Status = CommissionPayoutStatus.Cancelled;
|
||||||
payout.ProcessedBy = _currentUser.GetPerformedBy();
|
payout.ProcessedBy = _currentUser.GetPerformedBy();
|
||||||
payout.ProcessedAt = DateTime.UtcNow;
|
payout.ProcessedAt = DateTime.Now;
|
||||||
payout.RejectionReason = request.Reason;
|
payout.RejectionReason = request.Reason;
|
||||||
payout.LastModified = DateTime.UtcNow;
|
payout.LastModified = DateTime.Now;
|
||||||
|
|
||||||
// TODO: Add PayoutHistory record with rejection reason
|
// TODO: Add PayoutHistory record with rejection reason
|
||||||
// var history = new CommissionPayoutHistory
|
// var history = new CommissionPayoutHistory
|
||||||
@@ -51,7 +51,7 @@ public class RejectWithdrawalCommandHandler : IRequestHandler<RejectWithdrawalCo
|
|||||||
// Action = (int)CommissionPayoutAction.Rejected,
|
// Action = (int)CommissionPayoutAction.Rejected,
|
||||||
// PerformedBy = "Admin", // TODO: Get from authenticated user
|
// PerformedBy = "Admin", // TODO: Get from authenticated user
|
||||||
// Reason = request.Reason,
|
// Reason = request.Reason,
|
||||||
// Created = DateTime.UtcNow
|
// Created = DateTime.Now
|
||||||
// };
|
// };
|
||||||
// _context.CommissionPayoutHistories.Add(history);
|
// _context.CommissionPayoutHistories.Add(history);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawal
|
|||||||
{
|
{
|
||||||
UserCommissionPayoutId = payout.Id,
|
UserCommissionPayoutId = payout.Id,
|
||||||
UserId = payout.UserId,
|
UserId = payout.UserId,
|
||||||
WeekNumber = payout.WeekNumber,
|
WeekDefinitionId = payout.WeekDefinitionId,
|
||||||
AmountBefore = payout.TotalAmount,
|
AmountBefore = payout.TotalAmount,
|
||||||
AmountAfter = payout.TotalAmount,
|
AmountAfter = payout.TotalAmount,
|
||||||
OldStatus = oldStatus,
|
OldStatus = oldStatus,
|
||||||
|
|||||||
+3
-8
@@ -5,7 +5,7 @@ public record TriggerWeeklyCalculationCommand : IRequest<TriggerWeeklyCalculatio
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// شماره هفته (فرمت: "YYYY-Www")
|
/// شماره هفته (فرمت: "YYYY-Www")
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string WeekNumber { get; init; } = string.Empty;
|
public long WeekDefinitionId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// اگر true باشد، محاسبات قبلی را حذف و دوباره محاسبه میکند
|
/// اگر true باشد، محاسبات قبلی را حذف و دوباره محاسبه میکند
|
||||||
@@ -13,17 +13,12 @@ public record TriggerWeeklyCalculationCommand : IRequest<TriggerWeeklyCalculatio
|
|||||||
public bool ForceRecalculate { get; init; }
|
public bool ForceRecalculate { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Skip balance calculation
|
/// Skip balance calculation (Step 1)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SkipBalances { get; init; }
|
public bool SkipBalances { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Skip pool calculation
|
/// Skip pool calculation and payout processing (Step 2)
|
||||||
/// </summary>
|
|
||||||
public bool SkipPool { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Skip payout processing
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SkipPayouts { get; init; }
|
public bool SkipPayouts { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-29
@@ -1,7 +1,6 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
|
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
|
||||||
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
|
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
|
||||||
using CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.CommissionCQ.Commands.TriggerWeeklyCalculation;
|
namespace CMSMicroservice.Application.CommissionCQ.Commands.TriggerWeeklyCalculation;
|
||||||
|
|
||||||
@@ -9,13 +8,16 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
private readonly ICommissionCalculationStrategyFactory _strategyFactory;
|
||||||
|
|
||||||
public TriggerWeeklyCalculationCommandHandler(
|
public TriggerWeeklyCalculationCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IMediator mediator)
|
IMediator mediator,
|
||||||
|
ICommissionCalculationStrategyFactory strategyFactory)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
_strategyFactory = strategyFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TriggerWeeklyCalculationResponseDto> Handle(
|
public async Task<TriggerWeeklyCalculationResponseDto> Handle(
|
||||||
@@ -23,12 +25,12 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var executionId = Guid.NewGuid().ToString();
|
var executionId = Guid.NewGuid().ToString();
|
||||||
var startedAt = DateTime.UtcNow;
|
var startedAt = DateTime.Now;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Validate week number format
|
// Validate week number format
|
||||||
if (string.IsNullOrWhiteSpace(request.WeekNumber))
|
if (request.WeekDefinitionId <= 0)
|
||||||
{
|
{
|
||||||
return new TriggerWeeklyCalculationResponseDto
|
return new TriggerWeeklyCalculationResponseDto
|
||||||
{
|
{
|
||||||
@@ -41,42 +43,36 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
|
|||||||
|
|
||||||
var steps = new List<string>();
|
var steps = new List<string>();
|
||||||
|
|
||||||
// Step 1: Calculate Weekly Balances
|
// ⭐ دریافت استراتژی براساس Config (ORM یا SP)
|
||||||
|
var strategy = await _strategyFactory.CreateStrategyAsync(cancellationToken);
|
||||||
|
var strategyName = strategy.GetType().Name.Contains("StoredProcedure") ? "SP" : "ORM";
|
||||||
|
|
||||||
|
// Step 1: Calculate Weekly Balances (تا 15 لول)
|
||||||
if (!request.SkipBalances)
|
if (!request.SkipBalances)
|
||||||
{
|
{
|
||||||
await _mediator.Send(new CalculateWeeklyBalancesCommand
|
var balancesCount = await strategy.CalculateWeeklyBalancesAsync(
|
||||||
{
|
request.WeekDefinitionId,
|
||||||
WeekNumber = request.WeekNumber,
|
request.ForceRecalculate,
|
||||||
ForceRecalculate = request.ForceRecalculate
|
cancellationToken);
|
||||||
}, cancellationToken);
|
|
||||||
steps.Add("محاسبه امتیازات هفتگی");
|
steps.Add($"محاسبه امتیازات هفتگی ({balancesCount} کاربر)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Calculate Weekly Commission Pool
|
// Step 2: Calculate Pool & Process Payouts (محاسبه استخر + پرداخت کاربران)
|
||||||
if (!request.SkipPool)
|
|
||||||
{
|
|
||||||
await _mediator.Send(new CalculateWeeklyCommissionPoolCommand
|
|
||||||
{
|
|
||||||
WeekNumber = request.WeekNumber
|
|
||||||
}, cancellationToken);
|
|
||||||
steps.Add("محاسبه استخر کمیسیون");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Process User Payouts
|
|
||||||
if (!request.SkipPayouts)
|
if (!request.SkipPayouts)
|
||||||
{
|
{
|
||||||
await _mediator.Send(new ProcessUserPayoutsCommand
|
var poolId = await strategy.CalculateWeeklyCommissionPoolAsync(
|
||||||
{
|
request.WeekDefinitionId,
|
||||||
WeekNumber = request.WeekNumber,
|
request.ForceRecalculate,
|
||||||
ForceReprocess = request.ForceRecalculate
|
cancellationToken);
|
||||||
}, cancellationToken);
|
|
||||||
steps.Add("پردازش پرداختهای کاربران");
|
steps.Add($"محاسبه استخر و پرداخت کاربران (Pool: {poolId})");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new TriggerWeeklyCalculationResponseDto
|
return new TriggerWeeklyCalculationResponseDto
|
||||||
{
|
{
|
||||||
Success = true,
|
Success = true,
|
||||||
Message = $"محاسبات هفته {request.WeekNumber} با موفقیت انجام شد. مراحل: {string.Join(", ", steps)}",
|
Message = $"محاسبات هفته {request.WeekDefinitionId} با موفقیت انجام شد [{strategyName}]. مراحل: {string.Join(", ", steps)}",
|
||||||
ExecutionId = executionId,
|
ExecutionId = executionId,
|
||||||
StartedAt = startedAt
|
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>
|
||||||
/// از هفته (فیلتر اختیاری)
|
/// از هفته (فیلتر اختیاری)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? FromWeek { get; init; }
|
public int? FromWeekOrder { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// تا هفته (فیلتر اختیاری)
|
/// تا هفته (فیلتر اختیاری)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ToWeek { get; init; }
|
public int? ToWeekOrder { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// فقط Pool های محاسبه شده
|
/// فقط Pool های محاسبه شده
|
||||||
|
|||||||
+28
-8
@@ -3,25 +3,31 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools;
|
|||||||
public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQuery, GetAllWeeklyPoolsResponseDto>
|
public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQuery, GetAllWeeklyPoolsResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
|
|
||||||
public GetAllWeeklyPoolsQueryHandler(IApplicationDbContext context)
|
public GetAllWeeklyPoolsQueryHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IWeekDefinitionRepository weekDefinitionRepository)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetAllWeeklyPoolsResponseDto> Handle(GetAllWeeklyPoolsQuery request, CancellationToken cancellationToken)
|
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
|
// 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)
|
if (request.OnlyCalculated.HasValue && request.OnlyCalculated.Value)
|
||||||
@@ -30,7 +36,7 @@ public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Order by week number descending (newest first)
|
// Order by week number descending (newest first)
|
||||||
query = query.OrderByDescending(x => x.WeekNumber);
|
query = query.OrderByDescending(x => x.WeekDefinitionId);
|
||||||
|
|
||||||
// Count total
|
// Count total
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
@@ -42,7 +48,8 @@ public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQu
|
|||||||
.Select(x => new WeeklyCommissionPoolDto
|
.Select(x => new WeeklyCommissionPoolDto
|
||||||
{
|
{
|
||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
WeekNumber = x.WeekNumber,
|
WeekDefinitionId = x.WeekDefinitionId,
|
||||||
|
WeekDisplayName = x.WeekDefinition.DisplayName,
|
||||||
TotalPoolAmount = x.TotalPoolAmount,
|
TotalPoolAmount = x.TotalPoolAmount,
|
||||||
TotalBalances = x.TotalBalances,
|
TotalBalances = x.TotalBalances,
|
||||||
ValuePerBalance = x.ValuePerBalance,
|
ValuePerBalance = x.ValuePerBalance,
|
||||||
@@ -52,6 +59,19 @@ public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQu
|
|||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.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
|
return new GetAllWeeklyPoolsResponseDto
|
||||||
{
|
{
|
||||||
MetaData = new MetaDataDto
|
MetaData = new MetaDataDto
|
||||||
|
|||||||
+2
-1
@@ -9,7 +9,8 @@ public record GetAllWeeklyPoolsResponseDto
|
|||||||
public record WeeklyCommissionPoolDto
|
public record WeeklyCommissionPoolDto
|
||||||
{
|
{
|
||||||
public long Id { get; init; }
|
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 long TotalPoolAmount { get; init; }
|
||||||
public int TotalBalances { get; init; }
|
public int TotalBalances { get; init; }
|
||||||
public long ValuePerBalance { 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>
|
||||||
/// شماره هفته (اختیاری)
|
/// شماره هفته (اختیاری)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? WeekNumber { get; init; }
|
public long? WeekDefinitionId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// مرتبسازی
|
/// مرتبسازی
|
||||||
|
|||||||
+17
-5
@@ -3,15 +3,20 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetCommissionPayoutHi
|
|||||||
public class GetCommissionPayoutHistoryQueryHandler : IRequestHandler<GetCommissionPayoutHistoryQuery, GetCommissionPayoutHistoryResponseDto>
|
public class GetCommissionPayoutHistoryQueryHandler : IRequestHandler<GetCommissionPayoutHistoryQuery, GetCommissionPayoutHistoryResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
|
|
||||||
public GetCommissionPayoutHistoryQueryHandler(IApplicationDbContext context)
|
public GetCommissionPayoutHistoryQueryHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IWeekDefinitionRepository weekDefinitionRepository)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetCommissionPayoutHistoryResponseDto> Handle(GetCommissionPayoutHistoryQuery request, CancellationToken cancellationToken)
|
public async Task<GetCommissionPayoutHistoryResponseDto> Handle(GetCommissionPayoutHistoryQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var query = _context.CommissionPayoutHistories
|
var query = _context.CommissionPayoutHistories
|
||||||
|
.Include(i=>i.WeekDefinition)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
@@ -26,12 +31,12 @@ public class GetCommissionPayoutHistoryQueryHandler : IRequestHandler<GetCommiss
|
|||||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
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);
|
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||||
|
|
||||||
@@ -42,7 +47,8 @@ public class GetCommissionPayoutHistoryQueryHandler : IRequestHandler<GetCommiss
|
|||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
UserCommissionPayoutId = x.UserCommissionPayoutId,
|
UserCommissionPayoutId = x.UserCommissionPayoutId,
|
||||||
UserId = x.UserId,
|
UserId = x.UserId,
|
||||||
WeekNumber = x.WeekNumber,
|
WeekDefinitionId = x.WeekDefinitionId,
|
||||||
|
WeekDisplayName=x.WeekDefinition!.DisplayName,
|
||||||
AmountBefore = x.AmountBefore,
|
AmountBefore = x.AmountBefore,
|
||||||
AmountAfter = x.AmountAfter,
|
AmountAfter = x.AmountAfter,
|
||||||
OldStatus = x.OldStatus,
|
OldStatus = x.OldStatus,
|
||||||
@@ -54,6 +60,12 @@ public class GetCommissionPayoutHistoryQueryHandler : IRequestHandler<GetCommiss
|
|||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
// // Populate WeekDisplayName from cache
|
||||||
|
// foreach (var model in models)
|
||||||
|
// {
|
||||||
|
// model.WeekDisplayName = _weekDefinitionRepository.GetDisplayNameByGregorianWeekNumber(model.WeekNumber);
|
||||||
|
// }
|
||||||
|
|
||||||
return new GetCommissionPayoutHistoryResponseDto
|
return new GetCommissionPayoutHistoryResponseDto
|
||||||
{
|
{
|
||||||
MetaData = meta,
|
MetaData = meta,
|
||||||
|
|||||||
+4
-5
@@ -13,11 +13,10 @@ public class GetCommissionPayoutHistoryQueryValidator : AbstractValidator<GetCom
|
|||||||
.GreaterThan(0)
|
.GreaterThan(0)
|
||||||
.WithMessage("شناسه کاربر معتبر نیست")
|
.WithMessage("شناسه کاربر معتبر نیست")
|
||||||
.When(x => x.UserId.HasValue);
|
.When(x => x.UserId.HasValue);
|
||||||
|
RuleFor(x => x.WeekDefinitionId)
|
||||||
RuleFor(x => x.WeekNumber)
|
.NotNull()
|
||||||
.Matches(@"^\d{4}-W\d{2}$")
|
.GreaterThan(0)
|
||||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد")
|
.WithMessage("شماره هفته نمیتواند خالی باشد");
|
||||||
.When(x => !string.IsNullOrEmpty(x.WeekNumber));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
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 Id { get; set; }
|
||||||
public long UserCommissionPayoutId { get; set; }
|
public long UserCommissionPayoutId { get; set; }
|
||||||
public long UserId { 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 AmountBefore { get; set; }
|
||||||
public long AmountAfter { get; set; }
|
public long AmountAfter { get; set; }
|
||||||
public CommissionPayoutStatus? OldStatus { get; set; }
|
public CommissionPayoutStatus? OldStatus { get; set; }
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query برای دریافت پرداختهای کمیسیون کاربر جاری (از JWT)
|
||||||
|
/// </summary>
|
||||||
|
public record GetMyCommissionPayoutsQuery : IRequest<GetMyCommissionPayoutsResponseDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// فیلتر وضعیت
|
||||||
|
/// </summary>
|
||||||
|
public CommissionPayoutStatus? Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شماره هفته (اختیاری)
|
||||||
|
/// </summary>
|
||||||
|
public long? WeekDefinitionId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pagination
|
||||||
|
/// </summary>
|
||||||
|
public PaginationState? PaginationState { get; init; }
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Extensions;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||||
|
|
||||||
|
public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissionPayoutsQuery, GetMyCommissionPayoutsResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
|
public GetMyCommissionPayoutsQueryHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
ICurrentUserService currentUser)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetMyCommissionPayoutsResponseDto> Handle(GetMyCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// دریافت UserId از JWT (فقط برای Customer API)
|
||||||
|
if (!long.TryParse(_currentUser.UserId, out var userId))
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = _context.UserCommissionPayouts
|
||||||
|
.Include(x => x.WeekDefinition)
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.AsNoTracking()
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
// فیلترها
|
||||||
|
if (request.Status.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.Status == request.Status.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.WeekDefinitionId.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// مرتبسازی: جدیدترین اول
|
||||||
|
query = query.OrderByDescending(x => x.Created);
|
||||||
|
|
||||||
|
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||||
|
|
||||||
|
var models = await query
|
||||||
|
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||||
|
.Select(x => new GetMyCommissionPayoutsResponseModel
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
WeekDefinitionId = x.WeekDefinitionId,
|
||||||
|
WeekDisplayName = x.WeekDefinition != null ? x.WeekDefinition.DisplayName : "",
|
||||||
|
BalancesEarned = x.BalancesEarned,
|
||||||
|
TotalAmount = x.TotalAmount,
|
||||||
|
AmountFormatted = x.TotalAmount.ToString("N0") + " تومان",
|
||||||
|
Status = x.Status,
|
||||||
|
CalculatedDate = x.PaidAt ?? (DateTime?)x.Created,
|
||||||
|
DatePersian = ""
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new GetMyCommissionPayoutsResponseDto
|
||||||
|
{
|
||||||
|
MetaData = meta,
|
||||||
|
Models = models
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||||
|
|
||||||
|
public class GetMyCommissionPayoutsQueryValidator : AbstractValidator<GetMyCommissionPayoutsQuery>
|
||||||
|
{
|
||||||
|
public GetMyCommissionPayoutsQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.PaginationState)
|
||||||
|
.NotNull()
|
||||||
|
.WithMessage("Pagination state is required");
|
||||||
|
|
||||||
|
When(x => x.PaginationState != null, () =>
|
||||||
|
{
|
||||||
|
RuleFor(x => x.PaginationState!.PageNumber)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("Page number must be greater than 0");
|
||||||
|
|
||||||
|
RuleFor(x => x.PaginationState!.PageSize)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.LessThanOrEqualTo(100)
|
||||||
|
.WithMessage("Page size must be between 1 and 100");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||||
|
|
||||||
|
public class GetMyCommissionPayoutsResponseDto
|
||||||
|
{
|
||||||
|
public MetaData? MetaData { get; set; }
|
||||||
|
public List<GetMyCommissionPayoutsResponseModel> Models { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetMyCommissionPayoutsResponseModel
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public long WeekDefinitionId { get; set; }
|
||||||
|
public string WeekDisplayName { get; set; } = string.Empty;
|
||||||
|
public int BalancesEarned { get; set; }
|
||||||
|
public long TotalAmount { get; set; }
|
||||||
|
public string AmountFormatted { get; set; } = string.Empty;
|
||||||
|
public CommissionPayoutStatus Status { get; set; }
|
||||||
|
public DateTime? CalculatedDate { get; set; }
|
||||||
|
public string DatePersian { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query برای دریافت تعادلهای هفتگی کاربر جاری (از JWT)
|
||||||
|
/// </summary>
|
||||||
|
public record GetMyWeeklyBalancesQuery : IRequest<GetUserWeeklyBalancesResponseDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه تعریف هفته (اختیاری)
|
||||||
|
/// </summary>
|
||||||
|
public long? WeekDefinitionId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فقط موارد Expired نشده؟
|
||||||
|
/// </summary>
|
||||||
|
public bool OnlyActive { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pagination
|
||||||
|
/// </summary>
|
||||||
|
public PaginationState? PaginationState { get; init; }
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler برای دریافت تعادلهای هفتگی کاربر جاری
|
||||||
|
/// </summary>
|
||||||
|
public class GetMyWeeklyBalancesQueryHandler : IRequestHandler<GetMyWeeklyBalancesQuery, GetUserWeeklyBalancesResponseDto>
|
||||||
|
{
|
||||||
|
private readonly ICurrentUserService _currentUserService;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
private readonly ILogger<GetMyWeeklyBalancesQueryHandler> _logger;
|
||||||
|
|
||||||
|
public GetMyWeeklyBalancesQueryHandler(
|
||||||
|
ICurrentUserService currentUserService,
|
||||||
|
IMediator mediator,
|
||||||
|
ILogger<GetMyWeeklyBalancesQueryHandler> logger)
|
||||||
|
{
|
||||||
|
_currentUserService = currentUserService;
|
||||||
|
_mediator = mediator;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetMyWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// دریافت UserId از JWT
|
||||||
|
if (!long.TryParse(_currentUserService.UserId, out var userId) || userId <= 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("GetMyWeeklyBalances called without valid user authentication");
|
||||||
|
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("GetMyWeeklyBalances for UserId: {UserId}, WeekDefinitionId: {WeekDefinitionId}",
|
||||||
|
userId, request.WeekDefinitionId);
|
||||||
|
|
||||||
|
// فراخوانی GetUserWeeklyBalancesQuery با UserId از JWT
|
||||||
|
var query = new GetUserWeeklyBalancesQuery
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
WeekDefinitionId = request.WeekDefinitionId,
|
||||||
|
OnlyActive = request.OnlyActive,
|
||||||
|
PaginationState = request.PaginationState
|
||||||
|
};
|
||||||
|
|
||||||
|
return await _mediator.Send(query, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -18,7 +18,7 @@ public record GetUserCommissionPayoutsQuery : IRequest<GetUserCommissionPayoutsR
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// شماره هفته (اختیاری)
|
/// شماره هفته (اختیاری)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? WeekNumber { get; init; }
|
public long? WeekDefinitionId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// مرتبسازی
|
/// مرتبسازی
|
||||||
|
|||||||
+36
-8
@@ -3,22 +3,41 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayo
|
|||||||
public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommissionPayoutsQuery, GetUserCommissionPayoutsResponseDto>
|
public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommissionPayoutsQuery, GetUserCommissionPayoutsResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
public GetUserCommissionPayoutsQueryHandler(IApplicationDbContext context)
|
public GetUserCommissionPayoutsQueryHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IWeekDefinitionRepository weekDefinitionRepository,
|
||||||
|
ICurrentUserService currentUser)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
|
_currentUser = currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetUserCommissionPayoutsResponseDto> Handle(GetUserCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
public async Task<GetUserCommissionPayoutsResponseDto> Handle(GetUserCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var query = _context.UserCommissionPayouts
|
var query = _context.UserCommissionPayouts
|
||||||
|
.Include(x => x.WeekDefinition)
|
||||||
|
.Include(x => x.User)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
// فیلترها
|
// اگر UserId داده نشده، از CurrentUser بگیر (برای Customer API)
|
||||||
if (request.UserId.HasValue)
|
long? userId = request.UserId;
|
||||||
|
if (!userId.HasValue || userId.Value == 0)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
if (long.TryParse(_currentUser.UserId, out var currentUserId))
|
||||||
|
{
|
||||||
|
userId = currentUserId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلترها
|
||||||
|
if (userId.HasValue && userId.Value > 0)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.UserId == userId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Status.HasValue)
|
if (request.Status.HasValue)
|
||||||
@@ -26,12 +45,12 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
|||||||
query = query.Where(x => x.Status == request.Status.Value);
|
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);
|
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||||
|
|
||||||
@@ -41,7 +60,10 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
|||||||
{
|
{
|
||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
UserId = x.UserId,
|
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,
|
WeeklyPoolId = x.WeeklyPoolId,
|
||||||
BalancesEarned = x.BalancesEarned,
|
BalancesEarned = x.BalancesEarned,
|
||||||
ValuePerBalance = x.ValuePerBalance,
|
ValuePerBalance = x.ValuePerBalance,
|
||||||
@@ -55,6 +77,12 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
|||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
// // Populate WeekDisplayName from cache
|
||||||
|
// foreach (var model in models)
|
||||||
|
// {
|
||||||
|
// model.WeekDisplayName = _weekDefinitionRepository.GetDisplayNameByGregorianWeekNumber(model.WeekNumber);
|
||||||
|
// }
|
||||||
|
|
||||||
return new GetUserCommissionPayoutsResponseDto
|
return new GetUserCommissionPayoutsResponseDto
|
||||||
{
|
{
|
||||||
MetaData = meta,
|
MetaData = meta,
|
||||||
|
|||||||
+4
-4
@@ -14,10 +14,10 @@ public class GetUserCommissionPayoutsQueryValidator : AbstractValidator<GetUserC
|
|||||||
.WithMessage("وضعیت معتبر نیست")
|
.WithMessage("وضعیت معتبر نیست")
|
||||||
.When(x => x.Status.HasValue);
|
.When(x => x.Status.HasValue);
|
||||||
|
|
||||||
RuleFor(x => x.WeekNumber)
|
RuleFor(x => x.WeekDefinitionId)
|
||||||
.Matches(@"^\d{4}-W\d{2}$")
|
.GreaterThan(0)
|
||||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد")
|
.WithMessage("شماره هفته نمیتواند صفر باشد")
|
||||||
.When(x => !string.IsNullOrEmpty(x.WeekNumber));
|
.When(x => x.WeekDefinitionId.HasValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
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 Id { get; set; }
|
||||||
public long UserId { 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 WeeklyPoolId { get; set; }
|
||||||
public long BalancesEarned { get; set; }
|
public long BalancesEarned { get; set; }
|
||||||
public decimal ValuePerBalance { get; set; }
|
public decimal ValuePerBalance { get; set; }
|
||||||
|
|||||||
+3
-2
@@ -11,9 +11,10 @@ public record GetUserWeeklyBalancesQuery : IRequest<GetUserWeeklyBalancesRespons
|
|||||||
public long? UserId { get; init; }
|
public long? UserId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// شماره هفته (اختیاری)
|
/// شناسه تعریف هفته (اختیاری) - روش ترجیحی
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? WeekNumber { get; init; }
|
public long? WeekDefinitionId { get; init; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// فقط موارد Expired نشده؟
|
/// فقط موارد Expired نشده؟
|
||||||
|
|||||||
+38
-10
@@ -3,35 +3,57 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances
|
|||||||
public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBalancesQuery, GetUserWeeklyBalancesResponseDto>
|
public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBalancesQuery, GetUserWeeklyBalancesResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
public GetUserWeeklyBalancesQueryHandler(IApplicationDbContext context)
|
public GetUserWeeklyBalancesQueryHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IWeekDefinitionRepository weekDefinitionRepository,
|
||||||
|
ICurrentUserService currentUser)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
|
_currentUser = currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetUserWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetUserWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var query = _context.NetworkWeeklyBalances
|
var query = _context.NetworkWeeklyBalances
|
||||||
|
.Include(x => x.WeekDefinition)
|
||||||
|
.Include(x => x.User)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
// فیلترها
|
// اگر UserId داده نشده، از CurrentUser بگیر (برای Customer API)
|
||||||
if (request.UserId.HasValue)
|
long? userId = request.UserId;
|
||||||
|
if (!userId.HasValue || userId.Value == 0)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
if (long.TryParse(_currentUser.UserId, out var currentUserId))
|
||||||
|
{
|
||||||
|
userId = currentUserId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(request.WeekNumber))
|
// فیلترها
|
||||||
|
if (userId.HasValue && userId.Value > 0)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.WeekNumber == request.WeekNumber);
|
query = query.Where(x => x.UserId == userId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// فیلتر بر اساس WeekDefinitionId (روش ترجیحی)
|
||||||
|
if (request.WeekDefinitionId.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (request.OnlyActive.HasValue && request.OnlyActive.Value)
|
if (request.OnlyActive.HasValue && request.OnlyActive.Value)
|
||||||
{
|
{
|
||||||
query = query.Where(x => !x.IsExpired);
|
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);
|
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||||
|
|
||||||
@@ -41,9 +63,15 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
|||||||
{
|
{
|
||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
UserId = x.UserId,
|
UserId = x.UserId,
|
||||||
WeekNumber = x.WeekNumber,
|
UserFullName = x.User != null ? $"{x.User.FirstName} {x.User.LastName}".Trim() : "",
|
||||||
LeftLegBalances = x.LeftLegBalances,
|
WeekDefinitionId = x.WeekDefinitionId,
|
||||||
RightLegBalances = x.RightLegBalances,
|
WeekDisplayName = x.WeekDefinition != null ? x.WeekDefinition.DisplayName : "",
|
||||||
|
LeftLegNewMembers = x.LeftLegNewMembers,
|
||||||
|
LeftLegCarryover = x.LeftLegCarryover,
|
||||||
|
LeftLegTotal = x.LeftLegTotal,
|
||||||
|
RightLegNewMembers = x.RightLegNewMembers,
|
||||||
|
RightLegCarryover = x.RightLegCarryover,
|
||||||
|
RightLegTotal = x.RightLegTotal,
|
||||||
TotalBalances = x.TotalBalances,
|
TotalBalances = x.TotalBalances,
|
||||||
WeeklyPoolContribution = x.WeeklyPoolContribution,
|
WeeklyPoolContribution = x.WeeklyPoolContribution,
|
||||||
CalculatedAt = x.CalculatedAt,
|
CalculatedAt = x.CalculatedAt,
|
||||||
|
|||||||
+4
-4
@@ -9,10 +9,10 @@ public class GetUserWeeklyBalancesQueryValidator : AbstractValidator<GetUserWeek
|
|||||||
.WithMessage("شناسه کاربر معتبر نیست")
|
.WithMessage("شناسه کاربر معتبر نیست")
|
||||||
.When(x => x.UserId.HasValue);
|
.When(x => x.UserId.HasValue);
|
||||||
|
|
||||||
RuleFor(x => x.WeekNumber)
|
RuleFor(x => x.WeekDefinitionId)
|
||||||
.Matches(@"^\d{4}-W\d{2}$")
|
.GreaterThan(0)
|
||||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد")
|
.WithMessage("شماره هفته نمیتواند صفر باشد")
|
||||||
.When(x => !string.IsNullOrEmpty(x.WeekNumber));
|
.When(x => x.WeekDefinitionId.HasValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
|||||||
+14
-3
@@ -10,9 +10,20 @@ public class GetUserWeeklyBalancesResponseModel
|
|||||||
{
|
{
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
public long UserId { get; set; }
|
public long UserId { get; set; }
|
||||||
public string WeekNumber { get; set; } = string.Empty;
|
public string UserFullName { get; set; } = string.Empty;
|
||||||
public int LeftLegBalances { get; set; }
|
public long WeekDefinitionId { get; set; }
|
||||||
public int RightLegBalances { get; set; }
|
public string WeekDisplayName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// چپ
|
||||||
|
public int LeftLegNewMembers { get; set; }
|
||||||
|
public int LeftLegCarryover { get; set; }
|
||||||
|
public int LeftLegTotal { get; set; }
|
||||||
|
|
||||||
|
// راست
|
||||||
|
public int RightLegNewMembers { get; set; }
|
||||||
|
public int RightLegCarryover { get; set; }
|
||||||
|
public int RightLegTotal { get; set; }
|
||||||
|
|
||||||
public int TotalBalances { get; set; }
|
public int TotalBalances { get; set; }
|
||||||
public long WeeklyPoolContribution { get; set; }
|
public long WeeklyPoolContribution { get; set; }
|
||||||
public DateTime? CalculatedAt { get; set; }
|
public DateTime? CalculatedAt { 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; }
|
||||||
|
}
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
Id = w.Id,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
//شناسه هفته
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
//شماره ترتیب هفته (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>
|
||||||
/// شماره هفته
|
/// شماره هفته
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string WeekNumber { get; init; } = string.Empty;
|
public long WeekDefinitionId { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-4
@@ -3,21 +3,27 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWeeklyCommissionPo
|
|||||||
public class GetWeeklyCommissionPoolQueryHandler : IRequestHandler<GetWeeklyCommissionPoolQuery, WeeklyCommissionPoolDto?>
|
public class GetWeeklyCommissionPoolQueryHandler : IRequestHandler<GetWeeklyCommissionPoolQuery, WeeklyCommissionPoolDto?>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
|
|
||||||
public GetWeeklyCommissionPoolQueryHandler(IApplicationDbContext context)
|
public GetWeeklyCommissionPoolQueryHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IWeekDefinitionRepository weekDefinitionRepository)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WeeklyCommissionPoolDto?> Handle(GetWeeklyCommissionPoolQuery request, CancellationToken cancellationToken)
|
public async Task<WeeklyCommissionPoolDto?> Handle(GetWeeklyCommissionPoolQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var pool = await _context.WeeklyCommissionPools
|
var pool = await _context.WeeklyCommissionPools
|
||||||
|
.Include(x => x.WeekDefinition)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(x => x.WeekNumber == request.WeekNumber)
|
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId)
|
||||||
.Select(x => new WeeklyCommissionPoolDto
|
.Select(x => new WeeklyCommissionPoolDto
|
||||||
{
|
{
|
||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
WeekNumber = x.WeekNumber,
|
WeekDefinitionId = x.WeekDefinitionId,
|
||||||
|
WeekDisplayName=x.WeekDefinition.DisplayName,
|
||||||
TotalPoolAmount = x.TotalPoolAmount,
|
TotalPoolAmount = x.TotalPoolAmount,
|
||||||
TotalBalances = x.TotalBalances,
|
TotalBalances = x.TotalBalances,
|
||||||
ValuePerBalance = x.ValuePerBalance,
|
ValuePerBalance = x.ValuePerBalance,
|
||||||
@@ -27,6 +33,11 @@ public class GetWeeklyCommissionPoolQueryHandler : IRequestHandler<GetWeeklyComm
|
|||||||
})
|
})
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.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()
|
public GetWeeklyCommissionPoolQueryValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.WeekNumber)
|
RuleFor(x => x.WeekDefinitionId)
|
||||||
.NotEmpty()
|
.GreaterThan(0)
|
||||||
.WithMessage("شماره هفته نمیتواند خالی باشد")
|
.WithMessage("شماره هفته نمیتواند صفر باشد");
|
||||||
.Matches(@"^\d{4}-W\d{2}$")
|
|
||||||
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
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 class WeeklyCommissionPoolDto
|
||||||
{
|
{
|
||||||
public long Id { get; set; }
|
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 TotalPoolAmount { get; set; }
|
||||||
public long TotalBalances { get; set; }
|
public long TotalBalances { get; set; }
|
||||||
public decimal ValuePerBalance { 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)
|
public async Task<WithdrawalReportsDto> Handle(GetWithdrawalReportsQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// تعیین بازه زمانی پیشفرض (30 روز گذشته)
|
// تعیین بازه زمانی پیشفرض (30 روز گذشته)
|
||||||
var endDate = request.EndDate ?? DateTime.UtcNow;
|
var endDate = request.EndDate ?? DateTime.Now;
|
||||||
var startDate = request.StartDate ?? endDate.AddDays(-30);
|
var startDate = request.StartDate ?? endDate.AddDays(-30);
|
||||||
|
|
||||||
// Query پایه
|
// Query پایه
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ public class GetWithdrawalRequestsQuery : IRequest<GetWithdrawalRequestsResponse
|
|||||||
{
|
{
|
||||||
public int? Status { get; set; } // CommissionPayoutStatus enum
|
public int? Status { get; set; } // CommissionPayoutStatus enum
|
||||||
public long? UserId { get; set; }
|
public long? UserId { get; set; }
|
||||||
public string? WeekNumber { get; set; }
|
public long? WeekDefinitionId { get; init; }
|
||||||
public string? IbanNumber { get; set; }
|
public string? IbanNumber { get; set; }
|
||||||
public PaginationState? PaginationState { get; set; }
|
public PaginationState? PaginationState { get; set; }
|
||||||
public string? SortBy { 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>
|
public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRequestsQuery, GetWithdrawalRequestsResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
|
|
||||||
public GetWithdrawalRequestsQueryHandler(IApplicationDbContext context)
|
public GetWithdrawalRequestsQueryHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IWeekDefinitionRepository weekDefinitionRepository)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetWithdrawalRequestsResponseDto> Handle(GetWithdrawalRequestsQuery request, CancellationToken cancellationToken)
|
public async Task<GetWithdrawalRequestsResponseDto> Handle(GetWithdrawalRequestsQuery request, CancellationToken cancellationToken)
|
||||||
@@ -14,6 +18,7 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
|
|||||||
var query = _context.UserCommissionPayouts
|
var query = _context.UserCommissionPayouts
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(x => x.User)
|
.Include(x => x.User)
|
||||||
|
.Include(x => x.WeekDefinition)
|
||||||
.Where(x => x.WithdrawalMethod != null) // Only requests with withdrawal method
|
.Where(x => x.WithdrawalMethod != null) // Only requests with withdrawal method
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
@@ -28,9 +33,9 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
|
|||||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
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))
|
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.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);
|
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||||
|
|
||||||
@@ -51,7 +56,8 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
|
|||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
UserId = x.UserId,
|
UserId = x.UserId,
|
||||||
UserName = x.User != null ? (x.User.FirstName + " " + x.User.LastName).Trim() : x.User?.Mobile ?? "N/A",
|
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,
|
Amount = x.TotalAmount,
|
||||||
Status = (int)x.Status,
|
Status = (int)x.Status,
|
||||||
WithdrawalMethod = x.WithdrawalMethod.HasValue ? (int)x.WithdrawalMethod.Value : 0,
|
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 Id { get; set; }
|
||||||
public long UserId { get; set; }
|
public long UserId { get; set; }
|
||||||
public string UserName { get; set; } = string.Empty;
|
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 long Amount { get; set; }
|
||||||
public int Status { get; set; } // CommissionPayoutStatus enum
|
public int Status { get; set; } // CommissionPayoutStatus enum
|
||||||
public int? WithdrawalMethod { get; set; }
|
public int? WithdrawalMethod { get; set; }
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerExecutionLog
|
|||||||
|
|
||||||
public record GetWorkerExecutionLogsQuery : IRequest<GetWorkerExecutionLogsResponseDto>
|
public record GetWorkerExecutionLogsQuery : IRequest<GetWorkerExecutionLogsResponseDto>
|
||||||
{
|
{
|
||||||
public string? WeekNumber { get; init; }
|
public long? WeekDefinitionId { get; init; }
|
||||||
public string? ExecutionId { get; init; }
|
public string? ExecutionId { get; init; }
|
||||||
public bool? SuccessOnly { get; init; }
|
public bool? SuccessOnly { get; init; }
|
||||||
public bool? FailedOnly { 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>
|
public class GetWorkerExecutionLogsQueryHandler : IRequestHandler<GetWorkerExecutionLogsQuery, GetWorkerExecutionLogsResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
|
|
||||||
public GetWorkerExecutionLogsQueryHandler(IApplicationDbContext context)
|
public GetWorkerExecutionLogsQueryHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IWeekDefinitionRepository weekDefinitionRepository)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetWorkerExecutionLogsResponseDto> Handle(
|
public async Task<GetWorkerExecutionLogsResponseDto> Handle(
|
||||||
@@ -17,12 +21,14 @@ public class GetWorkerExecutionLogsQueryHandler : IRequestHandler<GetWorkerExecu
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Query from database
|
// Query from database
|
||||||
var query = _context.WorkerExecutionLogs.AsQueryable();
|
var query = _context.WorkerExecutionLogs
|
||||||
|
.Include(i=> i.WeekDefinition)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
// Apply filters
|
// 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)
|
if (request.SuccessOnly == true)
|
||||||
@@ -49,7 +55,8 @@ public class GetWorkerExecutionLogsQueryHandler : IRequestHandler<GetWorkerExecu
|
|||||||
.Select(x => new WorkerExecutionLogModel
|
.Select(x => new WorkerExecutionLogModel
|
||||||
{
|
{
|
||||||
ExecutionId = x.ExecutionId.ToString(),
|
ExecutionId = x.ExecutionId.ToString(),
|
||||||
WeekNumber = x.WeekNumber,
|
WeekDefinitionId = x.WeekDefinitionId,
|
||||||
|
WeekDisplayName = x.WeekDefinition.DisplayName,
|
||||||
Step = "Full", // We only have full execution now
|
Step = "Full", // We only have full execution now
|
||||||
Success = x.Status == Domain.Entities.Commission.WorkerExecutionStatus.Success ||
|
Success = x.Status == Domain.Entities.Commission.WorkerExecutionStatus.Success ||
|
||||||
x.Status == Domain.Entities.Commission.WorkerExecutionStatus.SuccessWithWarnings,
|
x.Status == Domain.Entities.Commission.WorkerExecutionStatus.SuccessWithWarnings,
|
||||||
@@ -62,6 +69,12 @@ public class GetWorkerExecutionLogsQueryHandler : IRequestHandler<GetWorkerExecu
|
|||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
// // Populate WeekDisplayName from cache
|
||||||
|
// foreach (var log in logs)
|
||||||
|
// {
|
||||||
|
// log.WeekDisplayName = _weekDefinitionRepository.GetDisplayNameByGregorianWeekNumber(log.WeekNumber);
|
||||||
|
// }
|
||||||
|
|
||||||
return new GetWorkerExecutionLogsResponseDto
|
return new GetWorkerExecutionLogsResponseDto
|
||||||
{
|
{
|
||||||
MetaData = new MetaData
|
MetaData = new MetaData
|
||||||
|
|||||||
+2
-1
@@ -11,7 +11,8 @@ public class GetWorkerExecutionLogsResponseDto
|
|||||||
public class WorkerExecutionLogModel
|
public class WorkerExecutionLogModel
|
||||||
{
|
{
|
||||||
public string ExecutionId { get; set; } = string.Empty;
|
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 string Step { get; set; } = string.Empty; // "Balances" | "Pool" | "Payouts" | "Full"
|
||||||
public bool Success { get; set; }
|
public bool Success { get; set; }
|
||||||
public string? ErrorMessage { 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>
|
public class GetWorkerStatusQueryHandler : IRequestHandler<GetWorkerStatusQuery, GetWorkerStatusResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
|
|
||||||
public GetWorkerStatusQueryHandler(IApplicationDbContext context)
|
public GetWorkerStatusQueryHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IWeekDefinitionRepository weekDefinitionRepository)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetWorkerStatusResponseDto> Handle(
|
public async Task<GetWorkerStatusResponseDto> Handle(
|
||||||
@@ -20,15 +24,21 @@ public class GetWorkerStatusQueryHandler : IRequestHandler<GetWorkerStatusQuery,
|
|||||||
|
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
|
|
||||||
|
var currentWeek = _weekDefinitionRepository.GetCurrentWeek();
|
||||||
|
var currentWeekNumber = currentWeek != null
|
||||||
|
? $"{currentWeek.GregorianYear}-W{currentWeek.GregorianWeekNumber:D2}"
|
||||||
|
: null;
|
||||||
|
|
||||||
return new GetWorkerStatusResponseDto
|
return new GetWorkerStatusResponseDto
|
||||||
{
|
{
|
||||||
IsRunning = false,
|
IsRunning = false,
|
||||||
IsEnabled = true,
|
IsEnabled = true,
|
||||||
CurrentExecutionId = null,
|
CurrentExecutionId = null,
|
||||||
CurrentWeekNumber = null,
|
CurrentWeekNumber = currentWeekNumber,
|
||||||
|
CurrentWeekDisplayName = currentWeek?.DisplayName,
|
||||||
CurrentStep = "Idle",
|
CurrentStep = "Idle",
|
||||||
LastRunAt = DateTime.UtcNow.AddHours(-24),
|
LastRunAt = DateTime.Now.AddHours(-24),
|
||||||
NextScheduledRun = DateTime.UtcNow.AddDays(7),
|
NextScheduledRun = DateTime.Now.AddDays(7),
|
||||||
TotalExecutions = 48,
|
TotalExecutions = 48,
|
||||||
SuccessfulExecutions = 47,
|
SuccessfulExecutions = 47,
|
||||||
FailedExecutions = 1
|
FailedExecutions = 1
|
||||||
|
|||||||
+1
@@ -6,6 +6,7 @@ public class GetWorkerStatusResponseDto
|
|||||||
public bool IsEnabled { get; set; }
|
public bool IsEnabled { get; set; }
|
||||||
public string? CurrentExecutionId { get; set; }
|
public string? CurrentExecutionId { get; set; }
|
||||||
public string? CurrentWeekNumber { get; set; }
|
public string? CurrentWeekNumber { get; set; }
|
||||||
|
public string? CurrentWeekDisplayName { get; set; }
|
||||||
public string? CurrentStep { get; set; } // "Balances" | "Pool" | "Payouts" | "Idle"
|
public string? CurrentStep { get; set; } // "Balances" | "Pool" | "Payouts" | "Idle"
|
||||||
public DateTime? LastRunAt { get; set; }
|
public DateTime? LastRunAt { get; set; }
|
||||||
public DateTime? NextScheduledRun { get; set; }
|
public DateTime? NextScheduledRun { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Authorization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس بررسی مجوز کاربر بر اساس نقشهای JWT
|
||||||
|
/// </summary>
|
||||||
|
public interface IPermissionService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت نقشهای کاربر فعلی از JWT Claims
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<string>> GetUserRolesAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// بررسی اینکه آیا کاربر فعلی مجوز مشخصی دارد
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> HasPermissionAsync(string permission, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Authorization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ثوابت نام مجوزها — دستهبندی شده بر اساس حوزه
|
||||||
|
/// </summary>
|
||||||
|
public static class PermissionNames
|
||||||
|
{
|
||||||
|
// Dashboard
|
||||||
|
public const string DashboardView = "dashboard.view";
|
||||||
|
|
||||||
|
// Orders
|
||||||
|
public const string OrdersView = "orders.view";
|
||||||
|
public const string OrdersCreate = "orders.create";
|
||||||
|
public const string OrdersUpdate = "orders.update";
|
||||||
|
public const string OrdersDelete = "orders.delete";
|
||||||
|
public const string OrdersCancel = "orders.cancel";
|
||||||
|
public const string OrdersApprove = "orders.approve";
|
||||||
|
|
||||||
|
// Products
|
||||||
|
public const string ProductsView = "products.view";
|
||||||
|
public const string ProductsCreate = "products.create";
|
||||||
|
public const string ProductsUpdate = "products.update";
|
||||||
|
public const string ProductsDelete = "products.delete";
|
||||||
|
|
||||||
|
// Users
|
||||||
|
public const string UsersView = "users.view";
|
||||||
|
public const string UsersUpdate = "users.update";
|
||||||
|
public const string UsersDelete = "users.delete";
|
||||||
|
|
||||||
|
// Commission
|
||||||
|
public const string CommissionView = "commission.view";
|
||||||
|
public const string CommissionApproveWithdrawal = "commission.approve_withdrawal";
|
||||||
|
|
||||||
|
// Public Messages
|
||||||
|
public const string PublicMessagesView = "publicmessages.view";
|
||||||
|
public const string PublicMessagesCreate = "publicmessages.create";
|
||||||
|
public const string PublicMessagesUpdate = "publicmessages.update";
|
||||||
|
public const string PublicMessagesPublish = "publicmessages.publish";
|
||||||
|
|
||||||
|
// Manual Payments
|
||||||
|
public const string ManualPaymentsView = "manualpayments.view";
|
||||||
|
public const string ManualPaymentsCreate = "manualpayments.create";
|
||||||
|
public const string ManualPaymentsApprove = "manualpayments.approve";
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
public const string SettingsView = "settings.view";
|
||||||
|
public const string SettingsUpdate = "settings.update";
|
||||||
|
public const string SettingsDelete = "settings.delete";
|
||||||
|
public const string SettingsManageConfiguration = "settings.manage_configuration";
|
||||||
|
public const string SettingsManageVat = "settings.manage_vat";
|
||||||
|
|
||||||
|
// Reports
|
||||||
|
public const string ReportsView = "reports.view";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نام نقشها
|
||||||
|
/// </summary>
|
||||||
|
public static class RoleNames
|
||||||
|
{
|
||||||
|
public const string SuperAdmin = "Administrator";
|
||||||
|
public const string Admin = "Admin";
|
||||||
|
public const string Inspector = "Inspector";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تنظیمات نقش→مجوز — ماتریس دسترسی
|
||||||
|
/// </summary>
|
||||||
|
public static class RolePermissionConfig
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, HashSet<string>> RolePermissions = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
[RoleNames.SuperAdmin] = new(StringComparer.OrdinalIgnoreCase) { "*" }, // Full access
|
||||||
|
|
||||||
|
[RoleNames.Admin] = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
PermissionNames.DashboardView,
|
||||||
|
PermissionNames.OrdersView,
|
||||||
|
PermissionNames.OrdersCreate,
|
||||||
|
PermissionNames.OrdersUpdate,
|
||||||
|
PermissionNames.OrdersCancel,
|
||||||
|
PermissionNames.ProductsView,
|
||||||
|
PermissionNames.ProductsCreate,
|
||||||
|
PermissionNames.ProductsUpdate,
|
||||||
|
PermissionNames.ProductsDelete,
|
||||||
|
PermissionNames.UsersView,
|
||||||
|
PermissionNames.UsersUpdate,
|
||||||
|
PermissionNames.CommissionView,
|
||||||
|
PermissionNames.CommissionApproveWithdrawal,
|
||||||
|
PermissionNames.PublicMessagesView,
|
||||||
|
PermissionNames.PublicMessagesCreate,
|
||||||
|
PermissionNames.PublicMessagesUpdate,
|
||||||
|
PermissionNames.PublicMessagesPublish,
|
||||||
|
PermissionNames.ManualPaymentsView,
|
||||||
|
PermissionNames.ManualPaymentsCreate,
|
||||||
|
PermissionNames.ReportsView
|
||||||
|
},
|
||||||
|
|
||||||
|
[RoleNames.Inspector] = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
PermissionNames.DashboardView,
|
||||||
|
PermissionNames.OrdersView,
|
||||||
|
PermissionNames.UsersView,
|
||||||
|
PermissionNames.CommissionView,
|
||||||
|
PermissionNames.PublicMessagesView,
|
||||||
|
PermissionNames.ReportsView
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// بررسی اینکه آیا نقش مشخصی مجوز خاصی دارد
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasPermission(string role, string permission)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(role) || string.IsNullOrWhiteSpace(permission))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!RolePermissions.TryGetValue(role, out var permissions))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Wildcard: SuperAdmin has full access
|
||||||
|
if (permissions.Contains("*"))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return permissions.Contains(permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Authorization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute برای مشخص کردن مجوز لازم برای دسترسی به یک متد gRPC
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||||
|
public sealed class RequiresPermissionAttribute : Attribute
|
||||||
|
{
|
||||||
|
public RequiresPermissionAttribute(string permission)
|
||||||
|
{
|
||||||
|
Permission = permission ?? throw new ArgumentNullException(nameof(permission));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Permission { get; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using CMSMicroservice.Domain.Entities.Payment;
|
using CMSMicroservice.Domain.Entities.Payment;
|
||||||
using CMSMicroservice.Domain.Entities.Order;
|
using CMSMicroservice.Domain.Entities.Order;
|
||||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||||
|
using CMSMicroservice.Domain.Entities.Geography;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
@@ -29,8 +31,6 @@ public interface IApplicationDbContext
|
|||||||
DbSet<UserPackagePurchase> UserPackagePurchases { get; }
|
DbSet<UserPackagePurchase> UserPackagePurchases { get; }
|
||||||
DbSet<UserWallet> UserWallets { get; }
|
DbSet<UserWallet> UserWallets { get; }
|
||||||
DbSet<UserWalletChangeLog> UserWalletChangeLogs { get; }
|
DbSet<UserWalletChangeLog> UserWalletChangeLogs { get; }
|
||||||
DbSet<SystemConfiguration> SystemConfigurations { get; }
|
|
||||||
DbSet<SystemConfigurationHistory> SystemConfigurationHistories { get; }
|
|
||||||
DbSet<ManualPayment> ManualPayments { get; }
|
DbSet<ManualPayment> ManualPayments { get; }
|
||||||
DbSet<PublicMessage> PublicMessages { get; }
|
DbSet<PublicMessage> PublicMessages { get; }
|
||||||
DbSet<ClubMembership> ClubMemberships { get; }
|
DbSet<ClubMembership> ClubMemberships { get; }
|
||||||
@@ -39,19 +39,37 @@ public interface IApplicationDbContext
|
|||||||
DbSet<UserClubFeature> UserClubFeatures { get; }
|
DbSet<UserClubFeature> UserClubFeatures { get; }
|
||||||
DbSet<NetworkWeeklyBalance> NetworkWeeklyBalances { get; }
|
DbSet<NetworkWeeklyBalance> NetworkWeeklyBalances { get; }
|
||||||
DbSet<NetworkMembershipHistory> NetworkMembershipHistories { get; }
|
DbSet<NetworkMembershipHistory> NetworkMembershipHistories { get; }
|
||||||
|
DbSet<WeekDefinition> WeekDefinitions { get; }
|
||||||
DbSet<WeeklyCommissionPool> WeeklyCommissionPools { get; }
|
DbSet<WeeklyCommissionPool> WeeklyCommissionPools { get; }
|
||||||
DbSet<UserCommissionPayout> UserCommissionPayouts { get; }
|
DbSet<UserCommissionPayout> UserCommissionPayouts { get; }
|
||||||
DbSet<CommissionPayoutHistory> CommissionPayoutHistories { get; }
|
DbSet<CommissionPayoutHistory> CommissionPayoutHistories { get; }
|
||||||
DbSet<WorkerExecutionLog> WorkerExecutionLogs { get; }
|
DbSet<WorkerExecutionLog> WorkerExecutionLogs { get; }
|
||||||
DbSet<DayaLoanContract> DayaLoanContracts { get; }
|
DbSet<DayaLoanContract> DayaLoanContracts { get; }
|
||||||
|
DbSet<AppVersion> AppVersions { get; }
|
||||||
|
|
||||||
// ============= Discount Shop =============
|
// ============= Discount Shop =============
|
||||||
DbSet<DiscountProduct> DiscountProducts { get; }
|
DbSet<DiscountProduct> DiscountProducts { get; }
|
||||||
DbSet<DiscountCategory> DiscountCategories { get; }
|
DbSet<DiscountCategory> DiscountCategories { get; }
|
||||||
DbSet<DiscountProductCategory> DiscountProductCategories { get; }
|
DbSet<DiscountProductCategory> DiscountProductCategories { get; }
|
||||||
|
DbSet<DiscountProductImage> DiscountProductImages { get; }
|
||||||
DbSet<DiscountShoppingCart> DiscountShoppingCarts { get; }
|
DbSet<DiscountShoppingCart> DiscountShoppingCarts { get; }
|
||||||
DbSet<DiscountOrder> DiscountOrders { get; }
|
DbSet<DiscountOrder> DiscountOrders { get; }
|
||||||
DbSet<DiscountOrderDetail> DiscountOrderDetails { get; }
|
DbSet<DiscountOrderDetail> DiscountOrderDetails { get; }
|
||||||
|
|
||||||
|
// ============= Geography =============
|
||||||
|
DbSet<Country> Countries { get; }
|
||||||
|
DbSet<State> States { get; }
|
||||||
|
DbSet<City> Cities { get; }
|
||||||
|
|
||||||
|
// ============= Inventory Management =============
|
||||||
|
DbSet<Warehouse> Warehouses { get; }
|
||||||
|
DbSet<InventoryItem> InventoryItems { get; }
|
||||||
|
DbSet<StockMovement> StockMovements { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures
|
||||||
|
/// </summary>
|
||||||
|
DatabaseFacade Database { get; }
|
||||||
|
|
||||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس ارتباط با API چتیکا برای ساخت حساب کاربری
|
||||||
|
/// </summary>
|
||||||
|
public interface IChatikaApiService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ایجاد حساب کاربری در چتیکا
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mobileNumber">شماره موبایل کاربر</param>
|
||||||
|
/// <param name="fullName">نام کامل کاربر</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>نتیجه ایجاد حساب</returns>
|
||||||
|
Task<ChatikaAccountResult> CreateAccountAsync(
|
||||||
|
string mobileNumber,
|
||||||
|
string fullName,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نتیجه ایجاد حساب در چتیکا
|
||||||
|
/// </summary>
|
||||||
|
public class ChatikaAccountResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// موفقیت عملیات
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSuccess { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیام خطا در صورت عدم موفقیت
|
||||||
|
/// </summary>
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر در چتیکا
|
||||||
|
/// </summary>
|
||||||
|
public string? ChatikaUserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL دسترسی به چتیکا
|
||||||
|
/// </summary>
|
||||||
|
public string? AccessUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ایجاد نتیجه موفق
|
||||||
|
/// </summary>
|
||||||
|
public static ChatikaAccountResult Success(string? chatikaUserId = null, string? accessUrl = null) => new()
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
ChatikaUserId = chatikaUserId,
|
||||||
|
AccessUrl = accessUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ایجاد نتیجه ناموفق
|
||||||
|
/// </summary>
|
||||||
|
public static ChatikaAccountResult Failure(string errorMessage) => new()
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
ErrorMessage = errorMessage
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,37 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for uploading files to FMS (File Management Service)
|
||||||
|
/// </summary>
|
||||||
|
public interface IFileManagementService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Uploads a file to FMS and returns the stored file path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="directory">Target directory path (e.g. "Images/Products")</param>
|
||||||
|
/// <param name="fileBytes">Raw file bytes</param>
|
||||||
|
/// <param name="mime">MIME type (e.g. "image/jpeg")</param>
|
||||||
|
/// <param name="fileName">Original file name</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>The stored file path returned by FMS, or null if upload failed</returns>
|
||||||
|
Task<string?> UploadFileAsync(string directory, byte[] fileBytes, string mime, string? fileName, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uploads an image to FMS with optimization (resize + compress)
|
||||||
|
/// Returns both main image path and thumbnail path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="directory">Target directory path (e.g. "Images/Products")</param>
|
||||||
|
/// <param name="fileBytes">Raw image bytes</param>
|
||||||
|
/// <param name="mime">MIME type (e.g. "image/jpeg")</param>
|
||||||
|
/// <param name="fileName">Original file name</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Tuple of (mainImagePath, thumbnailPath), either can be null if upload failed</returns>
|
||||||
|
Task<(string? MainImagePath, string? ThumbnailPath)> UploadImageWithThumbnailAsync(
|
||||||
|
string directory, byte[] fileBytes, string mime, string? fileName,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a file from FMS by its ID
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> DeleteFileAsync(long fileId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس مدیریت موجودی - لایه بالاتر برای عملیات business
|
||||||
|
/// این سرویس مسئول همگامسازی موجودی بین InventoryItem و Product.RemainingCount است
|
||||||
|
/// </summary>
|
||||||
|
public interface IInventoryService
|
||||||
|
{
|
||||||
|
#region Initialization
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ایجاد رکورد موجودی برای محصول جدید
|
||||||
|
/// این متد باید در CreateProductCommandHandler و CreateDiscountProductCommandHandler فراخوانی شود
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="productId">شناسه محصول (Product.Id یا DiscountProduct.Id)</param>
|
||||||
|
/// <param name="productType">نوع محصول (RegularProduct یا DiscountProduct)</param>
|
||||||
|
/// <param name="initialQuantity">موجودی اولیه</param>
|
||||||
|
/// <param name="warehouseId">شناسه انبار (پیشفرض: انبار اصلی)</param>
|
||||||
|
/// <param name="lowStockThreshold">آستانه هشدار کمموجودی</param>
|
||||||
|
/// <param name="ct">CancellationToken</param>
|
||||||
|
/// <returns>شناسه InventoryItem ایجاد شده</returns>
|
||||||
|
Task<long> InitializeInventoryAsync(
|
||||||
|
long productId,
|
||||||
|
ProductType productType,
|
||||||
|
int initialQuantity,
|
||||||
|
long? warehouseId = null,
|
||||||
|
int lowStockThreshold = 10,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Query Operations
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت موجودی یک محصول
|
||||||
|
/// </summary>
|
||||||
|
Task<InventoryItem?> GetInventoryAsync(
|
||||||
|
long productId,
|
||||||
|
ProductType productType,
|
||||||
|
long? warehouseId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت موجودی قابل فروش (Quantity - ReservedQuantity)
|
||||||
|
/// </summary>
|
||||||
|
Task<int> GetAvailableQuantityAsync(
|
||||||
|
long productId,
|
||||||
|
ProductType productType,
|
||||||
|
long? warehouseId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// بررسی اینکه آیا موجودی کافی برای فروش وجود دارد
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> CheckAvailabilityAsync(
|
||||||
|
long productId,
|
||||||
|
ProductType productType,
|
||||||
|
int requiredQuantity,
|
||||||
|
long? warehouseId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت لیست محصولات کمموجود
|
||||||
|
/// </summary>
|
||||||
|
Task<List<InventoryItem>> GetLowStockItemsAsync(
|
||||||
|
ProductType? productType = null,
|
||||||
|
long? warehouseId = null,
|
||||||
|
int count = 50,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت تاریخچه حرکات موجودی
|
||||||
|
/// </summary>
|
||||||
|
Task<List<StockMovement>> GetStockMovementsAsync(
|
||||||
|
long productId,
|
||||||
|
ProductType productType,
|
||||||
|
DateTime? fromDate = null,
|
||||||
|
DateTime? toDate = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Order Flow Operations
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// رزرو موجودی برای سفارش pending
|
||||||
|
/// این متد در PlaceOrderCommandHandler فراخوانی میشود
|
||||||
|
/// فقط ReservedQuantity را افزایش میدهد، Quantity تغییر نمیکند
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="productId">شناسه محصول</param>
|
||||||
|
/// <param name="productType">نوع محصول</param>
|
||||||
|
/// <param name="quantity">تعداد رزرو</param>
|
||||||
|
/// <param name="orderId">شناسه سفارش (Order.Id یا DiscountOrder.Id)</param>
|
||||||
|
/// <param name="ct">CancellationToken</param>
|
||||||
|
/// <returns>true اگر رزرو موفق بود</returns>
|
||||||
|
Task<bool> ReserveStockAsync(
|
||||||
|
long productId,
|
||||||
|
ProductType productType,
|
||||||
|
int quantity,
|
||||||
|
long? orderId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آزادسازی رزرو (لغو سفارش یا timeout)
|
||||||
|
/// این متد در CancelOrderCommandHandler فراخوانی میشود
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> ReleaseReservationAsync(
|
||||||
|
long productId,
|
||||||
|
ProductType productType,
|
||||||
|
int quantity,
|
||||||
|
long? orderId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تایید فروش - کسر واقعی موجودی
|
||||||
|
/// این متد در CompleteOrderPaymentCommandHandler فراخوانی میشود
|
||||||
|
/// ReservedQuantity کاهش مییابد، Quantity کاهش مییابد، Product.RemainingCount sync میشود
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> ConfirmSaleAsync(
|
||||||
|
long productId,
|
||||||
|
ProductType productType,
|
||||||
|
int quantity,
|
||||||
|
long? orderId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Stock Management Operations
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ورود کالا به انبار (Restock)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="productId">شناسه محصول</param>
|
||||||
|
/// <param name="productType">نوع محصول</param>
|
||||||
|
/// <param name="quantity">تعداد ورودی</param>
|
||||||
|
/// <param name="referenceNumber">شماره مرجع (مثل شماره فاکتور خرید)</param>
|
||||||
|
/// <param name="note">یادداشت</param>
|
||||||
|
/// <param name="performedByUserId">شناسه کاربر انجامدهنده</param>
|
||||||
|
/// <param name="ct">CancellationToken</param>
|
||||||
|
Task<bool> AddStockAsync(
|
||||||
|
long productId,
|
||||||
|
ProductType productType,
|
||||||
|
int quantity,
|
||||||
|
string? referenceNumber = null,
|
||||||
|
string? note = null,
|
||||||
|
long? performedByUserId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تعدیل موجودی (تنظیم به مقدار جدید)
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> AdjustStockAsync(
|
||||||
|
long productId,
|
||||||
|
ProductType productType,
|
||||||
|
int newQuantity,
|
||||||
|
string? note = null,
|
||||||
|
long? performedByUserId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ثبت برگشت کالا از مشتری
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> ProcessReturnAsync(
|
||||||
|
long productId,
|
||||||
|
ProductType productType,
|
||||||
|
int quantity,
|
||||||
|
long? orderId = null,
|
||||||
|
string? note = null,
|
||||||
|
long? performedByUserId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ثبت ضایعات/مفقودی
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> RecordLossAsync(
|
||||||
|
long productId,
|
||||||
|
ProductType productType,
|
||||||
|
int quantity,
|
||||||
|
StockMovementType lossType, // Damaged or Lost
|
||||||
|
string? note = null,
|
||||||
|
long? performedByUserId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Bulk Operations
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// رزرو موجودی برای چند آیتم (یک سفارش با چند محصول)
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> BulkReserveStockAsync(
|
||||||
|
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
|
||||||
|
long? orderId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آزادسازی رزرو برای چند آیتم
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> BulkReleaseReservationAsync(
|
||||||
|
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
|
||||||
|
long? orderId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تایید فروش برای چند آیتم
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> BulkConfirmSaleAsync(
|
||||||
|
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
|
||||||
|
long? orderId = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user