Compare commits
70 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 |
@@ -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"
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
- production
|
- production
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: git.foursat.afrino.co
|
REGISTRY: 194.5.195.53:30080
|
||||||
IMAGE_NAME: admin/cms
|
IMAGE_NAME: admin/cms
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -24,8 +24,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
apk add --no-cache git curl
|
apk add --no-cache git curl
|
||||||
|
|
||||||
# Install kubectl
|
# Install kubectl with fixed version
|
||||||
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
KUBECTL_VERSION="v1.31.0"
|
||||||
|
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
|
||||||
chmod +x kubectl
|
chmod +x kubectl
|
||||||
mv kubectl /usr/local/bin/
|
mv kubectl /usr/local/bin/
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ jobs:
|
|||||||
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:32082", "gitea-svc:3000"]
|
||||||
}
|
}
|
||||||
DAEMON
|
DAEMON
|
||||||
mkdir -p ~/.docker
|
mkdir -p ~/.docker
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,490 +0,0 @@
|
|||||||
# Club Feature Management Services - Implementation Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Admin services for managing user club features (enable/disable features per user).
|
|
||||||
|
|
||||||
## Created Files
|
|
||||||
|
|
||||||
### 1. CQRS Layer (Application)
|
|
||||||
|
|
||||||
#### Query: GetUserClubFeatures
|
|
||||||
**Location:** `/CMS/src/CMSMicroservice.Application/ClubFeatureCQ/Queries/GetUserClubFeatures/`
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `GetUserClubFeaturesQuery.cs` - Query definition
|
|
||||||
- `GetUserClubFeaturesQueryHandler.cs` - Query handler
|
|
||||||
- `UserClubFeatureDto.cs` - Response DTO
|
|
||||||
|
|
||||||
**Purpose:** Get list of all club features for a specific user with their active status.
|
|
||||||
|
|
||||||
**Input:**
|
|
||||||
```csharp
|
|
||||||
public record GetUserClubFeaturesQuery : IRequest<List<UserClubFeatureDto>>
|
|
||||||
{
|
|
||||||
public long UserId { get; init; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output:**
|
|
||||||
```csharp
|
|
||||||
public class UserClubFeatureDto
|
|
||||||
{
|
|
||||||
public long Id { get; set; }
|
|
||||||
public long UserId { get; set; }
|
|
||||||
public long ClubMembershipId { get; set; }
|
|
||||||
public long ClubFeatureId { get; set; }
|
|
||||||
public string FeatureTitle { get; set; }
|
|
||||||
public string? FeatureDescription { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
public DateTime GrantedAt { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Logic:**
|
|
||||||
- Joins `UserClubFeatures` with `ClubFeature` table
|
|
||||||
- Filters by `UserId` and `!IsDeleted`
|
|
||||||
- Returns list of features with their active status
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Command: ToggleUserClubFeature
|
|
||||||
**Location:** `/CMS/src/CMSMicroservice.Application/ClubFeatureCQ/Commands/ToggleUserClubFeature/`
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `ToggleUserClubFeatureCommand.cs` - Command definition
|
|
||||||
- `ToggleUserClubFeatureCommandHandler.cs` - Command handler
|
|
||||||
- `ToggleUserClubFeatureResponse.cs` - Response DTO
|
|
||||||
|
|
||||||
**Purpose:** Enable or disable a specific club feature for a user.
|
|
||||||
|
|
||||||
**Input:**
|
|
||||||
```csharp
|
|
||||||
public record ToggleUserClubFeatureCommand : IRequest<ToggleUserClubFeatureResponse>
|
|
||||||
{
|
|
||||||
public long UserId { get; init; }
|
|
||||||
public long ClubFeatureId { get; init; }
|
|
||||||
public bool IsActive { get; init; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output:**
|
|
||||||
```csharp
|
|
||||||
public class ToggleUserClubFeatureResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; }
|
|
||||||
public long? UserClubFeatureId { get; set; }
|
|
||||||
public bool? IsActive { get; set; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Validations:**
|
|
||||||
1. ✅ User exists and not deleted
|
|
||||||
2. ✅ Club feature exists and not deleted
|
|
||||||
3. ✅ User has this feature assigned (exists in UserClubFeatures)
|
|
||||||
|
|
||||||
**Logic:**
|
|
||||||
- Find `UserClubFeature` record by `UserId` + `ClubFeatureId`
|
|
||||||
- Update `IsActive` field
|
|
||||||
- Set `LastModified` timestamp
|
|
||||||
- Save changes
|
|
||||||
|
|
||||||
**Error Messages:**
|
|
||||||
- "کاربر یافت نشد" - User not found
|
|
||||||
- "ویژگی باشگاه یافت نشد" - Club feature not found
|
|
||||||
- "این ویژگی برای کاربر یافت نشد" - User doesn't have this feature
|
|
||||||
|
|
||||||
**Success Messages:**
|
|
||||||
- "ویژگی با موفقیت فعال شد" - Feature activated successfully
|
|
||||||
- "ویژگی با موفقیت غیرفعال شد" - Feature deactivated successfully
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. gRPC Layer (Protobuf + WebApi)
|
|
||||||
|
|
||||||
#### Proto Definition
|
|
||||||
**File:** `/CMS/src/CMSMicroservice.Protobuf/Protos/clubmembership.proto`
|
|
||||||
|
|
||||||
**Added RPC Methods:**
|
|
||||||
```protobuf
|
|
||||||
rpc GetUserClubFeatures(GetUserClubFeaturesRequest) returns (GetUserClubFeaturesResponse){
|
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/ClubFeature/GetUserFeatures"
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
rpc ToggleUserClubFeature(ToggleUserClubFeatureRequest) returns (ToggleUserClubFeatureResponse){
|
|
||||||
option (google.api.http) = {
|
|
||||||
post: "/ClubFeature/ToggleFeature"
|
|
||||||
body: "*"
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Message Definitions:**
|
|
||||||
```protobuf
|
|
||||||
message GetUserClubFeaturesRequest {
|
|
||||||
int64 user_id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GetUserClubFeaturesResponse {
|
|
||||||
repeated UserClubFeatureModel features = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message UserClubFeatureModel {
|
|
||||||
int64 id = 1;
|
|
||||||
int64 user_id = 2;
|
|
||||||
int64 club_membership_id = 3;
|
|
||||||
int64 club_feature_id = 4;
|
|
||||||
string feature_title = 5;
|
|
||||||
string feature_description = 6;
|
|
||||||
bool is_active = 7;
|
|
||||||
google.protobuf.Timestamp granted_at = 8;
|
|
||||||
string notes = 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ToggleUserClubFeatureRequest {
|
|
||||||
int64 user_id = 1;
|
|
||||||
int64 club_feature_id = 2;
|
|
||||||
bool is_active = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ToggleUserClubFeatureResponse {
|
|
||||||
bool success = 1;
|
|
||||||
string message = 2;
|
|
||||||
google.protobuf.Int64Value user_club_feature_id = 3;
|
|
||||||
google.protobuf.BoolValue is_active = 4;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### gRPC Service Implementation
|
|
||||||
**File:** `/CMS/src/CMSMicroservice.WebApi/Services/ClubMembershipService.cs`
|
|
||||||
|
|
||||||
**Added Methods:**
|
|
||||||
```csharp
|
|
||||||
public override async Task<GetUserClubFeaturesResponse> GetUserClubFeatures(
|
|
||||||
GetUserClubFeaturesRequest request,
|
|
||||||
ServerCallContext context)
|
|
||||||
{
|
|
||||||
return await _dispatchRequestToCQRS.Handle<
|
|
||||||
GetUserClubFeaturesRequest,
|
|
||||||
GetUserClubFeaturesQuery,
|
|
||||||
GetUserClubFeaturesResponse>(request, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<Protobuf.Protos.ClubMembership.ToggleUserClubFeatureResponse>
|
|
||||||
ToggleUserClubFeature(
|
|
||||||
ToggleUserClubFeatureRequest request,
|
|
||||||
ServerCallContext context)
|
|
||||||
{
|
|
||||||
return await _dispatchRequestToCQRS.Handle<
|
|
||||||
ToggleUserClubFeatureRequest,
|
|
||||||
ToggleUserClubFeatureCommand,
|
|
||||||
Protobuf.Protos.ClubMembership.ToggleUserClubFeatureResponse>(request, context);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### AutoMapper Profile
|
|
||||||
**File:** `/CMS/src/CMSMicroservice.WebApi/Common/Mappings/ClubFeatureProfile.cs`
|
|
||||||
|
|
||||||
**Mappings:**
|
|
||||||
1. `GetUserClubFeaturesRequest` → `GetUserClubFeaturesQuery`
|
|
||||||
2. `UserClubFeatureDto` → `UserClubFeatureModel` (Proto)
|
|
||||||
3. `List<UserClubFeatureDto>` → `GetUserClubFeaturesResponse`
|
|
||||||
4. `ToggleUserClubFeatureRequest` → `ToggleUserClubFeatureCommand`
|
|
||||||
5. `ToggleUserClubFeatureResponse` (App) → `ToggleUserClubFeatureResponse` (Proto)
|
|
||||||
|
|
||||||
**Special Handling:**
|
|
||||||
- DateTime conversion to `Timestamp` (Protobuf format)
|
|
||||||
- Null-safe mapping for optional fields
|
|
||||||
- Fully qualified type names to avoid ambiguity
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### 1. Get User Club Features
|
|
||||||
**Method:** GET
|
|
||||||
**Endpoint:** `/ClubFeature/GetUserFeatures`
|
|
||||||
**Request:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user_id": 123
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"features": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"user_id": 123,
|
|
||||||
"club_membership_id": 456,
|
|
||||||
"club_feature_id": 1,
|
|
||||||
"feature_title": "دسترسی به فروشگاه تخفیف",
|
|
||||||
"feature_description": "امکان خرید از فروشگاه تخفیف",
|
|
||||||
"is_active": true,
|
|
||||||
"granted_at": "2025-12-09T18:30:00Z",
|
|
||||||
"notes": "اعطا شده بهطور خودکار هنگام فعالسازی"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Toggle User Club Feature
|
|
||||||
**Method:** POST
|
|
||||||
**Endpoint:** `/ClubFeature/ToggleFeature`
|
|
||||||
**Request:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user_id": 123,
|
|
||||||
"club_feature_id": 1,
|
|
||||||
"is_active": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response (Success):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "ویژگی با موفقیت غیرفعال شد",
|
|
||||||
"user_club_feature_id": 1,
|
|
||||||
"is_active": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response (Error - User Not Found):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"message": "کاربر یافت نشد"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response (Error - Feature Not Found):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"message": "ویژگی باشگاه یافت نشد"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response (Error - User Doesn't Have Feature):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"message": "این ویژگی برای کاربر یافت نشد"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Database Schema
|
|
||||||
|
|
||||||
### Table: UserClubFeatures
|
|
||||||
Existing table with newly added `IsActive` field:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE [CMS].[UserClubFeatures]
|
|
||||||
(
|
|
||||||
[Id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
|
||||||
[UserId] BIGINT NOT NULL,
|
|
||||||
[ClubMembershipId] BIGINT NOT NULL,
|
|
||||||
[ClubFeatureId] BIGINT NOT NULL,
|
|
||||||
[GrantedAt] DATETIME2 NOT NULL,
|
|
||||||
[IsActive] BIT NOT NULL DEFAULT 1, -- ← NEW FIELD
|
|
||||||
[Notes] NVARCHAR(MAX) NULL,
|
|
||||||
[Created] DATETIME2 NOT NULL,
|
|
||||||
[CreatedBy] NVARCHAR(MAX) NULL,
|
|
||||||
[LastModified] DATETIME2 NULL,
|
|
||||||
[LastModifiedBy] NVARCHAR(MAX) NULL,
|
|
||||||
[IsDeleted] BIT NOT NULL DEFAULT 0,
|
|
||||||
|
|
||||||
CONSTRAINT FK_UserClubFeatures_Users FOREIGN KEY ([UserId])
|
|
||||||
REFERENCES [Identity].[Users]([Id]),
|
|
||||||
CONSTRAINT FK_UserClubFeatures_ClubMembership FOREIGN KEY ([ClubMembershipId])
|
|
||||||
REFERENCES [CMS].[ClubMembership]([Id]),
|
|
||||||
CONSTRAINT FK_UserClubFeatures_ClubFeatures FOREIGN KEY ([ClubFeatureId])
|
|
||||||
REFERENCES [CMS].[ClubFeatures]([Id])
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Admin Panel Scenario
|
|
||||||
|
|
||||||
#### 1. View User's Club Features
|
|
||||||
```csharp
|
|
||||||
// Admin selects user ID: 123
|
|
||||||
var request = new GetUserClubFeaturesRequest { UserId = 123 };
|
|
||||||
var response = await client.GetUserClubFeaturesAsync(request);
|
|
||||||
|
|
||||||
// Display in grid:
|
|
||||||
foreach (var feature in response.Features)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Feature: {feature.FeatureTitle}");
|
|
||||||
Console.WriteLine($"Status: {(feature.IsActive ? "فعال" : "غیرفعال")}");
|
|
||||||
Console.WriteLine($"Granted: {feature.GrantedAt}");
|
|
||||||
Console.WriteLine("---");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output:**
|
|
||||||
```
|
|
||||||
Feature: دسترسی به فروشگاه تخفیف
|
|
||||||
Status: فعال
|
|
||||||
Granted: 2025-12-09 18:30:00
|
|
||||||
---
|
|
||||||
Feature: دسترسی به کمیسیون هفتگی
|
|
||||||
Status: فعال
|
|
||||||
Granted: 2025-12-09 18:30:00
|
|
||||||
---
|
|
||||||
Feature: دسترسی به شارژ شبکه
|
|
||||||
Status: غیرفعال
|
|
||||||
Granted: 2025-12-09 18:30:00
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 2. Disable a Feature
|
|
||||||
```csharp
|
|
||||||
// Admin clicks "Disable" on Feature ID: 3
|
|
||||||
var request = new ToggleUserClubFeatureRequest
|
|
||||||
{
|
|
||||||
UserId = 123,
|
|
||||||
ClubFeatureId = 3,
|
|
||||||
IsActive = false
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await client.ToggleUserClubFeatureAsync(request);
|
|
||||||
|
|
||||||
if (response.Success)
|
|
||||||
{
|
|
||||||
Console.WriteLine(response.Message);
|
|
||||||
// Output: ویژگی با موفقیت غیرفعال شد
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 3. Re-enable a Feature
|
|
||||||
```csharp
|
|
||||||
// Admin clicks "Enable" on Feature ID: 3
|
|
||||||
var request = new ToggleUserClubFeatureRequest
|
|
||||||
{
|
|
||||||
UserId = 123,
|
|
||||||
ClubFeatureId = 3,
|
|
||||||
IsActive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await client.ToggleUserClubFeatureAsync(request);
|
|
||||||
|
|
||||||
if (response.Success)
|
|
||||||
{
|
|
||||||
Console.WriteLine(response.Message);
|
|
||||||
// Output: ویژگی با موفقیت فعال شد
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Checklist
|
|
||||||
|
|
||||||
### Unit Tests (Recommended)
|
|
||||||
- [ ] GetUserClubFeaturesQueryHandler returns correct DTOs
|
|
||||||
- [ ] ToggleUserClubFeatureCommandHandler validates user exists
|
|
||||||
- [ ] ToggleUserClubFeatureCommandHandler validates feature exists
|
|
||||||
- [ ] ToggleUserClubFeatureCommandHandler validates user has feature
|
|
||||||
- [ ] ToggleUserClubFeatureCommandHandler updates IsActive correctly
|
|
||||||
- [ ] ToggleUserClubFeatureCommandHandler sets LastModified timestamp
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
- [ ] gRPC GetUserClubFeatures endpoint returns data
|
|
||||||
- [ ] gRPC ToggleUserClubFeature endpoint updates database
|
|
||||||
- [ ] AutoMapper mappings work correctly
|
|
||||||
- [ ] Proto serialization/deserialization works
|
|
||||||
|
|
||||||
### Manual Testing
|
|
||||||
1. **Get Features:**
|
|
||||||
```bash
|
|
||||||
grpcurl -d '{"user_id": 123}' \
|
|
||||||
-plaintext localhost:5000 \
|
|
||||||
clubmembership.ClubMembershipContract/GetUserClubFeatures
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Disable Feature:**
|
|
||||||
```bash
|
|
||||||
grpcurl -d '{"user_id": 123, "club_feature_id": 1, "is_active": false}' \
|
|
||||||
-plaintext localhost:5000 \
|
|
||||||
clubmembership.ClubMembershipContract/ToggleUserClubFeature
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Verify in Database:**
|
|
||||||
```sql
|
|
||||||
SELECT Id, UserId, ClubFeatureId, IsActive, LastModified
|
|
||||||
FROM CMS.UserClubFeatures
|
|
||||||
WHERE UserId = 123;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Build Status
|
|
||||||
✅ **All projects build successfully**
|
|
||||||
- CMSMicroservice.Domain: ✅
|
|
||||||
- CMSMicroservice.Application: ✅ (0 errors, 274 warnings)
|
|
||||||
- CMSMicroservice.Protobuf: ✅
|
|
||||||
- CMSMicroservice.WebApi: ✅ (0 errors, 17 warnings)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps (Optional Enhancements)
|
|
||||||
|
|
||||||
1. **Authorization:**
|
|
||||||
- Add `[Authorize(Roles = "Admin")]` attribute
|
|
||||||
- Validate admin permissions before toggling
|
|
||||||
|
|
||||||
2. **Audit Logging:**
|
|
||||||
- Log who changed the feature status
|
|
||||||
- Track `LastModifiedBy` field
|
|
||||||
|
|
||||||
3. **Bulk Operations:**
|
|
||||||
- Add endpoint to toggle multiple features at once
|
|
||||||
- Add endpoint to enable/disable all features for a user
|
|
||||||
|
|
||||||
4. **History Tracking:**
|
|
||||||
- Create `UserClubFeatureHistory` table
|
|
||||||
- Log every status change with timestamp and reason
|
|
||||||
|
|
||||||
5. **Notifications:**
|
|
||||||
- Send notification to user when feature is disabled
|
|
||||||
- Email/SMS alert for important features
|
|
||||||
|
|
||||||
6. **Business Rules:**
|
|
||||||
- Add validation: prevent disabling critical features
|
|
||||||
- Add expiration dates for features
|
|
||||||
- Add feature dependencies (e.g., Feature B requires Feature A)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
✅ Created CQRS Query + Command for club feature management
|
|
||||||
✅ Created gRPC Proto definitions and services
|
|
||||||
✅ Created AutoMapper mappings
|
|
||||||
✅ All builds successful
|
|
||||||
✅ Ready for deployment and testing
|
|
||||||
|
|
||||||
**Total Files Created:** 8
|
|
||||||
**Total Lines of Code:** ~350
|
|
||||||
**Build Errors:** 0
|
|
||||||
**Status:** ✅ Complete and ready for use
|
|
||||||
+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,6 +7,8 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
|
|||||||
+6
-29
@@ -1,5 +1,6 @@
|
|||||||
using CMSMicroservice.Application.Common.Exceptions;
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
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.Commission;
|
||||||
@@ -126,38 +127,14 @@ public class AcceptClubMembershipContractCommandHandler
|
|||||||
};
|
};
|
||||||
await _context.UserContracts.AddAsync(userContract, cancellationToken);
|
await _context.UserContracts.AddAsync(userContract, cancellationToken);
|
||||||
|
|
||||||
// 6. دریافت مقادیر از تنظیمات سیستم
|
// 6. دریافت مقادیر از SystemConstants (استاتیک)
|
||||||
var giftValueConfig = await _context.SystemConfigurations
|
long giftValue = SystemConstants.ClubMembershipGiftValue;
|
||||||
.FirstOrDefaultAsync(
|
long activationFeeValue = SystemConstants.ClubActivationFee;
|
||||||
c => c.Key == "Club.MembershipGiftValue" && c.IsActive,
|
|
||||||
cancellationToken
|
|
||||||
);
|
|
||||||
|
|
||||||
var activationFeeConfig = await _context.SystemConfigurations
|
|
||||||
.FirstOrDefaultAsync(
|
|
||||||
c => c.Key == "Club.ActivationFee" && c.IsActive,
|
|
||||||
cancellationToken
|
|
||||||
);
|
|
||||||
|
|
||||||
long giftValue = 28_000_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
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
long activationFeeValue = 25_200_000; // مقدار پیشفرض
|
|
||||||
if (activationFeeConfig != null && long.TryParse(activationFeeConfig.Value, out var activationFeeConfigValue))
|
|
||||||
{
|
|
||||||
activationFeeValue = activationFeeConfigValue;
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Using Club.ActivationFee from configuration: {activationFeeValue}",
|
|
||||||
activationFeeValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. فعالسازی باشگاه مشتریان
|
// 7. فعالسازی باشگاه مشتریان
|
||||||
ClubMembership clubMembership;
|
ClubMembership clubMembership;
|
||||||
|
|||||||
+8
-44
@@ -1,6 +1,7 @@
|
|||||||
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.Commission;
|
||||||
@@ -75,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}",
|
||||||
@@ -83,7 +84,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
wallet.Balance
|
wallet.Balance
|
||||||
);
|
);
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"برای فعالسازی باشگاه مشتریان باید حداقل 56 میلیون تومان موجودی اصلی داشته باشید"
|
$"برای فعالسازی باشگاه مشتریان باید حداقل {SystemConstants.BasePackageAmount:N0} ریال موجودی اصلی داشته باشید"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,51 +136,14 @@ 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
|
|
||||||
);
|
|
||||||
|
|
||||||
var activationFeeConfig = await _context.SystemConfigurations
|
|
||||||
.FirstOrDefaultAsync(
|
|
||||||
c => c.Key == "Club.ActivationFee" && c.IsActive,
|
|
||||||
cancellationToken
|
|
||||||
);
|
|
||||||
|
|
||||||
long giftValue = 28_000_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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
long activationFeeValue = 25_200_000; // مقدار پیشفرض
|
|
||||||
if (activationFeeConfig != null && long.TryParse(activationFeeConfig.Value, out var activationFeeConfigValue))
|
|
||||||
{
|
|
||||||
activationFeeValue = activationFeeConfigValue;
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Using Club.ActivationFee from configuration: {activationFeeValue}",
|
|
||||||
activationFeeValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"Club.ActivationFee not found in configuration, using default: {activationFeeValue}",
|
|
||||||
activationFeeValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ClubMembership entity;
|
ClubMembership entity;
|
||||||
bool isNewMembership = existingMembership == null;
|
bool isNewMembership = existingMembership == null;
|
||||||
|
|||||||
+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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-13
@@ -1,3 +1,5 @@
|
|||||||
|
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>
|
||||||
@@ -74,21 +76,11 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
|||||||
var balancesList = new List<NetworkWeeklyBalance>();
|
var balancesList = new List<NetworkWeeklyBalance>();
|
||||||
var calculatedAt = DateTime.Now;
|
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.OrderBy(o=>o.Id))
|
foreach (var user in usersInNetwork.OrderBy(o=>o.Id))
|
||||||
{
|
{
|
||||||
|
|||||||
+4
-6
@@ -1,3 +1,5 @@
|
|||||||
|
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>
|
||||||
@@ -47,12 +49,8 @@ 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
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQu
|
|||||||
{
|
{
|
||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
WeekDefinitionId = x.WeekDefinitionId,
|
WeekDefinitionId = x.WeekDefinitionId,
|
||||||
WeekDisplayName = x.WeekDefinition.PersianWeekNumber,
|
WeekDisplayName = x.WeekDefinition.DisplayName,
|
||||||
TotalPoolAmount = x.TotalPoolAmount,
|
TotalPoolAmount = x.TotalPoolAmount,
|
||||||
TotalBalances = x.TotalBalances,
|
TotalBalances = x.TotalBalances,
|
||||||
ValuePerBalance = x.ValuePerBalance,
|
ValuePerBalance = x.ValuePerBalance,
|
||||||
|
|||||||
+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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-4
@@ -4,13 +4,16 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
public GetUserCommissionPayoutsQueryHandler(
|
public GetUserCommissionPayoutsQueryHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IWeekDefinitionRepository weekDefinitionRepository)
|
IWeekDefinitionRepository weekDefinitionRepository,
|
||||||
|
ICurrentUserService currentUser)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_weekDefinitionRepository = weekDefinitionRepository;
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
|
_currentUser = currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetUserCommissionPayoutsResponseDto> Handle(GetUserCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
public async Task<GetUserCommissionPayoutsResponseDto> Handle(GetUserCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
||||||
@@ -21,10 +24,20 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
|||||||
.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)
|
||||||
|
|||||||
+25
-6
@@ -4,26 +4,40 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
public GetUserWeeklyBalancesQueryHandler(
|
public GetUserWeeklyBalancesQueryHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IWeekDefinitionRepository weekDefinitionRepository)
|
IWeekDefinitionRepository weekDefinitionRepository,
|
||||||
|
ICurrentUserService currentUser)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_weekDefinitionRepository = weekDefinitionRepository;
|
_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.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// فیلتر بر اساس WeekDefinitionId (روش ترجیحی)
|
// فیلتر بر اساس WeekDefinitionId (روش ترجیحی)
|
||||||
@@ -49,10 +63,15 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
|||||||
{
|
{
|
||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
UserId = x.UserId,
|
UserId = x.UserId,
|
||||||
|
UserFullName = x.User != null ? $"{x.User.FirstName} {x.User.LastName}".Trim() : "",
|
||||||
WeekDefinitionId = x.WeekDefinitionId,
|
WeekDefinitionId = x.WeekDefinitionId,
|
||||||
WeekDisplayName = x.WeekDefinition != null ? x.WeekDefinition.DisplayName : "",
|
WeekDisplayName = x.WeekDefinition != null ? x.WeekDefinition.DisplayName : "",
|
||||||
LeftLegBalances = x.LeftLegBalances,
|
LeftLegNewMembers = x.LeftLegNewMembers,
|
||||||
RightLegBalances = x.RightLegBalances,
|
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,
|
||||||
|
|||||||
+12
-3
@@ -10,11 +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 UserFullName { get; set; } = string.Empty;
|
||||||
public long WeekDefinitionId { get; set; }
|
public long WeekDefinitionId { get; set; }
|
||||||
|
|
||||||
public string WeekDisplayName { get; set; } = string.Empty;
|
public string WeekDisplayName { get; set; } = string.Empty;
|
||||||
public int LeftLegBalances { get; set; }
|
|
||||||
public int RightLegBalances { get; set; }
|
// چپ
|
||||||
|
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; }
|
||||||
|
|||||||
+1
@@ -107,6 +107,7 @@ public class GetWeekDefinitionsQueryHandler : IRequestHandler<GetWeekDefinitions
|
|||||||
// تبدیل به DTO
|
// تبدیل به DTO
|
||||||
var weekDtos = weeksList.Select(w => new WeekDefinitionItemDto
|
var weekDtos = weeksList.Select(w => new WeekDefinitionItemDto
|
||||||
{
|
{
|
||||||
|
Id = w.Id,
|
||||||
WeekOrder = w.WeekOrder,
|
WeekOrder = w.WeekOrder,
|
||||||
DisplayName = w.DisplayName,
|
DisplayName = w.DisplayName,
|
||||||
GregorianWeekNumber = w.GregorianWeekNumber,
|
GregorianWeekNumber = w.GregorianWeekNumber,
|
||||||
|
|||||||
+3
@@ -17,6 +17,9 @@ public class GetWeekDefinitionsResponseDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class WeekDefinitionItemDto
|
public class WeekDefinitionItemDto
|
||||||
{
|
{
|
||||||
|
//شناسه هفته
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
//شماره ترتیب هفته (1, 2, 3, ...)
|
//شماره ترتیب هفته (1, 2, 3, ...)
|
||||||
public int WeekOrder { get; set; }
|
public int WeekOrder { 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; }
|
||||||
|
}
|
||||||
@@ -31,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; }
|
||||||
@@ -47,11 +45,13 @@ public interface IApplicationDbContext
|
|||||||
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; }
|
||||||
@@ -61,6 +61,11 @@ public interface IApplicationDbContext
|
|||||||
DbSet<State> States { get; }
|
DbSet<State> States { get; }
|
||||||
DbSet<City> Cities { get; }
|
DbSet<City> Cities { get; }
|
||||||
|
|
||||||
|
// ============= Inventory Management =============
|
||||||
|
DbSet<Warehouse> Warehouses { get; }
|
||||||
|
DbSet<InventoryItem> InventoryItems { get; }
|
||||||
|
DbSet<StockMovement> StockMovements { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures
|
/// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس ارسال SMS با کاوهنگار
|
||||||
|
/// </summary>
|
||||||
|
public interface IKavenegarService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ارسال پیامک ساده
|
||||||
|
/// </summary>
|
||||||
|
Task SendAsync(string mobile, string message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ارسال پیامک با قالب (VerifyLookup)
|
||||||
|
/// </summary>
|
||||||
|
Task VerifyLookupAsync(string mobile, string token, string template = "Afrino");
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using CMSMicroservice.Application.UserCartsCQ.Queries.GetAllUserCartsByFilter;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.Common.Mappings;
|
|
||||||
|
|
||||||
public class UserCartsProfile : IRegister
|
|
||||||
{
|
|
||||||
void IRegister.Register(TypeAdapterConfig config)
|
|
||||||
{
|
|
||||||
config.NewConfig<UserCart,GetAllUserCartsByFilterResponseModel>()
|
|
||||||
.Map(dest => dest.Id, src => src.Id)
|
|
||||||
.Map(dest => dest.Count, src => src.Count)
|
|
||||||
.Map(dest => dest.ProductId, src => src.ProductId)
|
|
||||||
.Map(dest => dest.ProductTitle, src => src.Product.Title)
|
|
||||||
.Map(dest => dest.ProductShortInfomation, src => src.Product.ShortInfomation)
|
|
||||||
.Map(dest => dest.ProductDiscount, src => src.Product.Discount)
|
|
||||||
.Map(dest => dest.ProductPrice, src => src.Product.Price)
|
|
||||||
.Map(dest => dest.ProductThumbnailPath, src => src.Product.ThumbnailPath)
|
|
||||||
.Map(dest => dest.Created, src => src.Created)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
using CMSMicroservice.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter;
|
|
||||||
using CMSMicroservice.Application.UserOrderCQ.Queries.GetUserOrder;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.Common.Mappings;
|
|
||||||
|
|
||||||
public class UserOrderProfile : IRegister
|
|
||||||
{
|
|
||||||
void IRegister.Register(TypeAdapterConfig config)
|
|
||||||
{
|
|
||||||
config.NewConfig<UserOrder,GetUserOrderResponseDto>()
|
|
||||||
.Map(dest => dest.Id, src => src.Id)
|
|
||||||
.Map(dest => dest.Amount, src => src.Amount)
|
|
||||||
.Map(dest => dest.PackageId, src => src.PackageId)
|
|
||||||
.Map(dest => dest.TransactionId, src => src.TransactionId)
|
|
||||||
.Map(dest => dest.PaymentStatus, src => src.PaymentStatus)
|
|
||||||
.Map(dest => dest.PaymentDate, src => src.PaymentDate)
|
|
||||||
.Map(dest => dest.UserId, src => src.UserId)
|
|
||||||
.Map(dest => dest.UserAddressId, src => src.UserAddressId)
|
|
||||||
.Map(dest => dest.PaymentMethod, src => src.PaymentMethod)
|
|
||||||
.Map(dest => dest.UserAddressText, src => src.UserAddress.Address)
|
|
||||||
.Map(dest => dest.FactorDetails, src => src.FactorDetails.Select(s=>s.Adapt<GetUserOrderResponseFactorDetail>()))
|
|
||||||
|
|
||||||
;
|
|
||||||
|
|
||||||
config.NewConfig<UserOrder,GetAllUserOrderByFilterResponseModel>()
|
|
||||||
.Map(dest => dest.Id, src => src.Id)
|
|
||||||
.Map(dest => dest.Amount, src => src.Amount)
|
|
||||||
.Map(dest => dest.PackageId, src => src.PackageId)
|
|
||||||
.Map(dest => dest.TransactionId, src => src.TransactionId)
|
|
||||||
.Map(dest => dest.PaymentStatus, src => src.PaymentStatus)
|
|
||||||
.Map(dest => dest.PaymentDate, src => src.PaymentDate)
|
|
||||||
.Map(dest => dest.UserId, src => src.UserId)
|
|
||||||
.Map(dest => dest.UserAddressId, src => src.UserAddressId)
|
|
||||||
.Map(dest => dest.PaymentMethod, src => src.PaymentMethod)
|
|
||||||
.Map(dest => dest.UserAddressText, src => src.UserAddress.Address)
|
|
||||||
.Map(dest => dest.FactorDetails, src => src.FactorDetails.Select(s=>s.Adapt<GetUserOrderResponseFactorDetail>()))
|
|
||||||
;
|
|
||||||
|
|
||||||
config.NewConfig<FactorDetails,GetUserOrderResponseFactorDetail>()
|
|
||||||
.Map(dest => dest.ProductId, src => src.ProductId)
|
|
||||||
.Map(dest => dest.ProductTitle, src => src.Product.Title)
|
|
||||||
.Map(dest => dest.ProductThumbnailPath, src => src.Product.ThumbnailPath)
|
|
||||||
.Map(dest => dest.UnitPrice, src => src.Product.Price)
|
|
||||||
.Map(dest => dest.Count, src => src.Count)
|
|
||||||
.Map(dest => dest.UnitDiscountPrice, src => src.Product.Price*(src.Product.Discount/100))
|
|
||||||
;
|
|
||||||
|
|
||||||
config.NewConfig<FactorDetails,GetAllUserOrderByFilterResponseModelFactorDetail>()
|
|
||||||
.Map(dest => dest.ProductId, src => src.ProductId)
|
|
||||||
.Map(dest => dest.ProductTitle, src => src.Product.Title)
|
|
||||||
.Map(dest => dest.ProductThumbnailPath, src => src.Product.ThumbnailPath)
|
|
||||||
.Map(dest => dest.UnitPrice, src => src.Product.Price)
|
|
||||||
.Map(dest => dest.Count, src => src.Count)
|
|
||||||
.Map(dest => dest.UnitDiscountPrice, src => src.Product.Price*(src.Product.Discount/100))
|
|
||||||
;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس محاسبه مالیات بر ارزش افزوده (VAT)
|
||||||
|
/// </summary>
|
||||||
|
public static class VatCalculator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// نرخ VAT ایران - 9 درصد
|
||||||
|
/// </summary>
|
||||||
|
public const decimal VAT_RATE = 0.09m;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نرخ VAT به صورت درصد (9)
|
||||||
|
/// </summary>
|
||||||
|
public const int VAT_PERCENT = 9;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// محاسبه VAT از مبلغ خالص
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="netAmount">مبلغ خالص (بدون مالیات)</param>
|
||||||
|
/// <returns>مبلغ VAT</returns>
|
||||||
|
public static long CalculateVat(long netAmount)
|
||||||
|
{
|
||||||
|
return (long)(netAmount * VAT_RATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// محاسبه مبلغ ناخالص (شامل VAT) از مبلغ خالص
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="netAmount">مبلغ خالص</param>
|
||||||
|
/// <returns>مبلغ ناخالص (خالص + VAT)</returns>
|
||||||
|
public static long CalculateGrossAmount(long netAmount)
|
||||||
|
{
|
||||||
|
return netAmount + CalculateVat(netAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// استخراج مبلغ خالص از مبلغ ناخالص
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="grossAmount">مبلغ ناخالص (شامل VAT)</param>
|
||||||
|
/// <returns>مبلغ خالص</returns>
|
||||||
|
public static long ExtractNetAmount(long grossAmount)
|
||||||
|
{
|
||||||
|
return (long)(grossAmount / (1 + VAT_RATE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// استخراج VAT از مبلغ ناخالص
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="grossAmount">مبلغ ناخالص (شامل VAT)</param>
|
||||||
|
/// <returns>مبلغ VAT</returns>
|
||||||
|
public static long ExtractVatFromGross(long grossAmount)
|
||||||
|
{
|
||||||
|
return grossAmount - ExtractNetAmount(grossAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// جزئیات محاسبه VAT
|
||||||
|
/// </summary>
|
||||||
|
public record VatBreakdown(
|
||||||
|
long NetAmount,
|
||||||
|
long VatAmount,
|
||||||
|
long GrossAmount,
|
||||||
|
decimal VatRate
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// محاسبه کامل جزئیات VAT
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="netAmount">مبلغ خالص</param>
|
||||||
|
/// <returns>جزئیات کامل VAT</returns>
|
||||||
|
public static VatBreakdown CalculateBreakdown(long netAmount)
|
||||||
|
{
|
||||||
|
var vatAmount = CalculateVat(netAmount);
|
||||||
|
return new VatBreakdown(
|
||||||
|
NetAmount: netAmount,
|
||||||
|
VatAmount: vatAmount,
|
||||||
|
GrossAmount: netAmount + vatAmount,
|
||||||
|
VatRate: VAT_RATE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.DeactivateConfiguration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command برای غیرفعال کردن یک Configuration
|
|
||||||
/// </summary>
|
|
||||||
public record DeactivateConfigurationCommand : IRequest<Unit>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// شناسه Configuration
|
|
||||||
/// </summary>
|
|
||||||
public long ConfigurationId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// دلیل غیرفعالسازی
|
|
||||||
/// </summary>
|
|
||||||
public string? Reason { get; init; }
|
|
||||||
}
|
|
||||||
-51
@@ -1,51 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.DeactivateConfiguration;
|
|
||||||
|
|
||||||
public class DeactivateConfigurationCommandHandler : IRequestHandler<DeactivateConfigurationCommand, Unit>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly ICurrentUserService _currentUser;
|
|
||||||
|
|
||||||
public DeactivateConfigurationCommandHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
ICurrentUserService currentUser)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_currentUser = currentUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Unit> Handle(DeactivateConfigurationCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var entity = await _context.SystemConfigurations
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == request.ConfigurationId, cancellationToken)
|
|
||||||
?? throw new NotFoundException(nameof(SystemConfiguration), request.ConfigurationId);
|
|
||||||
|
|
||||||
// اگر از قبل غیرفعال است، خطا ندهیم
|
|
||||||
if (!entity.IsActive)
|
|
||||||
{
|
|
||||||
return Unit.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var oldValue = entity.Value;
|
|
||||||
entity.IsActive = false;
|
|
||||||
|
|
||||||
_context.SystemConfigurations.Update(entity);
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
// ثبت تاریخچه
|
|
||||||
var history = new SystemConfigurationHistory
|
|
||||||
{
|
|
||||||
ConfigurationId = entity.Id,
|
|
||||||
Scope = entity.Scope,
|
|
||||||
Key = entity.Key,
|
|
||||||
OldValue = oldValue,
|
|
||||||
NewValue = entity.Value,
|
|
||||||
Reason = request.Reason ?? "Configuration deactivated",
|
|
||||||
PerformedBy = _currentUser.GetPerformedBy()
|
|
||||||
};
|
|
||||||
|
|
||||||
await _context.SystemConfigurationHistories.AddAsync(history, cancellationToken);
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Unit.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.DeactivateConfiguration;
|
|
||||||
|
|
||||||
public class DeactivateConfigurationCommandValidator : AbstractValidator<DeactivateConfigurationCommand>
|
|
||||||
{
|
|
||||||
public DeactivateConfigurationCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.ConfigurationId)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("شناسه Configuration معتبر نیست");
|
|
||||||
|
|
||||||
RuleFor(x => x.Reason)
|
|
||||||
.MaximumLength(500)
|
|
||||||
.WithMessage("دلیل غیرفعالسازی نمیتواند بیشتر از 500 کاراکتر باشد")
|
|
||||||
.When(x => !string.IsNullOrEmpty(x.Reason));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(
|
|
||||||
ValidationContext<DeactivateConfigurationCommand>.CreateWithOptions(
|
|
||||||
(DeactivateConfigurationCommand)model,
|
|
||||||
x => x.IncludeProperties(propertyName)));
|
|
||||||
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-77
@@ -1,77 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
|
||||||
using CMSMicroservice.Domain.Enums;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.SeedVATConfiguration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Seed initial VAT configuration
|
|
||||||
/// نرخ مالیات پیشفرض ۹٪
|
|
||||||
/// </summary>
|
|
||||||
public class SeedVATConfigurationCommand : IRequest<Unit>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SeedVATConfigurationCommandHandler : IRequestHandler<SeedVATConfigurationCommand, Unit>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly ILogger<SeedVATConfigurationCommandHandler> _logger;
|
|
||||||
|
|
||||||
public SeedVATConfigurationCommandHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
ILogger<SeedVATConfigurationCommandHandler> logger)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Unit> Handle(SeedVATConfigurationCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var configs = new[]
|
|
||||||
{
|
|
||||||
new
|
|
||||||
{
|
|
||||||
Scope = ConfigurationScope.VAT,
|
|
||||||
Key = "Rate",
|
|
||||||
Value = "0.09",
|
|
||||||
Description = "نرخ مالیات بر ارزش افزوده (۹٪)"
|
|
||||||
},
|
|
||||||
new
|
|
||||||
{
|
|
||||||
Scope = ConfigurationScope.VAT,
|
|
||||||
Key = "IsEnabled",
|
|
||||||
Value = "true",
|
|
||||||
Description = "فعال/غیرفعال بودن محاسبه مالیات"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var config in configs)
|
|
||||||
{
|
|
||||||
var exists = _context.SystemConfigurations
|
|
||||||
.Any(x => x.Scope == config.Scope && x.Key == config.Key);
|
|
||||||
|
|
||||||
if (!exists)
|
|
||||||
{
|
|
||||||
_context.SystemConfigurations.Add(new Domain.Entities.Configuration.SystemConfiguration
|
|
||||||
{
|
|
||||||
Scope = config.Scope,
|
|
||||||
Key = config.Key,
|
|
||||||
Value = config.Value,
|
|
||||||
Description = config.Description
|
|
||||||
});
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"VAT configuration seeded: {Scope}.{Key} = {Value}",
|
|
||||||
config.Scope,
|
|
||||||
config.Key,
|
|
||||||
config.Value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Unit.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.SetConfigurationValue;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command برای تنظیم یا بهروزرسانی یک Configuration
|
|
||||||
/// </summary>
|
|
||||||
public record SetConfigurationValueCommand : IRequest<long>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// محدوده تنظیمات (System, Network, Club, Commission)
|
|
||||||
/// </summary>
|
|
||||||
public ConfigurationScope Scope { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// کلید یکتا برای تنظیمات
|
|
||||||
/// </summary>
|
|
||||||
public string Key { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// مقدار تنظیمات (JSON format)
|
|
||||||
/// </summary>
|
|
||||||
public string Value { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// توضیحات تنظیمات
|
|
||||||
/// </summary>
|
|
||||||
public string? Description { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// دلیل تغییر (برای History)
|
|
||||||
/// </summary>
|
|
||||||
public string? ChangeReason { get; init; }
|
|
||||||
}
|
|
||||||
-78
@@ -1,78 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.SetConfigurationValue;
|
|
||||||
|
|
||||||
public class SetConfigurationValueCommandHandler : IRequestHandler<SetConfigurationValueCommand, long>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly ICurrentUserService _currentUser;
|
|
||||||
|
|
||||||
public SetConfigurationValueCommandHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
ICurrentUserService currentUser)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_currentUser = currentUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<long> Handle(SetConfigurationValueCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// بررسی وجود Configuration با همین Scope و Key
|
|
||||||
var existingConfig = await _context.SystemConfigurations
|
|
||||||
.FirstOrDefaultAsync(x =>
|
|
||||||
x.Scope == request.Scope &&
|
|
||||||
x.Key == request.Key,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
SystemConfiguration entity;
|
|
||||||
bool isNewRecord = existingConfig == null;
|
|
||||||
string oldValue = null;
|
|
||||||
|
|
||||||
if (isNewRecord)
|
|
||||||
{
|
|
||||||
// ایجاد Configuration جدید
|
|
||||||
entity = new SystemConfiguration
|
|
||||||
{
|
|
||||||
Scope = request.Scope,
|
|
||||||
Key = request.Key,
|
|
||||||
Value = request.Value,
|
|
||||||
Description = request.Description,
|
|
||||||
IsActive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
await _context.SystemConfigurations.AddAsync(entity, cancellationToken);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// بهروزرسانی Configuration موجود
|
|
||||||
entity = existingConfig;
|
|
||||||
oldValue = entity.Value;
|
|
||||||
|
|
||||||
entity.Value = request.Value;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(request.Description))
|
|
||||||
{
|
|
||||||
entity.Description = request.Description;
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.SystemConfigurations.Update(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
// ثبت تاریخچه
|
|
||||||
var history = new SystemConfigurationHistory
|
|
||||||
{
|
|
||||||
ConfigurationId = entity.Id,
|
|
||||||
Scope = entity.Scope,
|
|
||||||
Key = entity.Key,
|
|
||||||
OldValue = oldValue,
|
|
||||||
NewValue = entity.Value,
|
|
||||||
Reason = request.ChangeReason ?? (isNewRecord ? "Initial creation" : "Value updated"),
|
|
||||||
PerformedBy = "System" // TODO: باید از Current User گرفته شود
|
|
||||||
};
|
|
||||||
|
|
||||||
await _context.SystemConfigurationHistories.AddAsync(history, cancellationToken);
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
return entity.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.SetConfigurationValue;
|
|
||||||
|
|
||||||
public class SetConfigurationValueCommandValidator : AbstractValidator<SetConfigurationValueCommand>
|
|
||||||
{
|
|
||||||
public SetConfigurationValueCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.Scope)
|
|
||||||
.IsInEnum()
|
|
||||||
.WithMessage("محدوده تنظیمات معتبر نیست");
|
|
||||||
|
|
||||||
RuleFor(x => x.Key)
|
|
||||||
.NotEmpty()
|
|
||||||
.WithMessage("کلید تنظیمات الزامی است")
|
|
||||||
.MaximumLength(100)
|
|
||||||
.WithMessage("کلید تنظیمات نمیتواند بیشتر از 100 کاراکتر باشد")
|
|
||||||
.Matches(@"^[a-zA-Z0-9_\.]+$")
|
|
||||||
.WithMessage("کلید تنظیمات فقط میتواند شامل حروف انگلیسی، اعداد، نقطه و آندرلاین باشد");
|
|
||||||
|
|
||||||
RuleFor(x => x.Value)
|
|
||||||
.NotEmpty()
|
|
||||||
.WithMessage("مقدار تنظیمات الزامی است")
|
|
||||||
.MaximumLength(2000)
|
|
||||||
.WithMessage("مقدار تنظیمات نمیتواند بیشتر از 2000 کاراکتر باشد");
|
|
||||||
|
|
||||||
RuleFor(x => x.Description)
|
|
||||||
.MaximumLength(500)
|
|
||||||
.WithMessage("توضیحات نمیتواند بیشتر از 500 کاراکتر باشد")
|
|
||||||
.When(x => !string.IsNullOrEmpty(x.Description));
|
|
||||||
|
|
||||||
RuleFor(x => x.ChangeReason)
|
|
||||||
.MaximumLength(500)
|
|
||||||
.WithMessage("دلیل تغییر نمیتواند بیشتر از 500 کاراکتر باشد")
|
|
||||||
.When(x => !string.IsNullOrEmpty(x.ChangeReason));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(
|
|
||||||
ValidationContext<SetConfigurationValueCommand>.CreateWithOptions(
|
|
||||||
(SetConfigurationValueCommand)model,
|
|
||||||
x => x.IncludeProperties(propertyName)));
|
|
||||||
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetAllConfigurations;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Query برای دریافت لیست تمام Configuration ها با فیلتر
|
|
||||||
/// </summary>
|
|
||||||
public record GetAllConfigurationsQuery : IRequest<GetAllConfigurationsResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// موقعیت صفحهبندی
|
|
||||||
/// </summary>
|
|
||||||
public PaginationState? PaginationState { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// مرتبسازی بر اساس
|
|
||||||
/// </summary>
|
|
||||||
public string? SortBy { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// فیلتر
|
|
||||||
/// </summary>
|
|
||||||
public GetAllConfigurationsFilter? Filter { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetAllConfigurationsFilter
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// فیلتر بر اساس محدوده
|
|
||||||
/// </summary>
|
|
||||||
public ConfigurationScope? Scope { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// جستجو در کلید
|
|
||||||
/// </summary>
|
|
||||||
public string? KeyContains { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// فقط Configuration های فعال
|
|
||||||
/// </summary>
|
|
||||||
public bool? IsActive { get; set; }
|
|
||||||
}
|
|
||||||
-50
@@ -1,50 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetAllConfigurations;
|
|
||||||
|
|
||||||
public class GetAllConfigurationsQueryHandler : IRequestHandler<GetAllConfigurationsQuery, GetAllConfigurationsResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public GetAllConfigurationsQueryHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<GetAllConfigurationsResponseDto> Handle(GetAllConfigurationsQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var query = _context.SystemConfigurations
|
|
||||||
.ApplyOrder(sortBy: request.SortBy)
|
|
||||||
.AsNoTracking()
|
|
||||||
.AsQueryable();
|
|
||||||
|
|
||||||
if (request.Filter is not null)
|
|
||||||
{
|
|
||||||
query = query
|
|
||||||
.Where(x => request.Filter.Scope == null || x.Scope == request.Filter.Scope)
|
|
||||||
.Where(x => request.Filter.KeyContains == null || x.Key.Contains(request.Filter.KeyContains))
|
|
||||||
.Where(x => request.Filter.IsActive == null || x.IsActive == request.Filter.IsActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
|
||||||
|
|
||||||
var models = await query
|
|
||||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
|
||||||
.Select(x => new GetAllConfigurationsResponseModel
|
|
||||||
{
|
|
||||||
Id = x.Id,
|
|
||||||
Scope = x.Scope,
|
|
||||||
Key = x.Key,
|
|
||||||
Value = x.Value,
|
|
||||||
Description = x.Description,
|
|
||||||
IsActive = x.IsActive,
|
|
||||||
Created = x.Created,
|
|
||||||
LastModified = x.LastModified
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new GetAllConfigurationsResponseDto
|
|
||||||
{
|
|
||||||
MetaData = meta,
|
|
||||||
Models = models
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-25
@@ -1,25 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetAllConfigurations;
|
|
||||||
|
|
||||||
public class GetAllConfigurationsQueryValidator : AbstractValidator<GetAllConfigurationsQuery>
|
|
||||||
{
|
|
||||||
public GetAllConfigurationsQueryValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.Filter.Scope)
|
|
||||||
.IsInEnum()
|
|
||||||
.WithMessage("محدوده تنظیمات معتبر نیست")
|
|
||||||
.When(x => x.Filter?.Scope != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(
|
|
||||||
ValidationContext<GetAllConfigurationsQuery>.CreateWithOptions(
|
|
||||||
(GetAllConfigurationsQuery)model,
|
|
||||||
x => x.IncludeProperties(propertyName)));
|
|
||||||
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetAllConfigurations;
|
|
||||||
|
|
||||||
public class GetAllConfigurationsResponseDto
|
|
||||||
{
|
|
||||||
public MetaData MetaData { get; set; }
|
|
||||||
public List<GetAllConfigurationsResponseModel> Models { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetAllConfigurationsResponseModel
|
|
||||||
{
|
|
||||||
public long Id { get; set; }
|
|
||||||
public ConfigurationScope Scope { get; set; }
|
|
||||||
public string Key { get; set; }
|
|
||||||
public string Value { get; set; }
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
public DateTimeOffset Created { get; set; }
|
|
||||||
public DateTimeOffset? LastModified { get; set; }
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationByKey;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DTO برای نمایش اطلاعات Configuration
|
|
||||||
/// </summary>
|
|
||||||
public class ConfigurationDto
|
|
||||||
{
|
|
||||||
public long Id { get; set; }
|
|
||||||
public ConfigurationScope Scope { get; set; }
|
|
||||||
public string Key { get; set; }
|
|
||||||
public string Value { get; set; }
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
public DateTimeOffset Created { get; set; }
|
|
||||||
public DateTimeOffset? LastModified { get; set; }
|
|
||||||
}
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationByKey;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Query برای دریافت یک Configuration بر اساس Scope و Key
|
|
||||||
/// </summary>
|
|
||||||
public record GetConfigurationByKeyQuery : IRequest<ConfigurationDto?>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// محدوده تنظیمات
|
|
||||||
/// </summary>
|
|
||||||
public ConfigurationScope Scope { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// کلید تنظیمات
|
|
||||||
/// </summary>
|
|
||||||
public string Key { get; init; }
|
|
||||||
}
|
|
||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationByKey;
|
|
||||||
|
|
||||||
public class GetConfigurationByKeyQueryHandler : IRequestHandler<GetConfigurationByKeyQuery, ConfigurationDto?>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public GetConfigurationByKeyQueryHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ConfigurationDto?> Handle(GetConfigurationByKeyQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var config = await _context.SystemConfigurations
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(x => x.Scope == request.Scope && x.Key == request.Key)
|
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (config == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new ConfigurationDto
|
|
||||||
{
|
|
||||||
Id = config.Id,
|
|
||||||
Scope = config.Scope,
|
|
||||||
Key = config.Key,
|
|
||||||
Value = config.Value,
|
|
||||||
Description = config.Description,
|
|
||||||
IsActive = config.IsActive,
|
|
||||||
Created = config.Created,
|
|
||||||
LastModified = config.LastModified
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationByKey;
|
|
||||||
|
|
||||||
public class GetConfigurationByKeyQueryValidator : AbstractValidator<GetConfigurationByKeyQuery>
|
|
||||||
{
|
|
||||||
public GetConfigurationByKeyQueryValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.Scope)
|
|
||||||
.IsInEnum()
|
|
||||||
.WithMessage("محدوده تنظیمات معتبر نیست");
|
|
||||||
|
|
||||||
RuleFor(x => x.Key)
|
|
||||||
.NotEmpty()
|
|
||||||
.WithMessage("کلید تنظیمات الزامی است");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(
|
|
||||||
ValidationContext<GetConfigurationByKeyQuery>.CreateWithOptions(
|
|
||||||
(GetConfigurationByKeyQuery)model,
|
|
||||||
x => x.IncludeProperties(propertyName)));
|
|
||||||
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationHistory;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Query برای دریافت تاریخچه تغییرات یک Configuration
|
|
||||||
/// </summary>
|
|
||||||
public record GetConfigurationHistoryQuery : IRequest<GetConfigurationHistoryResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// شناسه Configuration
|
|
||||||
/// </summary>
|
|
||||||
public long ConfigurationId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// موقعیت صفحهبندی
|
|
||||||
/// </summary>
|
|
||||||
public PaginationState? PaginationState { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// مرتبسازی بر اساس
|
|
||||||
/// </summary>
|
|
||||||
public string? SortBy { get; init; }
|
|
||||||
}
|
|
||||||
-53
@@ -1,53 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationHistory;
|
|
||||||
|
|
||||||
public class GetConfigurationHistoryQueryHandler : IRequestHandler<GetConfigurationHistoryQuery, GetConfigurationHistoryResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public GetConfigurationHistoryQueryHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<GetConfigurationHistoryResponseDto> Handle(GetConfigurationHistoryQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// بررسی وجود Configuration
|
|
||||||
var configExists = await _context.SystemConfigurations
|
|
||||||
.AnyAsync(x => x.Id == request.ConfigurationId, cancellationToken);
|
|
||||||
|
|
||||||
if (!configExists)
|
|
||||||
{
|
|
||||||
throw new NotFoundException(nameof(SystemConfiguration), request.ConfigurationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var query = _context.SystemConfigurationHistories
|
|
||||||
.Where(x => x.ConfigurationId == request.ConfigurationId)
|
|
||||||
.ApplyOrder(sortBy: request.SortBy ?? "Created") // پیشفرض: جدیدترین اول
|
|
||||||
.AsNoTracking()
|
|
||||||
.AsQueryable();
|
|
||||||
|
|
||||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
|
||||||
|
|
||||||
var models = await query
|
|
||||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
|
||||||
.Select(x => new GetConfigurationHistoryResponseModel
|
|
||||||
{
|
|
||||||
Id = x.Id,
|
|
||||||
ConfigurationId = x.ConfigurationId,
|
|
||||||
Scope = x.Scope,
|
|
||||||
Key = x.Key,
|
|
||||||
OldValue = x.OldValue,
|
|
||||||
NewValue = x.NewValue,
|
|
||||||
ChangeReason = x.Reason,
|
|
||||||
ChangedBy = x.PerformedBy,
|
|
||||||
Created = x.Created
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new GetConfigurationHistoryResponseDto
|
|
||||||
{
|
|
||||||
MetaData = meta,
|
|
||||||
Models = models
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationHistory;
|
|
||||||
|
|
||||||
public class GetConfigurationHistoryQueryValidator : AbstractValidator<GetConfigurationHistoryQuery>
|
|
||||||
{
|
|
||||||
public GetConfigurationHistoryQueryValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.ConfigurationId)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("شناسه Configuration معتبر نیست");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(
|
|
||||||
ValidationContext<GetConfigurationHistoryQuery>.CreateWithOptions(
|
|
||||||
(GetConfigurationHistoryQuery)model,
|
|
||||||
x => x.IncludeProperties(propertyName)));
|
|
||||||
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationHistory;
|
|
||||||
|
|
||||||
public class GetConfigurationHistoryResponseDto
|
|
||||||
{
|
|
||||||
public MetaData MetaData { get; set; }
|
|
||||||
public List<GetConfigurationHistoryResponseModel> Models { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetConfigurationHistoryResponseModel
|
|
||||||
{
|
|
||||||
public long Id { get; set; }
|
|
||||||
public long ConfigurationId { get; set; }
|
|
||||||
public ConfigurationScope Scope { get; set; }
|
|
||||||
public string Key { get; set; }
|
|
||||||
public string? OldValue { get; set; }
|
|
||||||
public string NewValue { get; set; }
|
|
||||||
public string ChangeReason { get; set; }
|
|
||||||
public string ChangedBy { get; set; }
|
|
||||||
public DateTimeOffset Created { get; set; }
|
|
||||||
}
|
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckAndProcessDayaLoans;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command یکپارچه برای استعلام وضعیت وام از سرویس دایا و پردازش خودکار وامهای تأیید شده
|
||||||
|
/// این Command هم استعلام میکند و هم در صورت تأیید، کیف پول را شارژ میکند
|
||||||
|
/// </summary>
|
||||||
|
public record CheckAndProcessDayaLoansCommand : IRequest<CheckAndProcessDayaLoansResponseDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// لیست کدهای ملی برای استعلام
|
||||||
|
/// </summary>
|
||||||
|
public required List<string> NationalCodes { get; init; }
|
||||||
|
}
|
||||||
+301
@@ -0,0 +1,301 @@
|
|||||||
|
using CMSMicroservice.Domain.Events;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using CMSMicroservice.Domain.Common;
|
||||||
|
using CMSMicroservice.Application.DayaLoanCQ.Services;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckAndProcessDayaLoans;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler یکپارچه برای استعلام و پردازش وام دایا
|
||||||
|
/// 1. استعلام از API دایا
|
||||||
|
/// 2. ذخیره/بهروزرسانی DayaLoanContract
|
||||||
|
/// 3. شارژ کیف پول برای وامهای تأیید شده
|
||||||
|
/// 4. ثبت Order پکیج طلایی
|
||||||
|
/// 5. ارسال SMS
|
||||||
|
/// </summary>
|
||||||
|
public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndProcessDayaLoansCommand, CheckAndProcessDayaLoansResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IDayaLoanApiService _dayaApiService;
|
||||||
|
private readonly IKavenegarService _smsService;
|
||||||
|
private readonly ILogger<CheckAndProcessDayaLoansCommandHandler> _logger;
|
||||||
|
|
||||||
|
public CheckAndProcessDayaLoansCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IDayaLoanApiService dayaApiService,
|
||||||
|
IKavenegarService smsService,
|
||||||
|
ILogger<CheckAndProcessDayaLoansCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_dayaApiService = dayaApiService;
|
||||||
|
_smsService = smsService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CheckAndProcessDayaLoansResponseDto> Handle(CheckAndProcessDayaLoansCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var results = new List<DayaLoanProcessResult>();
|
||||||
|
var processedCount = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. استعلام از سرویس دایا
|
||||||
|
_logger.LogInformation("Checking Daya loan status for {Count} national codes", request.NationalCodes.Count);
|
||||||
|
var dayaResults = await _dayaApiService.CheckLoanStatusAsync(request.NationalCodes, cancellationToken);
|
||||||
|
|
||||||
|
foreach (var dayaResult in dayaResults)
|
||||||
|
{
|
||||||
|
var result = new DayaLoanProcessResult
|
||||||
|
{
|
||||||
|
NationalCode = dayaResult.NationalCode,
|
||||||
|
Status = dayaResult.Status,
|
||||||
|
ContractNumber = dayaResult.ContractNumber,
|
||||||
|
WasProcessed = false,
|
||||||
|
Message = "استعلام موفق"
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 2. پیدا کردن کاربر
|
||||||
|
var user = await _context.Users
|
||||||
|
.Include(u => u.UserWallets)
|
||||||
|
.FirstOrDefaultAsync(u => u.NationalCode == dayaResult.NationalCode, cancellationToken);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
result.Message = "کاربر یافت نشد";
|
||||||
|
results.Add(result);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.UserId = user.Id;
|
||||||
|
|
||||||
|
// 3. ذخیره/بهروزرسانی DayaLoanContract
|
||||||
|
var contract = await _context.DayaLoanContracts
|
||||||
|
.FirstOrDefaultAsync(d => d.NationalCode == dayaResult.NationalCode, cancellationToken);
|
||||||
|
|
||||||
|
if (contract == null)
|
||||||
|
{
|
||||||
|
contract = new DayaLoanContract
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
NationalCode = dayaResult.NationalCode,
|
||||||
|
Status = dayaResult.Status,
|
||||||
|
ContractNumber = dayaResult.ContractNumber,
|
||||||
|
LastCheckDate = DateTime.Now,
|
||||||
|
IsProcessed = false
|
||||||
|
};
|
||||||
|
await _context.DayaLoanContracts.AddAsync(contract, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
contract.Status = dayaResult.Status;
|
||||||
|
contract.ContractNumber = dayaResult.ContractNumber;
|
||||||
|
contract.LastCheckDate = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. آیا باید پردازش مالی انجام شود؟
|
||||||
|
var shouldProcess = !contract.IsProcessed &&
|
||||||
|
!user.HasReceivedDayaCredit &&
|
||||||
|
!string.IsNullOrEmpty(dayaResult.ContractNumber) &&
|
||||||
|
(dayaResult.Status == DayaLoanStatus.PendingReceive ||
|
||||||
|
dayaResult.Status == DayaLoanStatus.Received);
|
||||||
|
|
||||||
|
if (shouldProcess)
|
||||||
|
{
|
||||||
|
// 5. پردازش مالی
|
||||||
|
var processResult = await ProcessDayaLoanAsync(user, dayaResult.ContractNumber!, cancellationToken);
|
||||||
|
|
||||||
|
contract.IsProcessed = true;
|
||||||
|
result.WasProcessed = true;
|
||||||
|
result.NewWalletBalance = processResult.NewBalance;
|
||||||
|
result.Message = "وام پردازش و کیف پول شارژ شد";
|
||||||
|
processedCount++;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Daya loan processed for User {UserId}, Contract: {ContractNumber}, NewBalance: {Balance}",
|
||||||
|
user.Id, dayaResult.ContractNumber, processResult.NewBalance);
|
||||||
|
}
|
||||||
|
else if (contract.IsProcessed || user.HasReceivedDayaCredit)
|
||||||
|
{
|
||||||
|
result.Message = "قبلاً پردازش شده";
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrEmpty(dayaResult.ContractNumber))
|
||||||
|
{
|
||||||
|
result.Message = "هنوز قرارداد ندارد";
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error processing Daya loan for NationalCode {NationalCode}", dayaResult.NationalCode);
|
||||||
|
result.Message = $"خطا: {ex.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
results.Add(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error calling Daya API service");
|
||||||
|
|
||||||
|
// در صورت خطا در API
|
||||||
|
foreach (var nationalCode in request.NationalCodes)
|
||||||
|
{
|
||||||
|
if (!results.Any(r => r.NationalCode == nationalCode))
|
||||||
|
{
|
||||||
|
results.Add(new DayaLoanProcessResult
|
||||||
|
{
|
||||||
|
NationalCode = nationalCode,
|
||||||
|
Status = DayaLoanStatus.PendingReceive,
|
||||||
|
WasProcessed = false,
|
||||||
|
Message = $"خطا در استعلام: {ex.Message}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CheckAndProcessDayaLoansResponseDto
|
||||||
|
{
|
||||||
|
Results = results,
|
||||||
|
TotalChecked = request.NationalCodes.Count,
|
||||||
|
WithContractCount = results.Count(r => !string.IsNullOrEmpty(r.ContractNumber)),
|
||||||
|
ProcessedCount = processedCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پردازش مالی وام دایا
|
||||||
|
/// </summary>
|
||||||
|
private async Task<(long NewBalance, long TransactionId)> ProcessDayaLoanAsync(
|
||||||
|
User user,
|
||||||
|
string contractNumber,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. ایجاد تراکنش
|
||||||
|
var transaction = new Transaction
|
||||||
|
{
|
||||||
|
Amount = SystemConstants.DayaLoanAmount,
|
||||||
|
Description = $"دریافت اعتبار دایا - قرارداد {contractNumber}",
|
||||||
|
PaymentStatus = PaymentStatus.Success,
|
||||||
|
PaymentDate = DateTime.Now,
|
||||||
|
RefId = contractNumber,
|
||||||
|
Type = TransactionType.DepositExternal1
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.Transactions.AddAsync(transaction, cancellationToken);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 2. یافتن یا ایجاد کیف پول
|
||||||
|
var wallet = user.UserWallets.FirstOrDefault();
|
||||||
|
if (wallet == null)
|
||||||
|
{
|
||||||
|
wallet = new UserWallet
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
Balance = 0,
|
||||||
|
NetworkBalance = 0,
|
||||||
|
DiscountBalance = 0
|
||||||
|
};
|
||||||
|
await _context.UserWallets.AddAsync(wallet, cancellationToken);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. شارژ کیف پول عادی
|
||||||
|
wallet.Balance += SystemConstants.DayaLoanAmount;
|
||||||
|
|
||||||
|
var mainLog = new UserWalletChangeLog
|
||||||
|
{
|
||||||
|
WalletId = wallet.Id,
|
||||||
|
CurrentBalance = 0,
|
||||||
|
ChangeValue = SystemConstants.DayaLoanAmount,
|
||||||
|
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||||
|
ChangeNerworkValue = 0,
|
||||||
|
IsIncrease = true,
|
||||||
|
RefrenceId = transaction.Id
|
||||||
|
};
|
||||||
|
await _context.UserWalletChangeLogs.AddAsync(mainLog, cancellationToken);
|
||||||
|
|
||||||
|
// 4. شارژ کیف پول تخفیف (دو برابر)
|
||||||
|
var discountAmount = SystemConstants.DayaLoanAmount * 2;
|
||||||
|
wallet.DiscountBalance += discountAmount;
|
||||||
|
|
||||||
|
var discountLog = new UserWalletChangeLog
|
||||||
|
{
|
||||||
|
WalletId = wallet.Id,
|
||||||
|
CurrentBalance = wallet.Balance,
|
||||||
|
ChangeValue = 0,
|
||||||
|
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||||
|
ChangeNerworkValue = 0,
|
||||||
|
CurrentDiscountBalance = 0,
|
||||||
|
ChangeDiscountValue = discountAmount,
|
||||||
|
IsIncrease = true,
|
||||||
|
RefrenceId = transaction.Id
|
||||||
|
};
|
||||||
|
await _context.UserWalletChangeLogs.AddAsync(discountLog, cancellationToken);
|
||||||
|
|
||||||
|
// 5. بهروزرسانی کاربر
|
||||||
|
user.HasReceivedDayaCredit = true;
|
||||||
|
user.DayaCreditReceivedAt = DateTime.Now;
|
||||||
|
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan;
|
||||||
|
|
||||||
|
// 6. ثبت Order پکیج پایه
|
||||||
|
var goldenPackage = await _context.Packages
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == 4, cancellationToken);
|
||||||
|
|
||||||
|
if (goldenPackage != null)
|
||||||
|
{
|
||||||
|
// 6. ثبت UserPackagePurchase برای پکیج پایه
|
||||||
|
var goldenPackageId =goldenPackage.Id;
|
||||||
|
var packagePurchase = new UserPackagePurchase
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
PackageId = goldenPackageId,
|
||||||
|
PurchaseMethod = PackagePurchaseMethod.DayaLoan,
|
||||||
|
PurchasedAt = DateTime.Now,
|
||||||
|
Amount = SystemConstants.DayaLoanAmount,
|
||||||
|
TransactionId = transaction.Id
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.UserPackagePurchases.AddAsync(packagePurchase, cancellationToken);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Domain Event
|
||||||
|
user.AddDomainEvent(new DayaLoanApprovedEvent(user, transaction, contractNumber));
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 8. ارسال SMS
|
||||||
|
await SendDayaLoanSmsAsync(user);
|
||||||
|
|
||||||
|
return (wallet.Balance, transaction.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ارسال SMS اطلاعرسانی
|
||||||
|
/// </summary>
|
||||||
|
private async Task SendDayaLoanSmsAsync(User user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userName = SmsTemplates.GetUserName(user.FirstName+" "+user.LastName);
|
||||||
|
var message = SmsTemplates.DayaLoanReceived(userName);
|
||||||
|
|
||||||
|
await _smsService.SendAsync(user.Mobile, message);
|
||||||
|
|
||||||
|
// ارسال SMS به ادمین برای اطلاع
|
||||||
|
var adminMessage = $"وام دایا دریافت شد\nکاربر: {user.FirstName} {user.LastName}\nموبایل: {user.Mobile}\nکدملی: {user.NationalCode}";
|
||||||
|
await _smsService.SendAsync("09199877503", adminMessage);
|
||||||
|
|
||||||
|
_logger.LogInformation("Daya loan SMS sent to User {UserId}", user.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to send Daya loan SMS to User {UserId}", user.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckAndProcessDayaLoans;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پاسخ Command استعلام و پردازش وام دایا
|
||||||
|
/// </summary>
|
||||||
|
public class CheckAndProcessDayaLoansResponseDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// نتایج استعلام و پردازش
|
||||||
|
/// </summary>
|
||||||
|
public required List<DayaLoanProcessResult> Results { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد کل استعلام شده
|
||||||
|
/// </summary>
|
||||||
|
public int TotalChecked { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد موفق (دارای قرارداد)
|
||||||
|
/// </summary>
|
||||||
|
public int WithContractCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد پردازش شده (شارژ کیف پول)
|
||||||
|
/// </summary>
|
||||||
|
public int ProcessedCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نتیجه پردازش هر کاربر
|
||||||
|
/// </summary>
|
||||||
|
public class DayaLoanProcessResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// کد ملی
|
||||||
|
/// </summary>
|
||||||
|
public required string NationalCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر
|
||||||
|
/// </summary>
|
||||||
|
public long? UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// وضعیت وام در دایا
|
||||||
|
/// </summary>
|
||||||
|
public DayaLoanStatus Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شماره قرارداد
|
||||||
|
/// </summary>
|
||||||
|
public string? ContractNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیا پردازش مالی انجام شد؟
|
||||||
|
/// </summary>
|
||||||
|
public bool WasProcessed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// موجودی کیف پول بعد از شارژ
|
||||||
|
/// </summary>
|
||||||
|
public long? NewWalletBalance { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیام
|
||||||
|
/// </summary>
|
||||||
|
public required string Message { get; set; }
|
||||||
|
}
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Enums;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckDayaLoanStatus;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command برای استعلام وضعیت وام از سرویس دایا
|
|
||||||
/// </summary>
|
|
||||||
public record CheckDayaLoanStatusCommand : IRequest<CheckDayaLoanStatusResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// لیست کدهای ملی برای استعلام
|
|
||||||
/// </summary>
|
|
||||||
public List<string> NationalCodes { get; init; }
|
|
||||||
}
|
|
||||||
-239
@@ -1,239 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
using CMSMicroservice.Domain.Enums;
|
|
||||||
using CMSMicroservice.Application.DayaLoanCQ.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckDayaLoanStatus;
|
|
||||||
|
|
||||||
public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanStatusCommand, CheckDayaLoanStatusResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly IDayaLoanApiService _dayaApiService;
|
|
||||||
private readonly ILogger<CheckDayaLoanStatusCommandHandler> _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// مبلغ وام دایا - 56 میلیون ریال
|
|
||||||
/// </summary>
|
|
||||||
private const long DAYA_LOAN_AMOUNT = 56_000_000;
|
|
||||||
|
|
||||||
public CheckDayaLoanStatusCommandHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
IDayaLoanApiService dayaApiService,
|
|
||||||
ILogger<CheckDayaLoanStatusCommandHandler> logger)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_dayaApiService = dayaApiService;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CheckDayaLoanStatusResponseDto> Handle(CheckDayaLoanStatusCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var results = new List<DayaLoanStatusItem>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// فراخوانی سرویس دایا (Mock یا Real)
|
|
||||||
var dayaResults = await _dayaApiService.CheckLoanStatusAsync(request.NationalCodes, cancellationToken);
|
|
||||||
|
|
||||||
foreach (var dayaResult in dayaResults)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
results.Add(new DayaLoanStatusItem
|
|
||||||
{
|
|
||||||
NationalCode = dayaResult.NationalCode,
|
|
||||||
Status = dayaResult.Status,
|
|
||||||
ContractNumber = dayaResult.ContractNumber,
|
|
||||||
Message = "استعلام موفق"
|
|
||||||
});
|
|
||||||
|
|
||||||
// ذخیره یا بهروزرسانی در دیتابیس
|
|
||||||
var existingContract = await _context.DayaLoanContracts
|
|
||||||
.FirstOrDefaultAsync(d => d.NationalCode == dayaResult.NationalCode, cancellationToken);
|
|
||||||
|
|
||||||
if (existingContract != null)
|
|
||||||
{
|
|
||||||
var previousStatus = existingContract.Status;
|
|
||||||
existingContract.LastCheckDate = DateTime.Now;
|
|
||||||
existingContract.Status = dayaResult.Status;
|
|
||||||
existingContract.ContractNumber = dayaResult.ContractNumber;
|
|
||||||
|
|
||||||
// بررسی تغییر وضعیت به PendingReceive یا Received
|
|
||||||
// فقط اگر قبلاً پردازش نشده باشد
|
|
||||||
if (!existingContract.IsProcessed &&
|
|
||||||
!string.IsNullOrEmpty(dayaResult.ContractNumber) &&
|
|
||||||
(dayaResult.Status == DayaLoanStatus.PendingReceive || dayaResult.Status == DayaLoanStatus.Received))
|
|
||||||
{
|
|
||||||
await ProcessDayaLoanReceivedAsync(existingContract.UserId, dayaResult.ContractNumber, cancellationToken);
|
|
||||||
existingContract.IsProcessed = true;
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Daya loan processed for User {UserId}, ContractNumber: {ContractNumber}, Amount: {Amount}",
|
|
||||||
existingContract.UserId, dayaResult.ContractNumber, DAYA_LOAN_AMOUNT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var user = await _context.Users
|
|
||||||
.FirstOrDefaultAsync(u => u.NationalCode == dayaResult.NationalCode, cancellationToken);
|
|
||||||
|
|
||||||
if (user != null)
|
|
||||||
{
|
|
||||||
var isProcessed = false;
|
|
||||||
|
|
||||||
// اگر وضعیت PendingReceive یا Received بود و قرارداد دارد، فوری پردازش کن
|
|
||||||
if (!string.IsNullOrEmpty(dayaResult.ContractNumber) &&
|
|
||||||
(dayaResult.Status == DayaLoanStatus.PendingReceive || dayaResult.Status == DayaLoanStatus.Received))
|
|
||||||
{
|
|
||||||
await ProcessDayaLoanReceivedAsync(user.Id, dayaResult.ContractNumber, cancellationToken);
|
|
||||||
isProcessed = true;
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Daya loan processed for new contract - User {UserId}, ContractNumber: {ContractNumber}, Amount: {Amount}",
|
|
||||||
user.Id, dayaResult.ContractNumber, DAYA_LOAN_AMOUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
var newContract = new DayaLoanContract
|
|
||||||
{
|
|
||||||
UserId = user.Id,
|
|
||||||
NationalCode = dayaResult.NationalCode,
|
|
||||||
Status = dayaResult.Status,
|
|
||||||
ContractNumber = dayaResult.ContractNumber,
|
|
||||||
LastCheckDate = DateTime.Now,
|
|
||||||
IsProcessed = isProcessed
|
|
||||||
};
|
|
||||||
|
|
||||||
await _context.DayaLoanContracts.AddAsync(newContract, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error processing Daya result for {NationalCode}", dayaResult.NationalCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error calling Daya API service");
|
|
||||||
|
|
||||||
// در صورت خطا، نتایج خالی برمیگردانیم
|
|
||||||
foreach (var nationalCode in request.NationalCodes)
|
|
||||||
{
|
|
||||||
results.Add(new DayaLoanStatusItem
|
|
||||||
{
|
|
||||||
NationalCode = nationalCode,
|
|
||||||
Status = DayaLoanStatus.PendingReceive,
|
|
||||||
ContractNumber = null,
|
|
||||||
Message = $"خطا در استعلام: {ex.Message}"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CheckDayaLoanStatusResponseDto
|
|
||||||
{
|
|
||||||
Results = results,
|
|
||||||
TotalChecked = request.NationalCodes.Count,
|
|
||||||
SuccessCount = results.Count(r => r.ContractNumber != null)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// پردازش دریافت وام دایا برای کاربر
|
|
||||||
/// 1. شارژ کیف پول (Balance + DiscountBalance)
|
|
||||||
/// 2. ثبت تراکنش
|
|
||||||
/// 3. ثبت لاگ کیف پول
|
|
||||||
/// 4. بهروزرسانی وضعیت کاربر
|
|
||||||
/// </summary>
|
|
||||||
private async Task ProcessDayaLoanReceivedAsync(long userId, string contractNumber, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// پیدا کردن کاربر
|
|
||||||
var user = await _context.Users
|
|
||||||
.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("User {UserId} not found for Daya loan processing", userId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// بررسی اینکه قبلاً دریافت نکرده باشد
|
|
||||||
if (user.HasReceivedDayaCredit)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("User {UserId} has already received Daya credit", userId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// پیدا کردن یا ایجاد کیف پول کاربر
|
|
||||||
var wallet = await _context.UserWallets
|
|
||||||
.FirstOrDefaultAsync(w => w.UserId == userId, cancellationToken);
|
|
||||||
|
|
||||||
if (wallet == null)
|
|
||||||
{
|
|
||||||
wallet = new UserWallet
|
|
||||||
{
|
|
||||||
UserId = userId,
|
|
||||||
Balance = 0,
|
|
||||||
NetworkBalance = 0,
|
|
||||||
DiscountBalance = 0
|
|
||||||
};
|
|
||||||
await _context.UserWallets.AddAsync(wallet, cancellationToken);
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. شارژ کیف پول - هم موجودی اصلی و هم موجودی تخفیف
|
|
||||||
var previousBalance = wallet.Balance;
|
|
||||||
var previousDiscountBalance = wallet.DiscountBalance;
|
|
||||||
|
|
||||||
wallet.Balance += DAYA_LOAN_AMOUNT;
|
|
||||||
wallet.DiscountBalance += DAYA_LOAN_AMOUNT;
|
|
||||||
|
|
||||||
// 2. ثبت تراکنش - RefId = شماره قرارداد، Status = 0 (Success)، Type = 2 (DepositExternal1)
|
|
||||||
var transaction = new Transaction
|
|
||||||
{
|
|
||||||
Amount = DAYA_LOAN_AMOUNT,
|
|
||||||
Description = $"شارژ کیف پول از وام دایا - قرارداد {contractNumber}",
|
|
||||||
PaymentStatus = PaymentStatus.Success, // 0
|
|
||||||
PaymentDate = DateTime.Now,
|
|
||||||
RefId = contractNumber,
|
|
||||||
Type = TransactionType.DepositExternal1 // 2
|
|
||||||
};
|
|
||||||
|
|
||||||
await _context.Transactions.AddAsync(transaction, cancellationToken);
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
// 3. ثبت لاگ کیف پول با شناسه تراکنش
|
|
||||||
var walletLog = new UserWalletChangeLog
|
|
||||||
{
|
|
||||||
WalletId = wallet.Id,
|
|
||||||
CurrentBalance = wallet.Balance,
|
|
||||||
ChangeValue = DAYA_LOAN_AMOUNT,
|
|
||||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
|
||||||
ChangeNerworkValue = 0,
|
|
||||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
|
||||||
ChangeDiscountValue = DAYA_LOAN_AMOUNT,
|
|
||||||
IsIncrease = true,
|
|
||||||
RefrenceId = transaction.Id
|
|
||||||
};
|
|
||||||
|
|
||||||
await _context.UserWalletChangeLogs.AddAsync(walletLog, cancellationToken);
|
|
||||||
|
|
||||||
// 4. بهروزرسانی وضعیت کاربر
|
|
||||||
user.HasReceivedDayaCredit = true;
|
|
||||||
user.DayaCreditReceivedAt = DateTime.Now;
|
|
||||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan; // 1
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Daya loan fully processed for User {UserId}: " +
|
|
||||||
"Wallet Balance {PreviousBalance} -> {NewBalance}, " +
|
|
||||||
"DiscountBalance {PreviousDiscountBalance} -> {NewDiscountBalance}, " +
|
|
||||||
"Transaction Id: {TransactionId}",
|
|
||||||
userId, previousBalance, wallet.Balance,
|
|
||||||
previousDiscountBalance, wallet.DiscountBalance,
|
|
||||||
transaction.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Enums;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckDayaLoanStatus;
|
|
||||||
|
|
||||||
public class CheckDayaLoanStatusResponseDto
|
|
||||||
{
|
|
||||||
public List<DayaLoanStatusItem> Results { get; set; }
|
|
||||||
public int TotalChecked { get; set; }
|
|
||||||
public int SuccessCount { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DayaLoanStatusItem
|
|
||||||
{
|
|
||||||
public string NationalCode { get; set; }
|
|
||||||
public DayaLoanStatus Status { get; set; }
|
|
||||||
public string? ContractNumber { get; set; }
|
|
||||||
public string Message { get; set; }
|
|
||||||
}
|
|
||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Enums;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command برای پردازش تایید وام دایا و شارژ کیف پول
|
|
||||||
/// </summary>
|
|
||||||
public record ProcessDayaLoanApprovalCommand : IRequest<ProcessDayaLoanApprovalResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// شناسه کاربر
|
|
||||||
/// </summary>
|
|
||||||
public long UserId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// شماره قرارداد دایا
|
|
||||||
/// </summary>
|
|
||||||
public string ContractNumber { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// مبلغ کیف پول عادی (56 میلیون)
|
|
||||||
/// </summary>
|
|
||||||
public long WalletAmount { get; init; } = 56_000_000;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// مبلغ کیف پول قفل شده (56 میلیون)
|
|
||||||
/// </summary>
|
|
||||||
public long LockedWalletAmount { get; init; } = 56_000_000;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// مبلغ کیف پول تخفیف (56 میلیون)
|
|
||||||
/// </summary>
|
|
||||||
public long DiscountWalletAmount { get; init; } = 56_000_000;
|
|
||||||
}
|
|
||||||
-169
@@ -1,169 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
using CMSMicroservice.Domain.Enums;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
|
|
||||||
|
|
||||||
public class ProcessDayaLoanApprovalCommandHandler : IRequestHandler<ProcessDayaLoanApprovalCommand, ProcessDayaLoanApprovalResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public ProcessDayaLoanApprovalCommandHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ProcessDayaLoanApprovalResponseDto> Handle(ProcessDayaLoanApprovalCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// پیدا کردن کاربر
|
|
||||||
var user = await _context.Users
|
|
||||||
.Include(u => u.UserWallets)
|
|
||||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException(nameof(User), request.UserId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// چک کردن که قبلاً دریافت نکرده باشد
|
|
||||||
if (user.HasReceivedDayaCredit)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"کاربر {request.UserId} قبلاً اعتبار دایا را دریافت کرده است");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ایجاد تراکنش با RefId = شماره قرارداد دایا
|
|
||||||
var transaction = new Transaction
|
|
||||||
{
|
|
||||||
Amount = request.WalletAmount + request.LockedWalletAmount + request.DiscountWalletAmount, // 168 میلیون
|
|
||||||
Description = $"دریافت اعتبار دایا - قرارداد {request.ContractNumber}",
|
|
||||||
PaymentStatus = PaymentStatus.Success,
|
|
||||||
PaymentDate = DateTime.Now,
|
|
||||||
RefId = request.ContractNumber, // شماره قرارداد دایا
|
|
||||||
Type = TransactionType.DepositExternal1
|
|
||||||
};
|
|
||||||
|
|
||||||
await _context.Transactions.AddAsync(transaction, cancellationToken);
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
// یافتن یا ایجاد کیف پول کاربر
|
|
||||||
var wallet = user.UserWallets.FirstOrDefault();
|
|
||||||
if (wallet == null)
|
|
||||||
{
|
|
||||||
wallet = new UserWallet
|
|
||||||
{
|
|
||||||
UserId = request.UserId,
|
|
||||||
Balance = 0,
|
|
||||||
NetworkBalance = 0,
|
|
||||||
DiscountBalance = 0
|
|
||||||
};
|
|
||||||
await _context.UserWallets.AddAsync(wallet, cancellationToken);
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// شارژ کیف پول عادی (56 میلیون)
|
|
||||||
var balanceBeforeMain = wallet.Balance;
|
|
||||||
wallet.Balance += request.WalletAmount;
|
|
||||||
|
|
||||||
// لاگ کیف پول عادی
|
|
||||||
var mainLog = new UserWalletChangeLog
|
|
||||||
{
|
|
||||||
WalletId = wallet.Id,
|
|
||||||
CurrentBalance = wallet.Balance,
|
|
||||||
ChangeValue = request.WalletAmount,
|
|
||||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
|
||||||
ChangeNerworkValue = 0,
|
|
||||||
IsIncrease = true,
|
|
||||||
RefrenceId = transaction.Id
|
|
||||||
};
|
|
||||||
await _context.UserWalletChangeLogs.AddAsync(mainLog, cancellationToken);
|
|
||||||
|
|
||||||
// شارژ کیف پول شبکه/کارمزد (56 میلیون) - نامگذاری قدیم: کیف پول قفل شده
|
|
||||||
var balanceBeforeLocked = wallet.NetworkBalance;
|
|
||||||
wallet.NetworkBalance += request.LockedWalletAmount;
|
|
||||||
|
|
||||||
// لاگ کیف پول شبکه
|
|
||||||
var networkLog = new UserWalletChangeLog
|
|
||||||
{
|
|
||||||
WalletId = wallet.Id,
|
|
||||||
CurrentBalance = wallet.Balance,
|
|
||||||
ChangeValue = 0,
|
|
||||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
|
||||||
ChangeNerworkValue = request.LockedWalletAmount,
|
|
||||||
IsIncrease = true,
|
|
||||||
RefrenceId = transaction.Id
|
|
||||||
};
|
|
||||||
await _context.UserWalletChangeLogs.AddAsync(networkLog, cancellationToken);
|
|
||||||
|
|
||||||
// شارژ کیف پول تخفیف (56 میلیون)
|
|
||||||
var balanceBeforeDiscount = wallet.DiscountBalance;
|
|
||||||
wallet.DiscountBalance += request.DiscountWalletAmount;
|
|
||||||
|
|
||||||
// لاگ کیف پول تخفیف
|
|
||||||
var discountLog = new UserWalletChangeLog
|
|
||||||
{
|
|
||||||
WalletId = wallet.Id,
|
|
||||||
CurrentBalance = wallet.Balance,
|
|
||||||
ChangeValue = 0,
|
|
||||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
|
||||||
ChangeNerworkValue = 0,
|
|
||||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
|
||||||
ChangeDiscountValue = request.DiscountWalletAmount,
|
|
||||||
IsIncrease = true,
|
|
||||||
RefrenceId = transaction.Id
|
|
||||||
};
|
|
||||||
await _context.UserWalletChangeLogs.AddAsync(discountLog, cancellationToken);
|
|
||||||
|
|
||||||
// بهروزرسانی وضعیت کاربر
|
|
||||||
user.HasReceivedDayaCredit = true;
|
|
||||||
user.DayaCreditReceivedAt = DateTime.Now;
|
|
||||||
|
|
||||||
// تنظیم نحوه خرید پکیج به DayaLoan
|
|
||||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan;
|
|
||||||
|
|
||||||
// ثبت سفارش پکیج (فعلاً پکیج طلایی)
|
|
||||||
var goldenPackage = await _context.Packages
|
|
||||||
.FirstOrDefaultAsync(p => p.Title.Contains("طلایی") || p.Title.Contains("Golden"), cancellationToken);
|
|
||||||
|
|
||||||
if (goldenPackage != null)
|
|
||||||
{
|
|
||||||
// پیدا کردن آدرس پیشفرض کاربر
|
|
||||||
var defaultAddress = await _context.UserAddresses
|
|
||||||
.Where(a => a.UserId == request.UserId)
|
|
||||||
.OrderByDescending(a => a.Created)
|
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (defaultAddress != null)
|
|
||||||
{
|
|
||||||
var packageOrder = new UserOrder
|
|
||||||
{
|
|
||||||
UserId = request.UserId,
|
|
||||||
PackageId = goldenPackage.Id,
|
|
||||||
Amount = request.WalletAmount, // 56 میلیون
|
|
||||||
PaymentStatus = PaymentStatus.Success,
|
|
||||||
PaymentDate = DateTime.Now,
|
|
||||||
DeliveryStatus = DeliveryStatus.None,
|
|
||||||
UserAddressId = defaultAddress.Id,
|
|
||||||
TransactionId = transaction.Id,
|
|
||||||
PaymentMethod = PaymentMethod.IPG
|
|
||||||
};
|
|
||||||
|
|
||||||
await _context.UserOrders.AddAsync(packageOrder, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ثبت Event
|
|
||||||
user.AddDomainEvent(new DayaLoanApprovedEvent(user, transaction, request.ContractNumber));
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new ProcessDayaLoanApprovalResponseDto
|
|
||||||
{
|
|
||||||
UserId = user.Id,
|
|
||||||
TransactionId = transaction.Id,
|
|
||||||
ContractNumber = request.ContractNumber,
|
|
||||||
MainWalletBalance = wallet.Balance,
|
|
||||||
LockedWalletBalance = wallet.NetworkBalance,
|
|
||||||
DiscountWalletBalance = wallet.DiscountBalance,
|
|
||||||
Message = "اعتبار دایا با موفقیت دریافت شد"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
|
|
||||||
|
|
||||||
public class ProcessDayaLoanApprovalCommandValidator : AbstractValidator<ProcessDayaLoanApprovalCommand>
|
|
||||||
{
|
|
||||||
public ProcessDayaLoanApprovalCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(v => v.UserId)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
|
|
||||||
|
|
||||||
RuleFor(v => v.ContractNumber)
|
|
||||||
.NotEmpty()
|
|
||||||
.WithMessage("شماره قرارداد الزامی است")
|
|
||||||
.MaximumLength(100)
|
|
||||||
.WithMessage("شماره قرارداد نباید بیش از 100 کاراکتر باشد");
|
|
||||||
|
|
||||||
RuleFor(v => v.WalletAmount)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("مبلغ کیف پول باید بزرگتر از صفر باشد");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
|
|
||||||
|
|
||||||
public class ProcessDayaLoanApprovalResponseDto
|
|
||||||
{
|
|
||||||
public long UserId { get; set; }
|
|
||||||
public long TransactionId { get; set; }
|
|
||||||
public string ContractNumber { get; set; }
|
|
||||||
public long MainWalletBalance { get; set; }
|
|
||||||
public long LockedWalletBalance { get; set; }
|
|
||||||
public long DiscountWalletBalance { get; set; }
|
|
||||||
public string Message { get; set; }
|
|
||||||
}
|
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.AddDiscountProductImage;
|
||||||
|
|
||||||
|
public class AddDiscountProductImageCommand : IRequest<long>
|
||||||
|
{
|
||||||
|
public long DiscountProductId { get; set; }
|
||||||
|
public string ImagePath { get; set; } = string.Empty;
|
||||||
|
public string ThumbnailPath { get; set; } = string.Empty;
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? AltText { get; set; }
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.AddDiscountProductImage;
|
||||||
|
|
||||||
|
public class AddDiscountProductImageCommandHandler : IRequestHandler<AddDiscountProductImageCommand, long>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public AddDiscountProductImageCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> Handle(AddDiscountProductImageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Verify product exists
|
||||||
|
var productExists = await _context.DiscountProducts
|
||||||
|
.AnyAsync(p => p.Id == request.DiscountProductId, cancellationToken);
|
||||||
|
|
||||||
|
if (!productExists)
|
||||||
|
throw new InvalidOperationException($"DiscountProduct with Id {request.DiscountProductId} not found.");
|
||||||
|
|
||||||
|
// Get the max sort order for this product
|
||||||
|
var maxSortOrder = await _context.DiscountProductImages
|
||||||
|
.Where(i => i.DiscountProductId == request.DiscountProductId)
|
||||||
|
.MaxAsync(i => (int?)i.SortOrder, cancellationToken) ?? 0;
|
||||||
|
|
||||||
|
var image = new DiscountProductImage
|
||||||
|
{
|
||||||
|
DiscountProductId = request.DiscountProductId,
|
||||||
|
ImagePath = request.ImagePath,
|
||||||
|
ThumbnailPath = request.ThumbnailPath,
|
||||||
|
Title = request.Title,
|
||||||
|
AltText = request.AltText,
|
||||||
|
SortOrder = maxSortOrder + 1,
|
||||||
|
IsActive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.DiscountProductImages.Add(image);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return image.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.AddToCustomerCart;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command برای افزودن محصول به سبد خرید کاربر فعلی
|
||||||
|
/// </summary>
|
||||||
|
public class AddToCustomerCartCommand : IRequest<AddToCustomerCartCommandResponse>
|
||||||
|
{
|
||||||
|
public long ProductId { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
// UserId from ICurrentUserService
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AddToCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public bool Success { get; set; }
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.AddToCustomerCart;
|
||||||
|
|
||||||
|
public class AddToCustomerCartCommandHandler : IRequestHandler<AddToCustomerCartCommand, AddToCustomerCartCommandResponse>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
|
public AddToCustomerCartCommandHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AddToCustomerCartCommandResponse> Handle(AddToCustomerCartCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Extract UserId from JWT token
|
||||||
|
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
|
||||||
|
if (userId == 0)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if product exists and is not deleted
|
||||||
|
var product = await _context.Products
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == request.ProductId && !p.IsDeleted, cancellationToken);
|
||||||
|
|
||||||
|
if (product == null)
|
||||||
|
{
|
||||||
|
return new AddToCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "محصول یافت نشد یا حذف شده است"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if item already exists in cart
|
||||||
|
var existingCartItem = await _context.UserCarts
|
||||||
|
.FirstOrDefaultAsync(uc => uc.UserId == userId && uc.ProductId == request.ProductId, cancellationToken);
|
||||||
|
|
||||||
|
if (existingCartItem != null)
|
||||||
|
{
|
||||||
|
// Update count
|
||||||
|
existingCartItem.Count += request.Count;
|
||||||
|
_context.UserCarts.Update(existingCartItem);
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new AddToCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
Id = existingCartItem.Id,
|
||||||
|
Success = true,
|
||||||
|
Message = "تعداد محصول در سبد خرید بهروزرسانی شد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new cart item
|
||||||
|
var cartItem = new UserCart
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
ProductId = request.ProductId,
|
||||||
|
Count = request.Count,
|
||||||
|
Created = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.UserCarts.Add(cartItem);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new AddToCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
Id = cartItem.Id,
|
||||||
|
Success = true,
|
||||||
|
Message = "محصول به سبد خرید اضافه شد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-6
@@ -8,10 +8,14 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayme
|
|||||||
public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderPaymentCommand, CompleteOrderPaymentResponseDto>
|
public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderPaymentCommand, CompleteOrderPaymentResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IInventoryService _inventoryService;
|
||||||
|
|
||||||
public CompleteOrderPaymentCommandHandler(IApplicationDbContext context)
|
public CompleteOrderPaymentCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IInventoryService inventoryService)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_inventoryService = inventoryService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CompleteOrderPaymentResponseDto> Handle(CompleteOrderPaymentCommand request, CancellationToken cancellationToken)
|
public async Task<CompleteOrderPaymentResponseDto> Handle(CompleteOrderPaymentCommand request, CancellationToken cancellationToken)
|
||||||
@@ -63,12 +67,18 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
|
|||||||
userWallet.DiscountBalance -= order.DiscountBalanceUsed;
|
userWallet.DiscountBalance -= order.DiscountBalanceUsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update product stock and sale count
|
// تایید فروش و کسر موجودی از طریق InventoryService
|
||||||
foreach (var orderDetail in order.OrderDetails)
|
foreach (var orderDetail in order.OrderDetails)
|
||||||
{
|
{
|
||||||
var product = orderDetail.Product;
|
await _inventoryService.ConfirmSaleAsync(
|
||||||
product.RemainingCount -= orderDetail.Count;
|
orderDetail.ProductId,
|
||||||
product.SaleCount += orderDetail.Count;
|
ProductType.DiscountProduct,
|
||||||
|
orderDetail.Count,
|
||||||
|
order.Id,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
// افزایش تعداد فروش
|
||||||
|
orderDetail.Product.SaleCount += orderDetail.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
@@ -82,7 +92,17 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Payment failed
|
// Payment failed - آزادسازی رزرو
|
||||||
|
foreach (var orderDetail in order.OrderDetails)
|
||||||
|
{
|
||||||
|
await _inventoryService.ReleaseReservationAsync(
|
||||||
|
orderDetail.ProductId,
|
||||||
|
ProductType.DiscountProduct,
|
||||||
|
orderDetail.Count,
|
||||||
|
order.Id,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
transaction.PaymentStatus = PaymentStatus.Reject;
|
transaction.PaymentStatus = PaymentStatus.Reject;
|
||||||
order.PaymentStatus = PaymentStatus.Reject;
|
order.PaymentStatus = PaymentStatus.Reject;
|
||||||
|
|
||||||
|
|||||||
-1
@@ -11,6 +11,5 @@ public class CreateDiscountProductCommand : IRequest<long>
|
|||||||
public int MaxDiscountPercent { get; set; }
|
public int MaxDiscountPercent { get; set; }
|
||||||
public string ImagePath { get; set; }
|
public string ImagePath { get; set; }
|
||||||
public string ThumbnailPath { get; set; }
|
public string ThumbnailPath { get; set; }
|
||||||
public int RemainingCount { get; set; }
|
|
||||||
public List<long> CategoryIds { get; set; } = new();
|
public List<long> CategoryIds { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-2
@@ -1,5 +1,6 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -8,10 +9,14 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.CreateDiscountProd
|
|||||||
public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscountProductCommand, long>
|
public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscountProductCommand, long>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IInventoryService _inventoryService;
|
||||||
|
|
||||||
public CreateDiscountProductCommandHandler(IApplicationDbContext context)
|
public CreateDiscountProductCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IInventoryService inventoryService)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_inventoryService = inventoryService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<long> Handle(CreateDiscountProductCommand request, CancellationToken cancellationToken)
|
public async Task<long> Handle(CreateDiscountProductCommand request, CancellationToken cancellationToken)
|
||||||
@@ -25,7 +30,7 @@ public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscoun
|
|||||||
MaxDiscountPercent = request.MaxDiscountPercent,
|
MaxDiscountPercent = request.MaxDiscountPercent,
|
||||||
ImagePath = request.ImagePath,
|
ImagePath = request.ImagePath,
|
||||||
ThumbnailPath = request.ThumbnailPath,
|
ThumbnailPath = request.ThumbnailPath,
|
||||||
RemainingCount = request.RemainingCount,
|
RemainingCount = 0, // موجودی اولیه صفر - باید از طریق Inventory اضافه شود
|
||||||
Rate = 0,
|
Rate = 0,
|
||||||
SaleCount = 0,
|
SaleCount = 0,
|
||||||
ViewCount = 0,
|
ViewCount = 0,
|
||||||
@@ -35,6 +40,13 @@ public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscoun
|
|||||||
_context.DiscountProducts.Add(product);
|
_context.DiscountProducts.Add(product);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// ایجاد رکورد موجودی در سیستم انبارداری با موجودی اولیه صفر
|
||||||
|
await _inventoryService.InitializeInventoryAsync(
|
||||||
|
product.Id,
|
||||||
|
ProductType.DiscountProduct,
|
||||||
|
0, // موجودی اولیه صفر - باید از طریق Inventory اضافه شود
|
||||||
|
ct: cancellationToken);
|
||||||
|
|
||||||
// Add product categories
|
// Add product categories
|
||||||
if (request.CategoryIds.Any())
|
if (request.CategoryIds.Any())
|
||||||
{
|
{
|
||||||
|
|||||||
+4
-13
@@ -11,26 +11,17 @@ public class CreateDiscountProductCommandValidator : AbstractValidator<CreateDis
|
|||||||
.MaximumLength(200).WithMessage("عنوان محصول نمیتواند بیشتر از 200 کاراکتر باشد");
|
.MaximumLength(200).WithMessage("عنوان محصول نمیتواند بیشتر از 200 کاراکتر باشد");
|
||||||
|
|
||||||
RuleFor(v => v.ShortInfomation)
|
RuleFor(v => v.ShortInfomation)
|
||||||
.NotEmpty().WithMessage("توضیحات کوتاه الزامی است")
|
.MaximumLength(500).WithMessage("توضیحات کوتاه نمیتواند بیشتر از 500 کاراکتر باشد")
|
||||||
.MaximumLength(500).WithMessage("توضیحات کوتاه نمیتواند بیشتر از 500 کاراکتر باشد");
|
.When(v => !string.IsNullOrEmpty(v.ShortInfomation));
|
||||||
|
|
||||||
RuleFor(v => v.FullInformation)
|
RuleFor(v => v.FullInformation)
|
||||||
.NotEmpty().WithMessage("توضیحات کامل الزامی است")
|
.MaximumLength(10000).WithMessage("توضیحات کامل نمیتواند بیشتر از 10000 کاراکتر باشد")
|
||||||
.MaximumLength(2000).WithMessage("توضیحات کامل نمیتواند بیشتر از 2000 کاراکتر باشد");
|
.When(v => !string.IsNullOrEmpty(v.FullInformation));
|
||||||
|
|
||||||
RuleFor(v => v.Price)
|
RuleFor(v => v.Price)
|
||||||
.GreaterThan(0).WithMessage("قیمت باید بیشتر از صفر باشد");
|
.GreaterThan(0).WithMessage("قیمت باید بیشتر از صفر باشد");
|
||||||
|
|
||||||
RuleFor(v => v.MaxDiscountPercent)
|
RuleFor(v => v.MaxDiscountPercent)
|
||||||
.InclusiveBetween(0, 100).WithMessage("درصد تخفیف باید بین 0 تا 100 باشد");
|
.InclusiveBetween(0, 100).WithMessage("درصد تخفیف باید بین 0 تا 100 باشد");
|
||||||
|
|
||||||
RuleFor(v => v.RemainingCount)
|
|
||||||
.GreaterThanOrEqualTo(0).WithMessage("موجودی نمیتواند منفی باشد");
|
|
||||||
|
|
||||||
RuleFor(v => v.ImagePath)
|
|
||||||
.NotEmpty().WithMessage("تصویر محصول الزامی است");
|
|
||||||
|
|
||||||
RuleFor(v => v.ThumbnailPath)
|
|
||||||
.NotEmpty().WithMessage("تصویر بندانگشتی الزامی است");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.DeleteDiscountProductImage;
|
||||||
|
|
||||||
|
public class DeleteDiscountProductImageCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.DeleteDiscountProductImage;
|
||||||
|
|
||||||
|
public class DeleteDiscountProductImageCommandHandler : IRequestHandler<DeleteDiscountProductImageCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public DeleteDiscountProductImageCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Handle(DeleteDiscountProductImageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var image = await _context.DiscountProductImages
|
||||||
|
.FirstOrDefaultAsync(i => i.Id == request.Id, cancellationToken);
|
||||||
|
|
||||||
|
if (image == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_context.DiscountProductImages.Remove(image);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Reorder remaining images for this product
|
||||||
|
var remainingImages = await _context.DiscountProductImages
|
||||||
|
.Where(i => i.DiscountProductId == image.DiscountProductId)
|
||||||
|
.OrderBy(i => i.SortOrder)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
for (int i = 0; i < remainingImages.Count; i++)
|
||||||
|
{
|
||||||
|
remainingImages[i].SortOrder = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-4
@@ -1,4 +1,5 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Services;
|
||||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||||
using CMSMicroservice.Domain.Entities.Payment;
|
using CMSMicroservice.Domain.Entities.Payment;
|
||||||
using CMSMicroservice.Domain.Enums;
|
using CMSMicroservice.Domain.Enums;
|
||||||
@@ -10,10 +11,14 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.PlaceOrder;
|
|||||||
public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, PlaceOrderResponseDto>
|
public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, PlaceOrderResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IInventoryService _inventoryService;
|
||||||
|
|
||||||
public PlaceOrderCommandHandler(IApplicationDbContext context)
|
public PlaceOrderCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IInventoryService inventoryService)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_inventoryService = inventoryService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PlaceOrderResponseDto> Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
|
public async Task<PlaceOrderResponseDto> Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
|
||||||
@@ -109,9 +114,10 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
|||||||
|
|
||||||
var gatewayAmountRequired = totalAmount - actualDiscountBalanceUsed;
|
var gatewayAmountRequired = totalAmount - actualDiscountBalanceUsed;
|
||||||
|
|
||||||
// Calculate VAT (9%)
|
// Calculate VAT using centralized calculator
|
||||||
var vatAmount = (gatewayAmountRequired * 9) / 100;
|
var vatBreakdown = VatCalculator.CalculateBreakdown(gatewayAmountRequired);
|
||||||
var finalGatewayAmount = gatewayAmountRequired + vatAmount;
|
var vatAmount = vatBreakdown.VatAmount;
|
||||||
|
var finalGatewayAmount = vatBreakdown.GrossAmount;
|
||||||
|
|
||||||
// Create transaction for gateway payment
|
// Create transaction for gateway payment
|
||||||
var transaction = new Transaction
|
var transaction = new Transaction
|
||||||
@@ -150,6 +156,17 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
|||||||
|
|
||||||
_context.DiscountOrderDetails.AddRange(orderDetails);
|
_context.DiscountOrderDetails.AddRange(orderDetails);
|
||||||
|
|
||||||
|
// رزرو موجودی برای سفارش pending
|
||||||
|
foreach (var cartItem in cartItems)
|
||||||
|
{
|
||||||
|
await _inventoryService.ReserveStockAsync(
|
||||||
|
cartItem.ProductId,
|
||||||
|
ProductType.DiscountProduct,
|
||||||
|
cartItem.Count,
|
||||||
|
order.Id,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear cart
|
// Clear cart
|
||||||
_context.DiscountShoppingCarts.RemoveRange(cartItems);
|
_context.DiscountShoppingCarts.RemoveRange(cartItems);
|
||||||
|
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.RemoveFromCustomerCart;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command برای حذف محصول از سبد خرید
|
||||||
|
/// </summary>
|
||||||
|
public class RemoveFromCustomerCartCommand : IRequest<RemoveFromCustomerCartCommandResponse>
|
||||||
|
{
|
||||||
|
public long CartItemId { get; set; }
|
||||||
|
// UserId from ICurrentUserService
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RemoveFromCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public bool Success { get; set; }
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.RemoveFromCustomerCart;
|
||||||
|
|
||||||
|
public class RemoveFromCustomerCartCommandHandler : IRequestHandler<RemoveFromCustomerCartCommand, RemoveFromCustomerCartCommandResponse>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
|
public RemoveFromCustomerCartCommandHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RemoveFromCustomerCartCommandResponse> Handle(RemoveFromCustomerCartCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Extract UserId from JWT token
|
||||||
|
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
|
||||||
|
if (userId == 0)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and remove cart item
|
||||||
|
var cartItem = await _context.UserCarts
|
||||||
|
.FirstOrDefaultAsync(uc => uc.Id == request.CartItemId && uc.UserId == userId, cancellationToken);
|
||||||
|
|
||||||
|
if (cartItem == null)
|
||||||
|
{
|
||||||
|
return new RemoveFromCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "آیتم سبد خرید یافت نشد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.UserCarts.Remove(cartItem);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new RemoveFromCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "آیتم از سبد خرید حذف شد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.ReorderDiscountProductImages;
|
||||||
|
|
||||||
|
public class ReorderDiscountProductImagesCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
public long DiscountProductId { get; set; }
|
||||||
|
public List<long> ImageIds { get; set; } = new();
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.ReorderDiscountProductImages;
|
||||||
|
|
||||||
|
public class ReorderDiscountProductImagesCommandHandler : IRequestHandler<ReorderDiscountProductImagesCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public ReorderDiscountProductImagesCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Handle(ReorderDiscountProductImagesCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var images = await _context.DiscountProductImages
|
||||||
|
.Where(i => i.DiscountProductId == request.DiscountProductId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!images.Any())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Validate all image IDs belong to this product
|
||||||
|
var imageIdSet = images.Select(i => i.Id).ToHashSet();
|
||||||
|
if (!request.ImageIds.All(id => imageIdSet.Contains(id)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Update sort order based on the new order
|
||||||
|
for (int i = 0; i < request.ImageIds.Count; i++)
|
||||||
|
{
|
||||||
|
var image = images.FirstOrDefault(img => img.Id == request.ImageIds[i]);
|
||||||
|
if (image != null)
|
||||||
|
{
|
||||||
|
image.SortOrder = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateCustomerCartItem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command برای بهروزرسانی تعداد محصول در سبد خرید
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateCustomerCartItemCommand : IRequest<UpdateCustomerCartItemCommandResponse>
|
||||||
|
{
|
||||||
|
public long CartItemId { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
// UserId from ICurrentUserService
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateCustomerCartItemCommandResponse
|
||||||
|
{
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public bool Success { get; set; }
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateCustomerCartItem;
|
||||||
|
|
||||||
|
public class UpdateCustomerCartItemCommandHandler : IRequestHandler<UpdateCustomerCartItemCommand, UpdateCustomerCartItemCommandResponse>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
|
public UpdateCustomerCartItemCommandHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UpdateCustomerCartItemCommandResponse> Handle(UpdateCustomerCartItemCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Extract UserId from JWT token
|
||||||
|
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
|
||||||
|
if (userId == 0)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find cart item
|
||||||
|
var cartItem = await _context.UserCarts
|
||||||
|
.FirstOrDefaultAsync(uc => uc.Id == request.CartItemId && uc.UserId == userId, cancellationToken);
|
||||||
|
|
||||||
|
if (cartItem == null)
|
||||||
|
{
|
||||||
|
return new UpdateCustomerCartItemCommandResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "آیتم سبد خرید یافت نشد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update count
|
||||||
|
if (request.Count <= 0)
|
||||||
|
{
|
||||||
|
// Remove item if count is 0 or negative
|
||||||
|
_context.UserCarts.Remove(cartItem);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new UpdateCustomerCartItemCommandResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "آیتم از سبد خرید حذف شد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cartItem.Count = request.Count;
|
||||||
|
_context.UserCarts.Update(cartItem);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new UpdateCustomerCartItemCommandResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "تعداد آیتم بهروزرسانی شد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-12
@@ -14,12 +14,12 @@ public class UpdateDiscountProductCommandValidator : AbstractValidator<UpdateDis
|
|||||||
.MaximumLength(200).WithMessage("عنوان محصول نباید بیشتر از 200 کاراکتر باشد");
|
.MaximumLength(200).WithMessage("عنوان محصول نباید بیشتر از 200 کاراکتر باشد");
|
||||||
|
|
||||||
RuleFor(x => x.ShortInfomation)
|
RuleFor(x => x.ShortInfomation)
|
||||||
.NotEmpty().WithMessage("توضیحات کوتاه الزامی است")
|
.MaximumLength(500).WithMessage("توضیحات کوتاه نباید بیشتر از 500 کاراکتر باشد")
|
||||||
.MaximumLength(500).WithMessage("توضیحات کوتاه نباید بیشتر از 500 کاراکتر باشد");
|
.When(x => !string.IsNullOrEmpty(x.ShortInfomation));
|
||||||
|
|
||||||
RuleFor(x => x.FullInformation)
|
RuleFor(x => x.FullInformation)
|
||||||
.NotEmpty().WithMessage("توضیحات کامل الزامی است")
|
.MaximumLength(10000).WithMessage("توضیحات کامل نباید بیشتر از 10000 کاراکتر باشد")
|
||||||
.MaximumLength(5000).WithMessage("توضیحات کامل نباید بیشتر از 5000 کاراکتر باشد");
|
.When(x => !string.IsNullOrEmpty(x.FullInformation));
|
||||||
|
|
||||||
RuleFor(x => x.Price)
|
RuleFor(x => x.Price)
|
||||||
.GreaterThan(0).WithMessage("قیمت باید بزرگتر از صفر باشد");
|
.GreaterThan(0).WithMessage("قیمت باید بزرگتر از صفر باشد");
|
||||||
@@ -27,14 +27,6 @@ public class UpdateDiscountProductCommandValidator : AbstractValidator<UpdateDis
|
|||||||
RuleFor(x => x.MaxDiscountPercent)
|
RuleFor(x => x.MaxDiscountPercent)
|
||||||
.InclusiveBetween(0, 100).WithMessage("درصد تخفیف باید بین 0 تا 100 باشد");
|
.InclusiveBetween(0, 100).WithMessage("درصد تخفیف باید بین 0 تا 100 باشد");
|
||||||
|
|
||||||
RuleFor(x => x.ImagePath)
|
|
||||||
.NotEmpty().WithMessage("مسیر تصویر اصلی الزامی است")
|
|
||||||
.MaximumLength(500).WithMessage("مسیر تصویر نباید بیشتر از 500 کاراکتر باشد");
|
|
||||||
|
|
||||||
RuleFor(x => x.ThumbnailPath)
|
|
||||||
.NotEmpty().WithMessage("مسیر تصویر بندانگشتی الزامی است")
|
|
||||||
.MaximumLength(500).WithMessage("مسیر تصویر نباید بیشتر از 500 کاراکتر باشد");
|
|
||||||
|
|
||||||
RuleFor(x => x.RemainingCount)
|
RuleFor(x => x.RemainingCount)
|
||||||
.GreaterThanOrEqualTo(0).WithMessage("موجودی نمیتواند منفی باشد");
|
.GreaterThanOrEqualTo(0).WithMessage("موجودی نمیتواند منفی باشد");
|
||||||
|
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateDiscountProductImage;
|
||||||
|
|
||||||
|
public class UpdateDiscountProductImageCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string ImagePath { get; set; } = string.Empty;
|
||||||
|
public string ThumbnailPath { get; set; } = string.Empty;
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? AltText { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateDiscountProductImage;
|
||||||
|
|
||||||
|
public class UpdateDiscountProductImageCommandHandler : IRequestHandler<UpdateDiscountProductImageCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public UpdateDiscountProductImageCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Handle(UpdateDiscountProductImageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var image = await _context.DiscountProductImages
|
||||||
|
.FirstOrDefaultAsync(i => i.Id == request.Id, cancellationToken);
|
||||||
|
|
||||||
|
if (image == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
image.ImagePath = request.ImagePath;
|
||||||
|
image.ThumbnailPath = request.ThumbnailPath;
|
||||||
|
image.Title = request.Title;
|
||||||
|
image.AltText = request.AltText;
|
||||||
|
image.IsActive = request.IsActive;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetAllDiscountOrders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// کوئری دریافت همه سفارشات فروشگاه تخفیفی برای ادمین
|
||||||
|
/// </summary>
|
||||||
|
public class GetAllDiscountOrdersQuery : IRequest<GetAllDiscountOrdersResponseDto>
|
||||||
|
{
|
||||||
|
public PaginationState? PaginationQuery { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فیلتر بر اساس شناسه کاربر
|
||||||
|
/// </summary>
|
||||||
|
public long? UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فیلتر بر اساس وضعیت پرداخت
|
||||||
|
/// </summary>
|
||||||
|
public PaymentStatus? PaymentStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فیلتر بر اساس وضعیت ارسال
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryStatus? DeliveryStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// جستجو بر اساس موبایل کاربر
|
||||||
|
/// </summary>
|
||||||
|
public string? UserMobile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// جستجو بر اساس کد رهگیری
|
||||||
|
/// </summary>
|
||||||
|
public string? TrackingCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فیلتر از تاریخ
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? FromDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فیلتر تا تاریخ
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ToDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حداقل مبلغ سفارش
|
||||||
|
/// </summary>
|
||||||
|
public long? MinAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حداکثر مبلغ سفارش
|
||||||
|
/// </summary>
|
||||||
|
public long? MaxAmount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetAllDiscountOrdersResponseDto
|
||||||
|
{
|
||||||
|
public MetaData MetaData { get; set; } = new();
|
||||||
|
public List<AdminOrderDto> Models { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AdminOrderDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
// اطلاعات کاربر
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public string UserFullName { get; set; } = string.Empty;
|
||||||
|
public string UserMobile { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// مبالغ
|
||||||
|
public long TotalAmount { get; set; }
|
||||||
|
public long DiscountBalanceUsed { get; set; }
|
||||||
|
public long GatewayAmountPaid { get; set; }
|
||||||
|
public long VatAmount { get; set; }
|
||||||
|
|
||||||
|
// وضعیت ها
|
||||||
|
public PaymentStatus PaymentStatus { get; set; }
|
||||||
|
public DateTime? PaymentDate { get; set; }
|
||||||
|
public DeliveryStatus DeliveryStatus { get; set; }
|
||||||
|
public DateTime? DeliveryDate { get; set; }
|
||||||
|
|
||||||
|
// آدرس تحویل
|
||||||
|
public string? ShippingAddress { get; set; }
|
||||||
|
public string? ReceiverName { get; set; }
|
||||||
|
public string? ReceiverMobile { get; set; }
|
||||||
|
|
||||||
|
// رهگیری
|
||||||
|
public string? TrackingCode { get; set; }
|
||||||
|
public string? AdminNote { get; set; }
|
||||||
|
|
||||||
|
// تاریخ ها
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
public DateTime? LastModified { get; set; }
|
||||||
|
|
||||||
|
// تعداد آیتم ها
|
||||||
|
public int ItemsCount { get; set; }
|
||||||
|
}
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetAllDiscountOrders;
|
||||||
|
|
||||||
|
public class GetAllDiscountOrdersQueryHandler : IRequestHandler<GetAllDiscountOrdersQuery, GetAllDiscountOrdersResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetAllDiscountOrdersQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetAllDiscountOrdersResponseDto> Handle(GetAllDiscountOrdersQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var query = _context.DiscountOrders
|
||||||
|
.Include(o => o.User)
|
||||||
|
.Include(o => o.UserAddress)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
// فیلتر بر اساس شناسه کاربر
|
||||||
|
if (request.UserId.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(o => o.UserId == request.UserId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلتر بر اساس وضعیت پرداخت
|
||||||
|
if (request.PaymentStatus.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(o => o.PaymentStatus == request.PaymentStatus.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلتر بر اساس وضعیت ارسال
|
||||||
|
if (request.DeliveryStatus.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(o => o.DeliveryStatus == request.DeliveryStatus.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// جستجو بر اساس موبایل کاربر
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.UserMobile))
|
||||||
|
{
|
||||||
|
query = query.Where(o => o.User.Mobile.Contains(request.UserMobile));
|
||||||
|
}
|
||||||
|
|
||||||
|
// جستجو بر اساس کد رهگیری
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.TrackingCode))
|
||||||
|
{
|
||||||
|
query = query.Where(o => o.TrackingCode != null && o.TrackingCode.Contains(request.TrackingCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلتر از تاریخ
|
||||||
|
if (request.FromDate.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(o => o.Created >= request.FromDate.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلتر تا تاریخ
|
||||||
|
if (request.ToDate.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(o => o.Created <= request.ToDate.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلتر حداقل مبلغ
|
||||||
|
if (request.MinAmount.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(o => o.TotalAmount >= request.MinAmount.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلتر حداکثر مبلغ
|
||||||
|
if (request.MaxAmount.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(o => o.TotalAmount <= request.MaxAmount.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
var pagination = request.PaginationQuery ?? new PaginationState { PageNumber = 1, PageSize = 20 };
|
||||||
|
|
||||||
|
var orders = await query
|
||||||
|
.OrderByDescending(o => o.Created)
|
||||||
|
.Skip((pagination.PageNumber - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.Select(o => new AdminOrderDto
|
||||||
|
{
|
||||||
|
Id = o.Id,
|
||||||
|
UserId = o.UserId,
|
||||||
|
UserFullName = (o.User.FirstName ?? "") + " " + (o.User.LastName ?? ""),
|
||||||
|
UserMobile = o.User.Mobile,
|
||||||
|
TotalAmount = o.TotalAmount,
|
||||||
|
DiscountBalanceUsed = o.DiscountBalanceUsed,
|
||||||
|
GatewayAmountPaid = o.GatewayAmountPaid,
|
||||||
|
VatAmount = o.VatAmount,
|
||||||
|
PaymentStatus = o.PaymentStatus,
|
||||||
|
PaymentDate = o.PaymentDate,
|
||||||
|
DeliveryStatus = o.DeliveryStatus,
|
||||||
|
DeliveryDate = null, // TODO: Add DeliveryDate to DiscountOrder if needed
|
||||||
|
ShippingAddress = o.UserAddress.Address,
|
||||||
|
ReceiverName = o.UserAddress.Title,
|
||||||
|
ReceiverMobile = o.User.Mobile,
|
||||||
|
TrackingCode = o.TrackingCode,
|
||||||
|
AdminNote = o.DeliveryDescription,
|
||||||
|
Created = o.Created,
|
||||||
|
LastModified = o.LastModified,
|
||||||
|
ItemsCount = o.OrderDetails.Count
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new GetAllDiscountOrdersResponseDto
|
||||||
|
{
|
||||||
|
MetaData = new MetaData
|
||||||
|
{
|
||||||
|
TotalCount = totalCount,
|
||||||
|
PageSize = pagination.PageSize,
|
||||||
|
CurrentPage = pagination.PageNumber,
|
||||||
|
TotalPage = (int)Math.Ceiling(totalCount / (double)pagination.PageSize)
|
||||||
|
},
|
||||||
|
Models = orders
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetCustomerCart;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query برای دریافت سبد خرید کاربر فعلی
|
||||||
|
/// </summary>
|
||||||
|
public class GetCustomerCartQuery : IRequest<GetCustomerCartQueryResponse>
|
||||||
|
{
|
||||||
|
// UserId from ICurrentUserService
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetCustomerCart;
|
||||||
|
|
||||||
|
public class GetCustomerCartQueryHandler : IRequestHandler<GetCustomerCartQuery, GetCustomerCartQueryResponse>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
|
public GetCustomerCartQueryHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetCustomerCartQueryResponse> Handle(GetCustomerCartQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Extract UserId from JWT token
|
||||||
|
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
|
||||||
|
if (userId == 0)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all cart items for the current user
|
||||||
|
var cartItems = await _context.UserCarts
|
||||||
|
.Include(uc => uc.Product)
|
||||||
|
.Where(uc => uc.UserId == userId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var response = new GetCustomerCartQueryResponse
|
||||||
|
{
|
||||||
|
TotalItemsCount = cartItems.Sum(c => c.Count),
|
||||||
|
Message = cartItems.Count > 0 ? "سبد خرید با موفقیت بازیابی شد" : "سبد خرید خالی است"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var item in cartItems)
|
||||||
|
{
|
||||||
|
// Use Product.ThumbnailPath directly
|
||||||
|
var thumbnailPath = item.Product?.ThumbnailPath ?? string.Empty;
|
||||||
|
var itemPrice = item.Product?.Price ?? 0;
|
||||||
|
var itemDiscount = item.Product?.Discount ?? 0;
|
||||||
|
var finalPrice = itemPrice * (100 - itemDiscount) / 100;
|
||||||
|
var totalItemPrice = finalPrice * item.Count;
|
||||||
|
|
||||||
|
response.Items.Add(new CustomerCartItemModel
|
||||||
|
{
|
||||||
|
Id = item.Id,
|
||||||
|
ProductId = item.ProductId,
|
||||||
|
ProductTitle = item.Product?.Title ?? string.Empty,
|
||||||
|
ProductShortInformation = item.Product?.ShortInfomation ?? string.Empty, // Typo in DB: ShortInfomation
|
||||||
|
ProductPrice = itemPrice,
|
||||||
|
ProductDiscount = itemDiscount,
|
||||||
|
ProductThumbnailPath = thumbnailPath,
|
||||||
|
Count = item.Count,
|
||||||
|
TotalItemPrice = totalItemPrice,
|
||||||
|
Created = item.Created
|
||||||
|
});
|
||||||
|
|
||||||
|
response.TotalPrice += totalItemPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetCustomerCart;
|
||||||
|
|
||||||
|
public class GetCustomerCartQueryResponse
|
||||||
|
{
|
||||||
|
public List<CustomerCartItemModel> Items { get; set; } = new();
|
||||||
|
public long TotalPrice { get; set; }
|
||||||
|
public int TotalItemsCount { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CustomerCartItemModel
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public long ProductId { get; set; }
|
||||||
|
public string ProductTitle { get; set; } = string.Empty;
|
||||||
|
public string ProductShortInformation { get; set; } = string.Empty;
|
||||||
|
public long ProductPrice { get; set; }
|
||||||
|
public int ProductDiscount { get; set; }
|
||||||
|
public string ProductThumbnailPath { get; set; } = string.Empty;
|
||||||
|
public int Count { get; set; }
|
||||||
|
public long TotalItemPrice { get; set; }
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountProductImages;
|
||||||
|
|
||||||
|
public class GetDiscountProductImagesQuery : IRequest<List<DiscountProductImageDto>>
|
||||||
|
{
|
||||||
|
public long DiscountProductId { get; set; }
|
||||||
|
public bool OnlyActive { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DiscountProductImageDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public long DiscountProductId { get; set; }
|
||||||
|
public string ImagePath { get; set; } = string.Empty;
|
||||||
|
public string ThumbnailPath { get; set; } = string.Empty;
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? AltText { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountProductImages;
|
||||||
|
|
||||||
|
public class GetDiscountProductImagesQueryHandler : IRequestHandler<GetDiscountProductImagesQuery, List<DiscountProductImageDto>>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetDiscountProductImagesQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<DiscountProductImageDto>> Handle(GetDiscountProductImagesQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var query = _context.DiscountProductImages
|
||||||
|
.Where(i => i.DiscountProductId == request.DiscountProductId);
|
||||||
|
|
||||||
|
if (request.OnlyActive)
|
||||||
|
query = query.Where(i => i.IsActive);
|
||||||
|
|
||||||
|
return await query
|
||||||
|
.OrderBy(i => i.SortOrder)
|
||||||
|
.Select(i => new DiscountProductImageDto
|
||||||
|
{
|
||||||
|
Id = i.Id,
|
||||||
|
DiscountProductId = i.DiscountProductId,
|
||||||
|
ImagePath = i.ImagePath,
|
||||||
|
ThumbnailPath = i.ThumbnailPath,
|
||||||
|
Title = i.Title,
|
||||||
|
AltText = i.AltText,
|
||||||
|
SortOrder = i.SortOrder,
|
||||||
|
IsActive = i.IsActive
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
+166
@@ -0,0 +1,166 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountSalesReport;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// کوئری گزارش فروش فروشگاه تخفیفی
|
||||||
|
/// </summary>
|
||||||
|
public class GetDiscountSalesReportQuery : IRequest<DiscountSalesReportDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// از تاریخ
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? FromDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تا تاریخ
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ToDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نوع گزارش
|
||||||
|
/// </summary>
|
||||||
|
public SalesReportType ReportType { get; set; } = SalesReportType.Summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SalesReportType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// خلاصه کلی
|
||||||
|
/// </summary>
|
||||||
|
Summary,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// روزانه
|
||||||
|
/// </summary>
|
||||||
|
Daily,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// هفتگی
|
||||||
|
/// </summary>
|
||||||
|
Weekly,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ماهانه
|
||||||
|
/// </summary>
|
||||||
|
Monthly
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DiscountSalesReportDto
|
||||||
|
{
|
||||||
|
// خلاصه کلی
|
||||||
|
public SalesSummary Summary { get; set; } = new();
|
||||||
|
|
||||||
|
// جزئیات زمانی (برای گزارشهای روزانه، هفتگی، ماهانه)
|
||||||
|
public List<SalesPeriodDto> Periods { get; set; } = new();
|
||||||
|
|
||||||
|
// پرفروشترین محصولات
|
||||||
|
public List<TopSellingProductDto> TopProducts { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SalesSummary
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد کل سفارشات
|
||||||
|
/// </summary>
|
||||||
|
public int TotalOrders { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد سفارشات موفق (پرداخت شده)
|
||||||
|
/// </summary>
|
||||||
|
public int CompletedOrders { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد سفارشات در انتظار پرداخت
|
||||||
|
/// </summary>
|
||||||
|
public int PendingOrders { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد سفارشات لغو شده
|
||||||
|
/// </summary>
|
||||||
|
public int CancelledOrders { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// جمع کل فروش (TotalAmount)
|
||||||
|
/// </summary>
|
||||||
|
public long TotalSalesAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// جمع تخفیف استفاده شده (DiscountBalanceUsed)
|
||||||
|
/// </summary>
|
||||||
|
public long TotalDiscountUsed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// جمع پرداخت از درگاه (GatewayAmountPaid)
|
||||||
|
/// </summary>
|
||||||
|
public long TotalGatewayPaid { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// جمع VAT
|
||||||
|
/// </summary>
|
||||||
|
public long TotalVatAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// میانگین ارزش سفارش
|
||||||
|
/// </summary>
|
||||||
|
public long AverageOrderValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد کاربران یکتا
|
||||||
|
/// </summary>
|
||||||
|
public int UniqueCustomers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد کل محصولات فروخته شده
|
||||||
|
/// </summary>
|
||||||
|
public int TotalProductsSold { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SalesPeriodDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// نام دوره (مثل: 1403/10/11 یا هفته 41)
|
||||||
|
/// </summary>
|
||||||
|
public string PeriodLabel { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تاریخ شروع دوره
|
||||||
|
/// </summary>
|
||||||
|
public DateTime PeriodStart { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تاریخ پایان دوره
|
||||||
|
/// </summary>
|
||||||
|
public DateTime PeriodEnd { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد سفارشات
|
||||||
|
/// </summary>
|
||||||
|
public int OrdersCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// جمع فروش
|
||||||
|
/// </summary>
|
||||||
|
public long TotalAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// جمع تخفیف
|
||||||
|
/// </summary>
|
||||||
|
public long DiscountUsed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// جمع پرداخت درگاه
|
||||||
|
/// </summary>
|
||||||
|
public long GatewayPaid { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TopSellingProductDto
|
||||||
|
{
|
||||||
|
public long ProductId { get; set; }
|
||||||
|
public string ProductTitle { get; set; } = string.Empty;
|
||||||
|
public string? ImagePath { get; set; }
|
||||||
|
public int QuantitySold { get; set; }
|
||||||
|
public long TotalRevenue { get; set; }
|
||||||
|
public int OrdersCount { get; set; }
|
||||||
|
}
|
||||||
+193
@@ -0,0 +1,193 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountSalesReport;
|
||||||
|
|
||||||
|
public class GetDiscountSalesReportQueryHandler : IRequestHandler<GetDiscountSalesReportQuery, DiscountSalesReportDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private static readonly PersianCalendar PersianCalendar = new();
|
||||||
|
|
||||||
|
public GetDiscountSalesReportQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DiscountSalesReportDto> Handle(GetDiscountSalesReportQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = new DiscountSalesReportDto();
|
||||||
|
|
||||||
|
// Base query for orders
|
||||||
|
var ordersQuery = _context.DiscountOrders.AsQueryable();
|
||||||
|
|
||||||
|
// Apply date filters
|
||||||
|
if (request.FromDate.HasValue)
|
||||||
|
{
|
||||||
|
ordersQuery = ordersQuery.Where(o => o.Created >= request.FromDate.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.ToDate.HasValue)
|
||||||
|
{
|
||||||
|
ordersQuery = ordersQuery.Where(o => o.Created <= request.ToDate.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all matching orders for summary
|
||||||
|
var orders = await ordersQuery.ToListAsync(cancellationToken);
|
||||||
|
var completedOrders = orders.Where(o => o.PaymentStatus == PaymentStatus.Success).ToList();
|
||||||
|
|
||||||
|
// Calculate summary
|
||||||
|
result.Summary = new SalesSummary
|
||||||
|
{
|
||||||
|
TotalOrders = orders.Count,
|
||||||
|
CompletedOrders = completedOrders.Count,
|
||||||
|
PendingOrders = orders.Count(o => o.PaymentStatus == PaymentStatus.Pending),
|
||||||
|
CancelledOrders = orders.Count(o => o.PaymentStatus == PaymentStatus.Reject),
|
||||||
|
TotalSalesAmount = completedOrders.Sum(o => o.TotalAmount),
|
||||||
|
TotalDiscountUsed = completedOrders.Sum(o => o.DiscountBalanceUsed),
|
||||||
|
TotalGatewayPaid = completedOrders.Sum(o => o.GatewayAmountPaid),
|
||||||
|
TotalVatAmount = completedOrders.Sum(o => o.VatAmount),
|
||||||
|
AverageOrderValue = completedOrders.Any() ? (long)completedOrders.Average(o => o.TotalAmount) : 0,
|
||||||
|
UniqueCustomers = orders.Select(o => o.UserId).Distinct().Count()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get total products sold
|
||||||
|
var orderIds = completedOrders.Select(o => o.Id).ToList();
|
||||||
|
result.Summary.TotalProductsSold = await _context.DiscountOrderDetails
|
||||||
|
.Where(d => orderIds.Contains(d.DiscountOrderId))
|
||||||
|
.SumAsync(d => d.Count, cancellationToken);
|
||||||
|
|
||||||
|
// Generate period-based reports
|
||||||
|
if (request.ReportType != SalesReportType.Summary && completedOrders.Any())
|
||||||
|
{
|
||||||
|
result.Periods = GeneratePeriodReport(completedOrders, request.ReportType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get top selling products
|
||||||
|
result.TopProducts = await GetTopSellingProducts(orderIds, cancellationToken);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SalesPeriodDto> GeneratePeriodReport(List<Domain.Entities.DiscountShop.DiscountOrder> orders, SalesReportType reportType)
|
||||||
|
{
|
||||||
|
var periods = new List<SalesPeriodDto>();
|
||||||
|
|
||||||
|
var groupedOrders = reportType switch
|
||||||
|
{
|
||||||
|
SalesReportType.Daily => orders.GroupBy(o => o.Created.Date),
|
||||||
|
SalesReportType.Weekly => orders.GroupBy(o => GetStartOfWeek(o.Created)),
|
||||||
|
SalesReportType.Monthly => orders.GroupBy(o => new DateTime(o.Created.Year, o.Created.Month, 1)),
|
||||||
|
_ => orders.GroupBy(o => o.Created.Date)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var group in groupedOrders.OrderBy(g => g.Key))
|
||||||
|
{
|
||||||
|
var periodStart = group.Key;
|
||||||
|
var periodEnd = reportType switch
|
||||||
|
{
|
||||||
|
SalesReportType.Daily => periodStart.AddDays(1).AddSeconds(-1),
|
||||||
|
SalesReportType.Weekly => periodStart.AddDays(7).AddSeconds(-1),
|
||||||
|
SalesReportType.Monthly => periodStart.AddMonths(1).AddSeconds(-1),
|
||||||
|
_ => periodStart.AddDays(1).AddSeconds(-1)
|
||||||
|
};
|
||||||
|
|
||||||
|
periods.Add(new SalesPeriodDto
|
||||||
|
{
|
||||||
|
PeriodLabel = GetPersianPeriodLabel(periodStart, reportType),
|
||||||
|
PeriodStart = periodStart,
|
||||||
|
PeriodEnd = periodEnd,
|
||||||
|
OrdersCount = group.Count(),
|
||||||
|
TotalAmount = group.Sum(o => o.TotalAmount),
|
||||||
|
DiscountUsed = group.Sum(o => o.DiscountBalanceUsed),
|
||||||
|
GatewayPaid = group.Sum(o => o.GatewayAmountPaid)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return periods;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<TopSellingProductDto>> GetTopSellingProducts(List<long> orderIds, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!orderIds.Any())
|
||||||
|
return new List<TopSellingProductDto>();
|
||||||
|
|
||||||
|
var topProducts = await _context.DiscountOrderDetails
|
||||||
|
.Where(d => orderIds.Contains(d.DiscountOrderId))
|
||||||
|
.GroupBy(d => d.ProductId)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
ProductId = g.Key,
|
||||||
|
QuantitySold = g.Sum(d => d.Count),
|
||||||
|
TotalRevenue = g.Sum(d => d.FinalPrice),
|
||||||
|
OrdersCount = g.Select(d => d.DiscountOrderId).Distinct().Count()
|
||||||
|
})
|
||||||
|
.OrderByDescending(x => x.QuantitySold)
|
||||||
|
.Take(10)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var productIds = topProducts.Select(p => p.ProductId).ToList();
|
||||||
|
var products = await _context.DiscountProducts
|
||||||
|
.Where(p => productIds.Contains(p.Id))
|
||||||
|
.Select(p => new { p.Id, p.Title, p.ThumbnailPath })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return topProducts.Select(tp => new TopSellingProductDto
|
||||||
|
{
|
||||||
|
ProductId = tp.ProductId,
|
||||||
|
ProductTitle = products.FirstOrDefault(p => p.Id == tp.ProductId)?.Title ?? "نامشخص",
|
||||||
|
ImagePath = products.FirstOrDefault(p => p.Id == tp.ProductId)?.ThumbnailPath,
|
||||||
|
QuantitySold = tp.QuantitySold,
|
||||||
|
TotalRevenue = tp.TotalRevenue,
|
||||||
|
OrdersCount = tp.OrdersCount
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime GetStartOfWeek(DateTime date)
|
||||||
|
{
|
||||||
|
// شروع هفته از شنبه (Saturday = 6 in DayOfWeek)
|
||||||
|
var diff = ((int)date.DayOfWeek + 1) % 7; // Saturday = 0
|
||||||
|
return date.AddDays(-diff).Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetPersianPeriodLabel(DateTime date, SalesReportType reportType)
|
||||||
|
{
|
||||||
|
var persianYear = PersianCalendar.GetYear(date);
|
||||||
|
var persianMonth = PersianCalendar.GetMonth(date);
|
||||||
|
var persianDay = PersianCalendar.GetDayOfMonth(date);
|
||||||
|
|
||||||
|
return reportType switch
|
||||||
|
{
|
||||||
|
SalesReportType.Daily => $"{persianYear}/{persianMonth:D2}/{persianDay:D2}",
|
||||||
|
SalesReportType.Weekly => $"هفته {GetPersianWeekOfYear(date)} - {persianYear}",
|
||||||
|
SalesReportType.Monthly => $"{GetPersianMonthName(persianMonth)} {persianYear}",
|
||||||
|
_ => $"{persianYear}/{persianMonth:D2}/{persianDay:D2}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetPersianWeekOfYear(DateTime date)
|
||||||
|
{
|
||||||
|
var firstDayOfYear = PersianCalendar.ToDateTime(PersianCalendar.GetYear(date), 1, 1, 0, 0, 0, 0);
|
||||||
|
var daysSinceStart = (date - firstDayOfYear).Days;
|
||||||
|
return (daysSinceStart / 7) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetPersianMonthName(int month) => month switch
|
||||||
|
{
|
||||||
|
1 => "فروردین",
|
||||||
|
2 => "اردیبهشت",
|
||||||
|
3 => "خرداد",
|
||||||
|
4 => "تیر",
|
||||||
|
5 => "مرداد",
|
||||||
|
6 => "شهریور",
|
||||||
|
7 => "مهر",
|
||||||
|
8 => "آبان",
|
||||||
|
9 => "آذر",
|
||||||
|
10 => "دی",
|
||||||
|
11 => "بهمن",
|
||||||
|
12 => "اسفند",
|
||||||
|
_ => "نامشخص"
|
||||||
|
};
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
namespace CMSMicroservice.Application.HealthCQ.Queries.GetSystemHealth;
|
||||||
|
|
||||||
|
public class GetSystemHealthQuery : IRequest<GetSystemHealthResponseDto>
|
||||||
|
{
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user