Compare commits
90 Commits
stage_new
...
b42d9e141d
| Author | SHA1 | Date | |
|---|---|---|---|
| b42d9e141d | |||
| f64b6be7da | |||
| 9185aa227d | |||
| b2d676b555 | |||
| b41342dcad | |||
| c3eeb16856 | |||
| 794dd01ac0 | |||
| 658d076bdf | |||
| 96daf899c7 | |||
| eddc82dfce | |||
| 0c95b03f0b | |||
| 45c01fc157 | |||
| 55a775f51c | |||
| 5b0558ee24 | |||
| cabf3886de | |||
| 0baed05fd4 | |||
| 7c4c978d3f | |||
| e327eb23ba | |||
| 20280cf07f | |||
| c67caca0ae | |||
| e14a653fa8 | |||
| 4968878773 | |||
| 8ffb53996b | |||
| 1a01fd9057 | |||
| 52af1bfaad | |||
| 2fd5300c1e | |||
| c76af47a4c | |||
| 77be841221 | |||
| 4743b52cae | |||
| 0b0fa3aa61 | |||
| 839250e132 | |||
| 8894e72a8e | |||
| 1c130b14e1 | |||
| 793d81f1dd | |||
| f6704eaa99 | |||
| d03fbe0a98 | |||
| f9c90bb871 | |||
| 70c4b15a23 | |||
| 5e662b5984 | |||
| 16ba2fa6fb | |||
| 389568af2b | |||
| 98f3036bd0 | |||
| 631646776e | |||
| d8031a0d17 | |||
| ca9b02a902 | |||
| dde4c68b2f | |||
| 8c3d710253 | |||
| a81fb39a32 | |||
| f8731bc7fc | |||
| d3d021c007 | |||
| bea43a0569 | |||
| 1cf501711f | |||
| f0117eb1d5 | |||
| 500e169141 | |||
| ed45a1759b | |||
| 7087df23ef | |||
| 6374d5c343 | |||
| 1dbb3ee502 | |||
| cd2549775a | |||
| 322242fbab | |||
| 3d42c91d21 | |||
| 9448f3fff0 | |||
| 02e2f8111f | |||
| 9d2b5ad2d4 | |||
| 8940362575 | |||
| bca3b7fe62 | |||
| c03b3705c3 | |||
| a9140bec04 | |||
| 17c2958b9c | |||
| 2e54aa7aca | |||
| 7b26073c63 | |||
| 28e94d6137 | |||
| c6ee356cbd | |||
| 2d09a69be9 | |||
| 7cebb27171 | |||
| 68489a8374 | |||
| 330f0cae5a | |||
| b16f01b6e4 | |||
| 13f24d523b | |||
| 41308e189a | |||
| 8953d86755 | |||
| ef8d5d4d97 | |||
| 288b85a59b | |||
| 30144a07c8 | |||
| 5bcf6c9840 | |||
| 9d51e70a7f | |||
| a31c88219f | |||
| ceeb29d843 | |||
| 0ba6af9049 | |||
| 5ad69cb3fa |
@@ -6,81 +6,84 @@ on:
|
||||
- kub-stage
|
||||
|
||||
env:
|
||||
REGISTRY: git.foursat.afrino.co
|
||||
REGISTRY: 194.5.195.53:30080
|
||||
IMAGE_NAME: admin/cms
|
||||
K8S_SERVER: 194.5.195.53
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: docker:latest
|
||||
image: 194.5.195.53:32082/docker-sshpass:latest
|
||||
options: --privileged
|
||||
env:
|
||||
HTTP_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
|
||||
HTTPS_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
|
||||
NO_PROXY: localhost,127.0.0.1,gitea-svc,194.5.195.53,10.0.0.0/8
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
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
|
||||
- name: Start Docker daemon
|
||||
run: |
|
||||
mkdir -p /etc/docker
|
||||
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
|
||||
mkdir -p ~/.docker
|
||||
cat > ~/.docker/config.json << 'CONF'
|
||||
{
|
||||
"proxies": {
|
||||
"default": {
|
||||
"httpProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
|
||||
"httpsProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
|
||||
"noProxy": "localhost,127.0.0.1,gitea-svc,194.5.195.53,10.0.0.0/8"
|
||||
}
|
||||
}
|
||||
}
|
||||
CONF
|
||||
echo "🚀 Starting Docker daemon..."
|
||||
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
|
||||
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
|
||||
run: |
|
||||
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
|
||||
run: |
|
||||
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
||||
-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 \
|
||||
.
|
||||
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest .
|
||||
|
||||
- name: Push to Registry
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
|
||||
- name: Deploy to Kubernetes
|
||||
run: |
|
||||
# Setup kubeconfig
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config
|
||||
|
||||
# Restart deployment to pull new image
|
||||
kubectl rollout restart deployment/cms || echo "Deployment doesn't exist yet"
|
||||
|
||||
# Wait for rollout to complete
|
||||
kubectl rollout status deployment/cms --timeout=5m || echo "Deployment rollout pending"
|
||||
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
|
||||
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||
kubectl rollout restart deployment/cms
|
||||
kubectl rollout status deployment/cms --timeout=180s
|
||||
"
|
||||
echo "✅ Deployed!"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
name: Build and Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
|
||||
env:
|
||||
REGISTRY: 194.5.195.53:30080
|
||||
IMAGE_NAME: admin/cms
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: docker:latest
|
||||
options: --privileged
|
||||
env:
|
||||
HTTP_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
|
||||
HTTPS_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
|
||||
NO_PROXY: localhost,127.0.0.1,gitea-svc,45.149.79.127,10.0.0.0/8
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apk add --no-cache git curl
|
||||
|
||||
# Install kubectl with fixed version
|
||||
KUBECTL_VERSION="v1.31.0"
|
||||
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
|
||||
chmod +x kubectl
|
||||
mv kubectl /usr/local/bin/
|
||||
|
||||
- name: Start Docker daemon with insecure registry
|
||||
run: |
|
||||
mkdir -p /etc/docker
|
||||
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||
{
|
||||
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32082", "gitea-svc:3000"]
|
||||
}
|
||||
DAEMON
|
||||
mkdir -p ~/.docker
|
||||
cat > ~/.docker/config.json << 'CONF'
|
||||
{
|
||||
"proxies": {
|
||||
"default": {
|
||||
"httpProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
|
||||
"httpsProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
|
||||
"noProxy": "localhost,127.0.0.1,gitea-svc,45.149.79.127,10.0.0.0/8"
|
||||
}
|
||||
}
|
||||
}
|
||||
CONF
|
||||
dockerd &
|
||||
for i in $(seq 1 30); do
|
||||
docker info >/dev/null 2>&1 && break || sleep 2
|
||||
done
|
||||
docker info
|
||||
|
||||
- name: Checkout code
|
||||
run: |
|
||||
git clone --depth 1 --branch production http://gitea-svc:3000/admin/CMS.git .
|
||||
git log -1 --format="%H %s"
|
||||
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
|
||||
--build-arg HTTP_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
|
||||
--build-arg HTTPS_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
|
||||
.
|
||||
|
||||
- name: Push to Registry
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod
|
||||
|
||||
- name: Deploy to Production
|
||||
run: |
|
||||
# Setup kubeconfig for PRODUCTION
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.KUBECONFIG_PROD }}" | base64 -d > ~/.kube/config
|
||||
|
||||
# Restart deployment to pull new image
|
||||
kubectl rollout restart deployment/cms || echo "Deployment doesn't exist yet"
|
||||
|
||||
# Wait for rollout to complete
|
||||
kubectl rollout status deployment/cms --timeout=5m || echo "Deployment rollout pending"
|
||||
@@ -492,3 +492,8 @@ fabric.properties
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
/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
|
||||
|
||||
# Copy NuGet config first
|
||||
COPY src/NuGet.config ./
|
||||
|
||||
# Copy solution and project files
|
||||
COPY src/ ./
|
||||
|
||||
# Restore and publish
|
||||
RUN dotnet restore "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj"
|
||||
# Restore with Nexus config and publish
|
||||
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
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
|
||||
FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
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)
|
||||
**Production Readiness**: 95%
|
||||
### 🏪 Inventory Management System - NEW!
|
||||
**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
|
||||
|
||||
### ✅ Completed Phases (7)
|
||||
1. ✅ Domain Layer (Entities, Enums, Value Objects)
|
||||
2. ✅ Club Membership System
|
||||
3. ✅ Binary Network Tree
|
||||
4. ✅ **Commission Calculation & Background Worker** (MVP)
|
||||
5. ✅ Protobuf gRPC Services
|
||||
6. ✅ History & Configuration Management
|
||||
7. ✅ Database Migration & Seed Data
|
||||
#### ✅ Completed Features
|
||||
- ✅ Binary network tree with automatic placement
|
||||
- ✅ Club membership (Member/Trial) with commission rates
|
||||
- ✅ Weekly commission calculation (Lesser Leg algorithm)
|
||||
- ✅ Background worker with Hangfire
|
||||
- ✅ Email + SMS notifications (MailKit + Kavenegar)
|
||||
- ✅ Health check endpoints (Kubernetes-ready)
|
||||
|
||||
### 🟡 Partially Complete (1)
|
||||
### 🟡 Partially Complete
|
||||
- Phase 10: Withdrawal & Settlement (40%)
|
||||
- ✅ Commands & Database
|
||||
- ❌ Payment Gateway Integration
|
||||
|
||||
### ❌ Not Started (1)
|
||||
### ❌ Not Started
|
||||
- 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 ✅
|
||||
- ✅ **MailKit 4.14.1** for Email (SMTP with HTML templates)
|
||||
@@ -63,8 +117,38 @@
|
||||
**Clean Architecture** with 4 layers:
|
||||
```
|
||||
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)
|
||||
├── 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
|
||||
├── Persistence/
|
||||
│ ├── Context/
|
||||
│ ├── Configurations/ # NEW: EF Core configs
|
||||
│ ├── Repositories/ # NEW: Repository implementations
|
||||
│ └── Migrations/
|
||||
└── DependencyInjection.cs # NEW: DI setup
|
||||
|
||||
CMSMicroservice.WebApi/ # gRPC Services, Controllers
|
||||
CMSMicroservice.Protobuf/ # Protocol Buffers definitions
|
||||
```
|
||||
@@ -84,6 +168,7 @@ CMSMicroservice.Protobuf/ # Protocol Buffers definitions
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
- **[Development Plan](docs/development-plan.md)** - NEW: Inventory system roadmap
|
||||
- **[Implementation Progress](docs/implementation-progress.md)** - Detailed phase-by-phase progress
|
||||
- **[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
|
||||
@@ -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
|
||||
|
||||
### Prerequisites
|
||||
@@ -197,7 +350,25 @@ curl http://localhost:5133/health/live # Liveness probe (K8s)
|
||||
|
||||
## 📊 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)
|
||||
- Daya or Bank Mellat API integration
|
||||
- IBAN transfer automation
|
||||
@@ -230,6 +401,7 @@ curl http://localhost:5133/health/live # Liveness probe (K8s)
|
||||
|
||||
## 🎯 MVP Features (100% Complete)
|
||||
|
||||
### 💼 Commission System:
|
||||
✅ Binary network tree with automatic placement
|
||||
✅ Club membership (Member/Trial) with different commission rates
|
||||
✅ 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)
|
||||
✅ 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
|
||||
|
||||
**Development**: FourSat Team
|
||||
**Last Updated**: 2025-12-01
|
||||
**Last Updated**: January 2026
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Docs moved to /totalDoc — see totalDoc/INDEX.md
|
||||
@@ -0,0 +1,421 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "a5404a90-7d5a-4e9f-a26b-bb2bc93f19a2",
|
||||
"name": "hushyar",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_exporter_id": "12336342",
|
||||
"_collection_link": "https://abbas-rahimzadeh-postman.postman.co/workspace/public-workspace~57c702f9-fed1-4b27-b507-0b0252812bf7/collection/12336342-a5404a90-7d5a-4e9f-a26b-bb2bc93f19a2?action=share&source=collection_link&creator=12336342"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "v1",
|
||||
"item": [
|
||||
{
|
||||
"name": "organization",
|
||||
"item": [
|
||||
{
|
||||
"name": "register-user",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "{{organizationApiKey}}",
|
||||
"description": "Organization API Key",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"organizations",
|
||||
"register-user"
|
||||
]
|
||||
},
|
||||
"description": "Register a new user or get existing user via organization API key. If user is new or has no organization, they will be assigned to this organization and their wallet will be charged with the organization's user_credit."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "success - new user",
|
||||
"originalRequest": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "{{organizationApiKey}}",
|
||||
"description": "Organization API Key",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"organizations",
|
||||
"register-user"
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "content-type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"id\": 1,\n \"mobile_number\": \"09123456789\",\n \"organization_id\": 1,\n \"organization_title\": \"Test Organization\",\n \"wallet_balance\": 100.0,\n \"is_new_user\": true,\n \"credit_charged\": 100.0\n}"
|
||||
},
|
||||
{
|
||||
"name": "success - existing user",
|
||||
"originalRequest": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "{{organizationApiKey}}",
|
||||
"description": "Organization API Key",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"organizations",
|
||||
"register-user"
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "content-type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"id\": 1,\n \"mobile_number\": \"09123456789\",\n \"organization_id\": 1,\n \"organization_title\": \"Test Organization\",\n \"wallet_balance\": 100.0,\n \"is_new_user\": false,\n \"credit_charged\": 0.0\n}"
|
||||
},
|
||||
{
|
||||
"name": "invalid api key",
|
||||
"originalRequest": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "invalid-api-key",
|
||||
"description": "Organization API Key",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"organizations",
|
||||
"register-user"
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "Unauthorized",
|
||||
"code": 401,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "content-type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"error_code\": \"INVALID_API_KEY\",\n \"message\": \"Invalid API key\",\n \"context\": {\n \"api_key_prefix\": \"invalid-\"\n }\n}"
|
||||
},
|
||||
{
|
||||
"name": "organization disabled",
|
||||
"originalRequest": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "{{organizationApiKey}}",
|
||||
"description": "Organization API Key",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"organizations",
|
||||
"register-user"
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "Forbidden",
|
||||
"code": 403,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "content-type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"error_code\": \"ORGANIZATION_DISABLED\",\n \"message\": \"Organization is disabled\",\n \"context\": {\n \"organization_id\": 1\n }\n}"
|
||||
},
|
||||
{
|
||||
"name": "organization expired",
|
||||
"originalRequest": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "{{organizationApiKey}}",
|
||||
"description": "Organization API Key",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"organizations",
|
||||
"register-user"
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "Forbidden",
|
||||
"code": 403,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "content-type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"error_code\": \"ORGANIZATION_EXPIRED\",\n \"message\": \"Organization has expired\",\n \"context\": {\n \"organization_id\": 1,\n \"expires_at\": \"2024-01-01T00:00:00\"\n }\n}"
|
||||
},
|
||||
{
|
||||
"name": "invalid mobile format",
|
||||
"originalRequest": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "{{organizationApiKey}}",
|
||||
"description": "Organization API Key",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"mobile_number\": \"01234567890\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"organizations",
|
||||
"register-user"
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "Unprocessable Entity",
|
||||
"code": 422,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "content-type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"mobile_number\": \"String should match pattern '^09\\\\d{9}$'\"\n}"
|
||||
},
|
||||
{
|
||||
"name": "missing api key header",
|
||||
"originalRequest": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"organizations",
|
||||
"register-user"
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "Unprocessable Entity",
|
||||
"code": 422,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "content-type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"detail\": [\n {\n \"type\": \"missing\",\n \"loc\": [\"header\", \"x-api-key\"],\n \"msg\": \"Field required\",\n \"input\": null\n }\n ]\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"packages": {},
|
||||
"exec": [
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"packages": {},
|
||||
"exec": [
|
||||
""
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseURL",
|
||||
"value": "http://127.0.0.0:8000",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "access_token",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "accessToken",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "refreshToken",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "organizationApiKey",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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,10 +7,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2" />
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.4.0" />
|
||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.10" />
|
||||
|
||||
+155
-12
@@ -1,5 +1,10 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Common;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Entities.Club;
|
||||
using CMSMicroservice.Domain.Entities.Commission;
|
||||
using CMSMicroservice.Domain.Entities.History;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -12,6 +17,8 @@ namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.AcceptClubMember
|
||||
/// 1. کد OTP را تایید میکند
|
||||
/// 2. قرارداد را در جدول UserContract ثبت میکند
|
||||
/// 3. باشگاه مشتری را فعال میکند (IsActive = true)
|
||||
/// 4. مبلغ را به Pool هفته جاری اضافه میکند
|
||||
/// 5. ویژگیهای باشگاه را به کاربر اعطا میکند
|
||||
/// </summary>
|
||||
public class AcceptClubMembershipContractCommandHandler
|
||||
: IRequestHandler<AcceptClubMembershipContractCommand, AcceptClubMembershipContractResponseDto>
|
||||
@@ -19,6 +26,8 @@ public class AcceptClubMembershipContractCommandHandler
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IConfiguration _cfg;
|
||||
private readonly IHashService _hashService;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
private readonly IWeekDefinitionRepository _weekRepository;
|
||||
private readonly ILogger<AcceptClubMembershipContractCommandHandler> _logger;
|
||||
|
||||
private const int MaxAttempts = 5;
|
||||
@@ -28,11 +37,15 @@ public class AcceptClubMembershipContractCommandHandler
|
||||
IApplicationDbContext context,
|
||||
IConfiguration cfg,
|
||||
IHashService hashService,
|
||||
ICurrentUserService currentUser,
|
||||
IWeekDefinitionRepository weekRepository,
|
||||
ILogger<AcceptClubMembershipContractCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_cfg = cfg;
|
||||
_hashService = hashService;
|
||||
_currentUser = currentUser;
|
||||
_weekRepository = weekRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -114,33 +127,150 @@ public class AcceptClubMembershipContractCommandHandler
|
||||
};
|
||||
await _context.UserContracts.AddAsync(userContract, cancellationToken);
|
||||
|
||||
// 6. فعالسازی باشگاه مشتریان
|
||||
if (user.ClubMembership == null)
|
||||
// 6. دریافت مقادیر از SystemConstants (استاتیک)
|
||||
long giftValue = SystemConstants.ClubMembershipGiftValue;
|
||||
long activationFeeValue = SystemConstants.ClubActivationFee;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Using Club.MembershipGiftValue: {GiftValue}, Club.ActivationFee: {ActivationFee}",
|
||||
giftValue, activationFeeValue
|
||||
);
|
||||
|
||||
// 7. فعالسازی باشگاه مشتریان
|
||||
ClubMembership clubMembership;
|
||||
bool isNewMembership = user.ClubMembership == null;
|
||||
var activationDate = DateTime.Now;
|
||||
|
||||
if (isNewMembership)
|
||||
{
|
||||
user.ClubMembership = new Domain.Entities.Club.ClubMembership
|
||||
clubMembership = new ClubMembership
|
||||
{
|
||||
UserId = user.Id,
|
||||
IsActive = true,
|
||||
ActivatedAt = DateTime.Now,
|
||||
InitialContribution = 56_000_000,
|
||||
GiftValue = 25_200_000,
|
||||
ActivatedAt = activationDate,
|
||||
InitialContribution = activationFeeValue,
|
||||
GiftValue = giftValue,
|
||||
TotalEarned = 0,
|
||||
PurchaseMethod = user.PackagePurchaseMethod
|
||||
};
|
||||
await _context.ClubMemberships.AddAsync(user.ClubMembership, cancellationToken);
|
||||
await _context.ClubMemberships.AddAsync(clubMembership, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created new club membership for UserId {UserId} via {Method}, GiftValue: {GiftValue}",
|
||||
user.Id,
|
||||
user.PackagePurchaseMethod,
|
||||
giftValue
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
user.ClubMembership.IsActive = true;
|
||||
user.ClubMembership.ActivatedAt = DateTime.Now;
|
||||
_context.ClubMemberships.Update(user.ClubMembership);
|
||||
clubMembership = user.ClubMembership!;
|
||||
clubMembership.IsActive = true;
|
||||
clubMembership.ActivatedAt = activationDate;
|
||||
clubMembership.PurchaseMethod = user.PackagePurchaseMethod;
|
||||
_context.ClubMemberships.Update(clubMembership);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Reactivated club membership for UserId {UserId}",
|
||||
user.Id
|
||||
);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 8. ثبت تاریخچه
|
||||
var history = new ClubMembershipHistory
|
||||
{
|
||||
ClubMembershipId = clubMembership.Id,
|
||||
UserId = clubMembership.UserId,
|
||||
OldIsActive = !isNewMembership && !user.ClubMembership!.IsActive,
|
||||
NewIsActive = true,
|
||||
Action = ClubMembershipAction.Activated,
|
||||
Reason = isNewMembership
|
||||
? $"Initial activation via contract signing - {user.PackagePurchaseMethod}"
|
||||
: $"Reactivated via contract signing - {user.PackagePurchaseMethod}",
|
||||
PerformedBy = _currentUser.GetPerformedBy()
|
||||
};
|
||||
|
||||
_context.ClubMembershipHistories.Add(history);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 9. اضافه کردن مبلغ به Pool هفته جاری
|
||||
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
|
||||
var weeklyPool = await _context.WeeklyCommissionPools
|
||||
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId, cancellationToken);
|
||||
|
||||
if (weeklyPool == null)
|
||||
{
|
||||
weeklyPool = new WeeklyCommissionPool
|
||||
{
|
||||
WeekDefinitionId = currentWeekDefinitionId,
|
||||
TotalPoolAmount = activationFeeValue,
|
||||
TotalBalances = 0,
|
||||
ValuePerBalance = 0,
|
||||
IsCalculated = false,
|
||||
CalculatedAt = null
|
||||
};
|
||||
|
||||
await _context.WeeklyCommissionPools.AddAsync(weeklyPool, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created new WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId} with initial amount: {Amount}",
|
||||
currentWeekDefinitionId,
|
||||
activationFeeValue
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
weeklyPool.TotalPoolAmount += activationFeeValue;
|
||||
_context.WeeklyCommissionPools.Update(weeklyPool);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Added {Amount} to existing WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId}. New total: {NewTotal}",
|
||||
activationFeeValue,
|
||||
currentWeekDefinitionId,
|
||||
weeklyPool.TotalPoolAmount
|
||||
);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 10. اعطای ویژگیهای باشگاه برای کاربر (فقط برای عضویت جدید)
|
||||
if (isNewMembership)
|
||||
{
|
||||
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
|
||||
var clubFeatures = await _context.ClubFeatures
|
||||
.Where(f => !f.IsDeleted && featureIds.Contains(f.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (clubFeatures.Any())
|
||||
{
|
||||
var userClubFeatures = clubFeatures.Select(feature => new UserClubFeature
|
||||
{
|
||||
UserId = user.Id,
|
||||
ClubMembershipId = clubMembership.Id,
|
||||
ClubFeatureId = feature.Id,
|
||||
GrantedAt = activationDate,
|
||||
IsActive = true,
|
||||
Notes = "اعطا شده هنگام امضای قرارداد"
|
||||
}).ToList();
|
||||
|
||||
_context.UserClubFeatures.AddRange(userClubFeatures);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Granted {Count} club features to UserId {UserId}",
|
||||
clubFeatures.Count,
|
||||
user.Id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Club membership contract accepted and activated for UserId: {UserId}, ContractId: {ContractId}",
|
||||
"Club membership contract accepted and activated for UserId: {UserId}, ContractId: {ContractId}, MembershipId: {MembershipId}",
|
||||
request.UserId,
|
||||
userContract.Id
|
||||
userContract.Id,
|
||||
clubMembership.Id
|
||||
);
|
||||
|
||||
return new AcceptClubMembershipContractResponseDto
|
||||
@@ -151,6 +281,19 @@ public class AcceptClubMembershipContractCommandHandler
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت شناسه تعریف هفته جاری
|
||||
/// </summary>
|
||||
private long GetCurrentWeekDefinitionId()
|
||||
{
|
||||
var week = _weekRepository.GetCurrentWeek();
|
||||
if (week == null)
|
||||
{
|
||||
throw new InvalidOperationException("هفته جاری در سیستم تعریف نشده است");
|
||||
}
|
||||
return week.Id;
|
||||
}
|
||||
|
||||
private async Task<(bool Success, string Message)> VerifyOtpAsync(
|
||||
string mobile,
|
||||
string code,
|
||||
|
||||
+12
-47
@@ -1,6 +1,7 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Common;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Entities.Club;
|
||||
using CMSMicroservice.Domain.Entities.Commission;
|
||||
@@ -75,7 +76,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
throw new NotFoundException("کیف پول کاربر یافت نشد");
|
||||
}
|
||||
|
||||
if (wallet.Balance < 56_000_000)
|
||||
if (wallet.Balance < SystemConstants.BasePackageAmount)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"User {UserId} has insufficient balance: {Balance}",
|
||||
@@ -83,7 +84,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
wallet.Balance
|
||||
);
|
||||
throw new BadRequestException(
|
||||
"برای فعالسازی باشگاه مشتریان باید حداقل 56 میلیون تومان موجودی اصلی داشته باشید"
|
||||
$"برای فعالسازی باشگاه مشتریان باید حداقل {SystemConstants.BasePackageAmount:N0} ریال موجودی اصلی داشته باشید"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,51 +136,14 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
var existingMembership = await _context.ClubMemberships
|
||||
.FirstOrDefaultAsync(c => c.UserId == user.Id, cancellationToken);
|
||||
|
||||
// 6.1. دریافت مبلغ هدیه از تنظیمات
|
||||
var giftValueConfig = await _context.SystemConfigurations
|
||||
.FirstOrDefaultAsync(
|
||||
c => c.Key == "Club.MembershipGiftValue" && c.IsActive,
|
||||
cancellationToken
|
||||
);
|
||||
// 6.1. دریافت مبلغ هدیه و هزینه فعالسازی از SystemConstants
|
||||
long giftValue = SystemConstants.ClubMembershipGiftValue;
|
||||
long activationFeeValue = SystemConstants.ClubActivationFee;
|
||||
|
||||
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(
|
||||
"Using Club.MembershipGiftValue from configuration: {GiftValue}",
|
||||
giftValue
|
||||
);
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
_logger.LogInformation(
|
||||
"Using Club.MembershipGiftValue: {GiftValue}, Club.ActivationFee: {ActivationFee}",
|
||||
giftValue, activationFeeValue
|
||||
);
|
||||
|
||||
ClubMembership entity;
|
||||
bool isNewMembership = existingMembership == null;
|
||||
@@ -297,8 +261,9 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
// 9. اضافه کردن ویژگیهای باشگاه برای کاربر (فقط برای عضویت جدید)
|
||||
if (isNewMembership)
|
||||
{
|
||||
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
|
||||
var clubFeatures = await _context.ClubFeatures
|
||||
.Where(f => !f.IsDeleted && new long[] { 1, 2, 3, 4 }.Contains(f.Id))
|
||||
.Where(f => !f.IsDeleted && featureIds.Contains(f.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (clubFeatures.Any())
|
||||
|
||||
+25
-1
@@ -3,14 +3,18 @@ namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership
|
||||
public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQuery, ClubMembershipDto?>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ILogger<GetClubMembershipQueryHandler> _logger;
|
||||
|
||||
public GetClubMembershipQueryHandler(IApplicationDbContext context)
|
||||
public GetClubMembershipQueryHandler(IApplicationDbContext context, ILogger<GetClubMembershipQueryHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ClubMembershipDto?> Handle(GetClubMembershipQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("GetClubMembership called for UserId: {UserId}", request.UserId);
|
||||
|
||||
var membership = await _context.ClubMemberships
|
||||
.AsNoTracking()
|
||||
.Where(x => x.UserId == request.UserId)
|
||||
@@ -27,6 +31,26 @@ public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQu
|
||||
})
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-13
@@ -1,3 +1,5 @@
|
||||
using CMSMicroservice.Domain.Common;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
|
||||
|
||||
public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWeeklyBalancesCommand, int>
|
||||
@@ -74,21 +76,11 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
||||
var balancesList = new List<NetworkWeeklyBalance>();
|
||||
var calculatedAt = DateTime.Now;
|
||||
|
||||
// خواندن یکباره Configuration ها (بهینهسازی - به جای N query)
|
||||
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;
|
||||
// استفاده از SystemConstants (استاتیک - بدون کوئری به دیتابیس)
|
||||
// سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل
|
||||
var maxBalancesPerLeg = int.Parse(configs.GetValueOrDefault("Commission.MaxWeeklyBalancesPerLeg", "300"));
|
||||
var maxBalancesPerLeg = SystemConstants.CommissionMaxWeeklyBalancesPerLeg;
|
||||
// حداکثر عمق شبکه برای شمارش اعضا (15 لول)
|
||||
var maxNetworkLevel = int.Parse(configs.GetValueOrDefault("Commission.MaxNetworkLevel", "15"));
|
||||
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
|
||||
|
||||
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;
|
||||
|
||||
public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayoutsCommand, int>
|
||||
@@ -47,12 +49,8 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// ⭐ خواندن MaxNetworkLevel از Config
|
||||
var maxNetworkLevelConfig = await _context.SystemConfigurations
|
||||
.Where(x => x.Key == "Commission.MaxNetworkLevel" && x.IsActive)
|
||||
.Select(x => x.Value)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
var maxNetworkLevel = int.Parse(maxNetworkLevelConfig ?? "15");
|
||||
// ⭐ خواندن MaxNetworkLevel از SystemConstants (استاتیک)
|
||||
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
|
||||
|
||||
// دریافت همه تعادلهای هفتگی (شامل صفرها هم برای محاسبه زیرمجموعه)
|
||||
var allWeeklyBalances = await _context.NetworkWeeklyBalances
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQu
|
||||
{
|
||||
Id = x.Id,
|
||||
WeekDefinitionId = x.WeekDefinitionId,
|
||||
WeekDisplayName = x.WeekDefinition.PersianWeekNumber,
|
||||
WeekDisplayName = x.WeekDefinition.DisplayName,
|
||||
TotalPoolAmount = x.TotalPoolAmount,
|
||||
TotalBalances = x.TotalBalances,
|
||||
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 IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public GetUserCommissionPayoutsQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekDefinitionRepository)
|
||||
IWeekDefinitionRepository weekDefinitionRepository,
|
||||
ICurrentUserService currentUser)
|
||||
{
|
||||
_context = context;
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<GetUserCommissionPayoutsResponseDto> Handle(GetUserCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
||||
@@ -21,10 +24,20 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
// فیلترها
|
||||
if (request.UserId.HasValue)
|
||||
// اگر UserId داده نشده، از CurrentUser بگیر (برای Customer API)
|
||||
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)
|
||||
|
||||
+25
-6
@@ -4,26 +4,40 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public GetUserWeeklyBalancesQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekDefinitionRepository)
|
||||
IWeekDefinitionRepository weekDefinitionRepository,
|
||||
ICurrentUserService currentUser)
|
||||
{
|
||||
_context = context;
|
||||
_weekDefinitionRepository = weekDefinitionRepository;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetUserWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.NetworkWeeklyBalances
|
||||
.Include(x => x.WeekDefinition)
|
||||
.Include(x => x.User)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
// فیلترها
|
||||
if (request.UserId.HasValue)
|
||||
// اگر UserId داده نشده، از CurrentUser بگیر (برای Customer API)
|
||||
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 (روش ترجیحی)
|
||||
@@ -49,10 +63,15 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
||||
{
|
||||
Id = x.Id,
|
||||
UserId = x.UserId,
|
||||
UserFullName = x.User != null ? $"{x.User.FirstName} {x.User.LastName}".Trim() : "",
|
||||
WeekDefinitionId = x.WeekDefinitionId,
|
||||
WeekDisplayName = x.WeekDefinition != null ? x.WeekDefinition.DisplayName : "",
|
||||
LeftLegBalances = x.LeftLegBalances,
|
||||
RightLegBalances = x.RightLegBalances,
|
||||
LeftLegNewMembers = x.LeftLegNewMembers,
|
||||
LeftLegCarryover = x.LeftLegCarryover,
|
||||
LeftLegTotal = x.LeftLegTotal,
|
||||
RightLegNewMembers = x.RightLegNewMembers,
|
||||
RightLegCarryover = x.RightLegCarryover,
|
||||
RightLegTotal = x.RightLegTotal,
|
||||
TotalBalances = x.TotalBalances,
|
||||
WeeklyPoolContribution = x.WeeklyPoolContribution,
|
||||
CalculatedAt = x.CalculatedAt,
|
||||
|
||||
+12
-3
@@ -10,11 +10,20 @@ public class GetUserWeeklyBalancesResponseModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string UserFullName { get; set; } = string.Empty;
|
||||
public long WeekDefinitionId { get; set; }
|
||||
|
||||
public string WeekDisplayName { get; set; } = string.Empty;
|
||||
public int LeftLegBalances { get; set; }
|
||||
public int RightLegBalances { get; set; }
|
||||
|
||||
// چپ
|
||||
public int 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 long WeeklyPoolContribution { get; set; }
|
||||
public DateTime? CalculatedAt { get; set; }
|
||||
|
||||
+1
@@ -107,6 +107,7 @@ public class GetWeekDefinitionsQueryHandler : IRequestHandler<GetWeekDefinitions
|
||||
// تبدیل به DTO
|
||||
var weekDtos = weeksList.Select(w => new WeekDefinitionItemDto
|
||||
{
|
||||
Id = w.Id,
|
||||
WeekOrder = w.WeekOrder,
|
||||
DisplayName = w.DisplayName,
|
||||
GregorianWeekNumber = w.GregorianWeekNumber,
|
||||
|
||||
+3
@@ -17,6 +17,9 @@ public class GetWeekDefinitionsResponseDto
|
||||
/// </summary>
|
||||
public class WeekDefinitionItemDto
|
||||
{
|
||||
//شناسه هفته
|
||||
public long Id { get; set; }
|
||||
|
||||
//شماره ترتیب هفته (1, 2, 3, ...)
|
||||
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; }
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using CMSMicroservice.Domain.Entities.Payment;
|
||||
using CMSMicroservice.Domain.Entities.Order;
|
||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||
using CMSMicroservice.Domain.Entities.Geography;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
@@ -30,8 +31,6 @@ public interface IApplicationDbContext
|
||||
DbSet<UserPackagePurchase> UserPackagePurchases { get; }
|
||||
DbSet<UserWallet> UserWallets { get; }
|
||||
DbSet<UserWalletChangeLog> UserWalletChangeLogs { get; }
|
||||
DbSet<SystemConfiguration> SystemConfigurations { get; }
|
||||
DbSet<SystemConfigurationHistory> SystemConfigurationHistories { get; }
|
||||
DbSet<ManualPayment> ManualPayments { get; }
|
||||
DbSet<PublicMessage> PublicMessages { get; }
|
||||
DbSet<ClubMembership> ClubMemberships { get; }
|
||||
@@ -46,11 +45,13 @@ public interface IApplicationDbContext
|
||||
DbSet<CommissionPayoutHistory> CommissionPayoutHistories { get; }
|
||||
DbSet<WorkerExecutionLog> WorkerExecutionLogs { get; }
|
||||
DbSet<DayaLoanContract> DayaLoanContracts { get; }
|
||||
DbSet<AppVersion> AppVersions { get; }
|
||||
|
||||
// ============= Discount Shop =============
|
||||
DbSet<DiscountProduct> DiscountProducts { get; }
|
||||
DbSet<DiscountCategory> DiscountCategories { get; }
|
||||
DbSet<DiscountProductCategory> DiscountProductCategories { get; }
|
||||
DbSet<DiscountProductImage> DiscountProductImages { get; }
|
||||
DbSet<DiscountShoppingCart> DiscountShoppingCarts { get; }
|
||||
DbSet<DiscountOrder> DiscountOrders { get; }
|
||||
DbSet<DiscountOrderDetail> DiscountOrderDetails { get; }
|
||||
@@ -60,5 +61,15 @@ public interface IApplicationDbContext
|
||||
DbSet<State> States { get; }
|
||||
DbSet<City> Cities { get; }
|
||||
|
||||
// ============= Inventory Management =============
|
||||
DbSet<Warehouse> Warehouses { get; }
|
||||
DbSet<InventoryItem> InventoryItems { get; }
|
||||
DbSet<StockMovement> StockMovements { get; }
|
||||
|
||||
/// <summary>
|
||||
/// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures
|
||||
/// </summary>
|
||||
DatabaseFacade Database { get; }
|
||||
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس ارتباط با API چتیکا برای ساخت حساب کاربری
|
||||
/// </summary>
|
||||
public interface IChatikaApiService
|
||||
{
|
||||
/// <summary>
|
||||
/// ایجاد حساب کاربری در چتیکا
|
||||
/// </summary>
|
||||
/// <param name="mobileNumber">شماره موبایل کاربر</param>
|
||||
/// <param name="fullName">نام کامل کاربر</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>نتیجه ایجاد حساب</returns>
|
||||
Task<ChatikaAccountResult> CreateAccountAsync(
|
||||
string mobileNumber,
|
||||
string fullName,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// نتیجه ایجاد حساب در چتیکا
|
||||
/// </summary>
|
||||
public class ChatikaAccountResult
|
||||
{
|
||||
/// <summary>
|
||||
/// موفقیت عملیات
|
||||
/// </summary>
|
||||
public bool IsSuccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// پیام خطا در صورت عدم موفقیت
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه کاربر در چتیکا
|
||||
/// </summary>
|
||||
public string? ChatikaUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL دسترسی به چتیکا
|
||||
/// </summary>
|
||||
public string? AccessUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ایجاد نتیجه موفق
|
||||
/// </summary>
|
||||
public static ChatikaAccountResult Success(string? chatikaUserId = null, string? accessUrl = null) => new()
|
||||
{
|
||||
IsSuccess = true,
|
||||
ChatikaUserId = chatikaUserId,
|
||||
AccessUrl = accessUrl
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ایجاد نتیجه ناموفق
|
||||
/// </summary>
|
||||
public static ChatikaAccountResult Failure(string errorMessage) => new()
|
||||
{
|
||||
IsSuccess = false,
|
||||
ErrorMessage = errorMessage
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,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>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IInventoryService _inventoryService;
|
||||
|
||||
public CompleteOrderPaymentCommandHandler(IApplicationDbContext context)
|
||||
public CompleteOrderPaymentCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IInventoryService inventoryService)
|
||||
{
|
||||
_context = context;
|
||||
_inventoryService = inventoryService;
|
||||
}
|
||||
|
||||
public async Task<CompleteOrderPaymentResponseDto> Handle(CompleteOrderPaymentCommand request, CancellationToken cancellationToken)
|
||||
@@ -63,12 +67,18 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
|
||||
userWallet.DiscountBalance -= order.DiscountBalanceUsed;
|
||||
}
|
||||
|
||||
// Update product stock and sale count
|
||||
// تایید فروش و کسر موجودی از طریق InventoryService
|
||||
foreach (var orderDetail in order.OrderDetails)
|
||||
{
|
||||
var product = orderDetail.Product;
|
||||
product.RemainingCount -= orderDetail.Count;
|
||||
product.SaleCount += orderDetail.Count;
|
||||
await _inventoryService.ConfirmSaleAsync(
|
||||
orderDetail.ProductId,
|
||||
ProductType.DiscountProduct,
|
||||
orderDetail.Count,
|
||||
order.Id,
|
||||
cancellationToken);
|
||||
|
||||
// افزایش تعداد فروش
|
||||
orderDetail.Product.SaleCount += orderDetail.Count;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
@@ -82,7 +92,17 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
|
||||
}
|
||||
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;
|
||||
order.PaymentStatus = PaymentStatus.Reject;
|
||||
|
||||
|
||||
-1
@@ -11,6 +11,5 @@ public class CreateDiscountProductCommand : IRequest<long>
|
||||
public int MaxDiscountPercent { get; set; }
|
||||
public string ImagePath { get; set; }
|
||||
public string ThumbnailPath { get; set; }
|
||||
public int RemainingCount { get; set; }
|
||||
public List<long> CategoryIds { get; set; } = new();
|
||||
}
|
||||
|
||||
+14
-2
@@ -1,5 +1,6 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -8,10 +9,14 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.CreateDiscountProd
|
||||
public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscountProductCommand, long>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IInventoryService _inventoryService;
|
||||
|
||||
public CreateDiscountProductCommandHandler(IApplicationDbContext context)
|
||||
public CreateDiscountProductCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IInventoryService inventoryService)
|
||||
{
|
||||
_context = context;
|
||||
_inventoryService = inventoryService;
|
||||
}
|
||||
|
||||
public async Task<long> Handle(CreateDiscountProductCommand request, CancellationToken cancellationToken)
|
||||
@@ -25,7 +30,7 @@ public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscoun
|
||||
MaxDiscountPercent = request.MaxDiscountPercent,
|
||||
ImagePath = request.ImagePath,
|
||||
ThumbnailPath = request.ThumbnailPath,
|
||||
RemainingCount = request.RemainingCount,
|
||||
RemainingCount = 0, // موجودی اولیه صفر - باید از طریق Inventory اضافه شود
|
||||
Rate = 0,
|
||||
SaleCount = 0,
|
||||
ViewCount = 0,
|
||||
@@ -35,6 +40,13 @@ public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscoun
|
||||
_context.DiscountProducts.Add(product);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// ایجاد رکورد موجودی در سیستم انبارداری با موجودی اولیه صفر
|
||||
await _inventoryService.InitializeInventoryAsync(
|
||||
product.Id,
|
||||
ProductType.DiscountProduct,
|
||||
0, // موجودی اولیه صفر - باید از طریق Inventory اضافه شود
|
||||
ct: cancellationToken);
|
||||
|
||||
// Add product categories
|
||||
if (request.CategoryIds.Any())
|
||||
{
|
||||
|
||||
+4
-13
@@ -11,26 +11,17 @@ public class CreateDiscountProductCommandValidator : AbstractValidator<CreateDis
|
||||
.MaximumLength(200).WithMessage("عنوان محصول نمیتواند بیشتر از 200 کاراکتر باشد");
|
||||
|
||||
RuleFor(v => v.ShortInfomation)
|
||||
.NotEmpty().WithMessage("توضیحات کوتاه الزامی است")
|
||||
.MaximumLength(500).WithMessage("توضیحات کوتاه نمیتواند بیشتر از 500 کاراکتر باشد");
|
||||
.MaximumLength(500).WithMessage("توضیحات کوتاه نمیتواند بیشتر از 500 کاراکتر باشد")
|
||||
.When(v => !string.IsNullOrEmpty(v.ShortInfomation));
|
||||
|
||||
RuleFor(v => v.FullInformation)
|
||||
.NotEmpty().WithMessage("توضیحات کامل الزامی است")
|
||||
.MaximumLength(2000).WithMessage("توضیحات کامل نمیتواند بیشتر از 2000 کاراکتر باشد");
|
||||
.MaximumLength(10000).WithMessage("توضیحات کامل نمیتواند بیشتر از 10000 کاراکتر باشد")
|
||||
.When(v => !string.IsNullOrEmpty(v.FullInformation));
|
||||
|
||||
RuleFor(v => v.Price)
|
||||
.GreaterThan(0).WithMessage("قیمت باید بیشتر از صفر باشد");
|
||||
|
||||
RuleFor(v => v.MaxDiscountPercent)
|
||||
.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.Services;
|
||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||
using CMSMicroservice.Domain.Entities.Payment;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
@@ -10,10 +11,14 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.PlaceOrder;
|
||||
public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, PlaceOrderResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IInventoryService _inventoryService;
|
||||
|
||||
public PlaceOrderCommandHandler(IApplicationDbContext context)
|
||||
public PlaceOrderCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IInventoryService inventoryService)
|
||||
{
|
||||
_context = context;
|
||||
_inventoryService = inventoryService;
|
||||
}
|
||||
|
||||
public async Task<PlaceOrderResponseDto> Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
|
||||
@@ -109,9 +114,10 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
||||
|
||||
var gatewayAmountRequired = totalAmount - actualDiscountBalanceUsed;
|
||||
|
||||
// Calculate VAT (9%)
|
||||
var vatAmount = (gatewayAmountRequired * 9) / 100;
|
||||
var finalGatewayAmount = gatewayAmountRequired + vatAmount;
|
||||
// Calculate VAT using centralized calculator
|
||||
var vatBreakdown = VatCalculator.CalculateBreakdown(gatewayAmountRequired);
|
||||
var vatAmount = vatBreakdown.VatAmount;
|
||||
var finalGatewayAmount = vatBreakdown.GrossAmount;
|
||||
|
||||
// Create transaction for gateway payment
|
||||
var transaction = new Transaction
|
||||
@@ -150,6 +156,17 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
||||
|
||||
_context.DiscountOrderDetails.AddRange(orderDetails);
|
||||
|
||||
// رزرو موجودی برای سفارش pending
|
||||
foreach (var cartItem in cartItems)
|
||||
{
|
||||
await _inventoryService.ReserveStockAsync(
|
||||
cartItem.ProductId,
|
||||
ProductType.DiscountProduct,
|
||||
cartItem.Count,
|
||||
order.Id,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
// Clear cart
|
||||
_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 کاراکتر باشد");
|
||||
|
||||
RuleFor(x => x.ShortInfomation)
|
||||
.NotEmpty().WithMessage("توضیحات کوتاه الزامی است")
|
||||
.MaximumLength(500).WithMessage("توضیحات کوتاه نباید بیشتر از 500 کاراکتر باشد");
|
||||
.MaximumLength(500).WithMessage("توضیحات کوتاه نباید بیشتر از 500 کاراکتر باشد")
|
||||
.When(x => !string.IsNullOrEmpty(x.ShortInfomation));
|
||||
|
||||
RuleFor(x => x.FullInformation)
|
||||
.NotEmpty().WithMessage("توضیحات کامل الزامی است")
|
||||
.MaximumLength(5000).WithMessage("توضیحات کامل نباید بیشتر از 5000 کاراکتر باشد");
|
||||
.MaximumLength(10000).WithMessage("توضیحات کامل نباید بیشتر از 10000 کاراکتر باشد")
|
||||
.When(x => !string.IsNullOrEmpty(x.FullInformation));
|
||||
|
||||
RuleFor(x => x.Price)
|
||||
.GreaterThan(0).WithMessage("قیمت باید بزرگتر از صفر باشد");
|
||||
@@ -27,14 +27,6 @@ public class UpdateDiscountProductCommandValidator : AbstractValidator<UpdateDis
|
||||
RuleFor(x => x.MaxDiscountPercent)
|
||||
.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)
|
||||
.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; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user