feat: Complete overhaul of FourSat documentation structure and content

- Added FINAL-STATUS.md detailing project completion and key metrics
- Created QUICK-REFERENCE.md for quick access to essential documents
- Updated README.md with project overview and quick start guide
- Established STRUCTURE.md outlining the final documentation structure
- Organized and archived old files, ensuring a clean and efficient directory
- Enhanced documentation quality with comprehensive metrics and checklists
This commit is contained in:
masoodafar-web
2025-12-04 17:32:31 +03:30
commit 119e870a26
67 changed files with 210873 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# BackOffice.BFF
BackOffice BFF
@@ -0,0 +1,593 @@
# BackOffice.BFF - CMS Integration Documentation
**Date**: 2025-11-30
**Status**: ✅ Integrated
**CMS Package Version**: 0.0.140
---
## 📋 Overview
BackOffice.BFF به CMS Microservice متصل شد و حالا می‌تواند به سرویس‌های Network-Club-Commission دسترسی داشته باشد.
این Integration به BackOffice امکان می‌دهد:
- مدیریت کامیسیون‌های کاربران
- مشاهده ساختار شبکه Binary Tree
- فعال/غیرفعال کردن عضویت باشگاه
- مشاهده گزارشات هفتگی کمیسیون
---
## 🏗️ Architecture
```
┌─────────────────────────────────────────────────────────┐
│ BackOffice.BFF (API Gateway) │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ IApplicationContractContext │ │
│ │ - Users (existing) │ │
│ │ - Products (existing) │ │
│ │ - Orders (existing) │ │
│ │ ✨ Commissions (NEW) │ │
│ │ ✨ NetworkMemberships (NEW) │ │
│ │ ✨ ClubMemberships (NEW) │ │
│ └──────────────────────────────────────────────────┘ │
│ ↓ gRPC │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ CMS Microservice │
│ https://cms.kbs1.ir │
│ │
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
│ │ CommissionContract │ │ NetworkMembershipContract│ │
│ │ - GetWeeklyPool │ │ - GetUserNetworkInfo │ │
│ │ - GetUserPayouts │ │ - GetNetworkTree │ │
│ │ - ProcessWithdrawal │ │ - CalculateLegBalances │ │
│ └─────────────────────┘ └─────────────────────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ ClubMembershipContract│ │
│ │ - ActivateClub │ │
│ │ - DeactivateClub │ │
│ │ - GetClubStatus │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## 📦 Integration Details
### 1️⃣ NuGet Package
**Package**: `Foursat.CMSMicroservice.Protobuf`
**Version**: `0.0.140` (Updated from 0.0.137)
**File**: `/BackOffice.BFF/src/BackOffice.BFF.Domain/BackOffice.BFF.Domain.csproj`
```xml
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.140" />
```
**What's New in 0.0.140**:
-`commission.proto` - Commission system contracts
-`networkmembership.proto` - Binary tree network contracts
-`clubmembership.proto` - Club membership contracts
-`configuration.proto` - System configuration contracts
---
### 2️⃣ Interface Definition
**File**: `/BackOffice.BFF/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs`
```csharp
public interface IApplicationContractContext
{
// ... existing services ...
// Network & Commission System (NEW)
CommissionContract.CommissionContractClient Commissions { get; }
NetworkMembershipContract.NetworkMembershipContractClient NetworkMemberships { get; }
ClubMembershipContract.ClubMembershipContractClient ClubMemberships { get; }
}
```
---
### 3️⃣ Implementation
**File**: `/BackOffice.BFF/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs`
```csharp
public class ApplicationContractContext : IApplicationContractContext
{
// ... existing implementations ...
// Network & Commission System
public CommissionContract.CommissionContractClient Commissions
=> GetService<CommissionContract.CommissionContractClient>();
public NetworkMembershipContract.NetworkMembershipContractClient NetworkMemberships
=> GetService<NetworkMembershipContract.NetworkMembershipContractClient>();
public ClubMembershipContract.ClubMembershipContractClient ClubMemberships
=> GetService<ClubMembershipContract.ClubMembershipContractClient>();
}
```
---
### 4️⃣ gRPC Configuration
**File**: `/BackOffice.BFF/src/BackOffice.BFF.WebApi/appsettings.json`
```json
{
"GrpcChannelOptions": {
"FMSMSAddress": "https://dl.afrino.co",
"CMSMSAddress": "https://cms.kbs1.ir"
}
}
```
**Auto-Registration**:
- gRPC clients به صورت خودکار توسط `ConfigureGrpcServices.BatchRegisterGrpcClients()` ثبت می‌شوند
- بر اساس نام Assembly (`CMSMicroservice.Protobuf`)
- با Address مشخص شده در `appsettings.json`
---
## 🔌 Available Services
### 1️⃣ CommissionContract
**Namespace**: `CMSMicroservice.Protobuf.Protos.Commission`
#### Commands:
```csharp
// محاسبه بالانس های هفتگی
await _context.Commissions.CalculateWeeklyBalancesAsync(
new CalculateWeeklyBalancesRequest { WeekNumber = "2025-W48" });
// محاسبه Pool هفتگی
await _context.Commissions.CalculateWeeklyCommissionPoolAsync(
new CalculateWeeklyCommissionPoolRequest { WeekNumber = "2025-W48" });
// پردازش Payout های کاربران
await _context.Commissions.ProcessUserPayoutsAsync(
new ProcessUserPayoutsRequest { WeekNumber = "2025-W48" });
// درخواست برداشت توسط کاربر
await _context.Commissions.RequestWithdrawalAsync(
new RequestWithdrawalRequest
{
UserId = 123,
Amount = 500000
});
// پردازش برداشت (توسط Admin)
await _context.Commissions.ProcessWithdrawalAsync(
new ProcessWithdrawalRequest
{
PayoutId = 456,
Status = WithdrawalStatus.Approved
});
```
#### Queries:
```csharp
// دریافت Pool هفتگی
var pool = await _context.Commissions.GetWeeklyCommissionPoolAsync(
new GetWeeklyCommissionPoolRequest { WeekNumber = "2025-W48" });
// دریافت Payout های یک کاربر
var payouts = await _context.Commissions.GetUserPayoutsAsync(
new GetUserPayoutsRequest
{
UserId = 123,
PageNumber = 1,
PageSize = 10
});
// دریافت تاریخچه Withdrawal ها
var withdrawals = await _context.Commissions.GetWithdrawalHistoryAsync(
new GetWithdrawalHistoryRequest
{
UserId = 123,
Status = WithdrawalStatus.Pending
});
```
---
### 2️⃣ NetworkMembershipContract
**Namespace**: `CMSMicroservice.Protobuf.Protos.NetworkMembership`
#### Queries:
```csharp
// دریافت اطلاعات شبکه یک کاربر
var networkInfo = await _context.NetworkMemberships.GetUserNetworkInfoAsync(
new GetUserNetworkInfoRequest { UserId = 123 });
// دریافت درخت شبکه (Binary Tree)
var tree = await _context.NetworkMemberships.GetNetworkTreeAsync(
new GetNetworkTreeRequest
{
RootUserId = 123,
MaxDepth = 5
});
// دریافت بالانس های هفتگی
var balances = await _context.NetworkMemberships.GetUserWeeklyBalancesAsync(
new GetUserWeeklyBalancesRequest
{
UserId = 123,
WeekNumber = "2025-W48"
});
// محاسبه بالانس Leg های یک کاربر
var legBalances = await _context.NetworkMemberships.CalculateLegBalancesAsync(
new CalculateLegBalancesRequest { UserId = 123 });
```
---
### 3️⃣ ClubMembershipContract
**Namespace**: `CMSMicroservice.Protobuf.Protos.ClubMembership`
#### Commands:
```csharp
// فعال کردن عضویت باشگاه
await _context.ClubMemberships.ActivateClubMembershipAsync(
new ActivateClubMembershipRequest
{
UserId = 123,
ActivationDate = Timestamp.FromDateTime(DateTime.UtcNow)
});
// غیرفعال کردن عضویت باشگاه
await _context.ClubMemberships.DeactivateClubMembershipAsync(
new DeactivateClubMembershipRequest
{
UserId = 123,
Reason = "User request"
});
```
#### Queries:
```csharp
// دریافت وضعیت عضویت باشگاه
var status = await _context.ClubMemberships.GetClubMembershipStatusAsync(
new GetClubMembershipStatusRequest { UserId = 123 });
// لیست تمام اعضای باشگاه
var members = await _context.ClubMemberships.GetAllClubMembersAsync(
new GetAllClubMembersRequest
{
IsActive = true,
PageNumber = 1,
PageSize = 20
});
```
---
## 📝 Usage Example in BFF
### Example 1: Create Commission Query Handler
**File**: `/BackOffice.BFF/src/BackOffice.BFF.Application/CommissionCQ/Queries/GetUserPayouts/GetUserPayoutsQuery.cs`
```csharp
public record GetUserPayoutsQuery : IRequest<GetUserPayoutsResponseDto>
{
public long UserId { get; init; }
public int PageNumber { get; init; } = 1;
public int PageSize { get; init; } = 10;
}
public class GetUserPayoutsQueryHandler
: IRequestHandler<GetUserPayoutsQuery, GetUserPayoutsResponseDto>
{
private readonly IApplicationContractContext _context;
public GetUserPayoutsQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetUserPayoutsResponseDto> Handle(
GetUserPayoutsQuery request,
CancellationToken cancellationToken)
{
var response = await _context.Commissions.GetUserPayoutsAsync(
request.Adapt<GetUserPayoutsRequest>(),
cancellationToken: cancellationToken);
return response.Adapt<GetUserPayoutsResponseDto>();
}
}
```
---
### Example 2: Create Network Tree Query Handler
**File**: `/BackOffice.BFF/src/BackOffice.BFF.Application/NetworkCQ/Queries/GetNetworkTree/GetNetworkTreeQuery.cs`
```csharp
public record GetNetworkTreeQuery : IRequest<GetNetworkTreeResponseDto>
{
public long RootUserId { get; init; }
public int MaxDepth { get; init; } = 5;
}
public class GetNetworkTreeQueryHandler
: IRequestHandler<GetNetworkTreeQuery, GetNetworkTreeResponseDto>
{
private readonly IApplicationContractContext _context;
public GetNetworkTreeQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetNetworkTreeResponseDto> Handle(
GetNetworkTreeQuery request,
CancellationToken cancellationToken)
{
var response = await _context.NetworkMemberships.GetNetworkTreeAsync(
request.Adapt<GetNetworkTreeRequest>(),
cancellationToken: cancellationToken);
return response.Adapt<GetNetworkTreeResponseDto>();
}
}
```
---
### Example 3: Create Club Activation Command Handler
**File**: `/BackOffice.BFF/src/BackOffice.BFF.Application/ClubCQ/Commands/ActivateClub/ActivateClubCommand.cs`
```csharp
public record ActivateClubCommand : IRequest<Unit>
{
public long UserId { get; init; }
public DateTimeOffset? ActivationDate { get; init; }
}
public class ActivateClubCommandHandler
: IRequestHandler<ActivateClubCommand, Unit>
{
private readonly IApplicationContractContext _context;
public ActivateClubCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> Handle(
ActivateClubCommand request,
CancellationToken cancellationToken)
{
await _context.ClubMemberships.ActivateClubMembershipAsync(
request.Adapt<ActivateClubMembershipRequest>(),
cancellationToken: cancellationToken);
return Unit.Value;
}
}
```
---
## 🔐 Authentication & Authorization
**JWT Token**:
- BackOffice.BFF به CMS با JWT Token متصل می‌شود
- Token از `ITokenProvider` گرفته می‌شود
- در Header با کلید `Authorization: Bearer {token}` ارسال می‌شود
**Implementation در `ConfigureGrpcServices.cs`**:
```csharp
private static async Task CallCredentials(
AuthInterceptorContext context,
Metadata metadata,
IServiceProvider serviceProvider)
{
var provider = serviceProvider.GetRequiredService<ITokenProvider>();
var token = await provider.GetTokenAsync();
metadata.Add("Authorization", $"Bearer {token}");
}
```
---
## 🧪 Testing
### Test Connection:
```csharp
// در یک Controller یا Handler:
var pool = await _context.Commissions.GetWeeklyCommissionPoolAsync(
new GetWeeklyCommissionPoolRequest { WeekNumber = "2025-W48" });
Console.WriteLine($"Total Pool Value: {pool.TotalPoolValue}");
Console.WriteLine($"Active Members: {pool.ActiveMembersCount}");
```
**Expected Output**:
```
Total Pool Value: 50000000
Active Members: 120
```
---
### Test with Postman/Swagger:
1. Start BackOffice.BFF:
```bash
cd /home/masoud/Apps/project/FourSat/BackOffice.BFF/src
dotnet run --project BackOffice.BFF.WebApi
```
2. Call API endpoint (example):
```http
GET /api/commission/weekly-pool?weekNumber=2025-W48
Authorization: Bearer {your-token}
```
3. Expected Response:
```json
{
"weekNumber": "2025-W48",
"totalPoolValue": 50000000,
"activeMembersCount": 120,
"isCalculated": true
}
```
---
## 📊 Status & Metrics
| Component | Status | Version | Notes |
|-----------|--------|---------|-------|
| **CMS Protobuf Package** | ✅ Active | 0.0.140 | With Network-Club-Commission |
| **gRPC Connection** | ✅ Configured | - | https://cms.kbs1.ir |
| **Auto-Registration** | ✅ Active | - | Via BatchRegisterGrpcClients |
| **Commission Client** | ✅ Ready | - | All commands & queries available |
| **Network Client** | ✅ Ready | - | Binary tree queries available |
| **Club Client** | ✅ Ready | - | Activation/Deactivation available |
| **Authentication** | ✅ Configured | JWT | Via ITokenProvider |
---
## 🔗 Related Documentation
### CMS Side:
- **Implementation Progress**: `/CMS/docs/implementation-progress.md`
- **Network System Design**: `/CMS/docs/network-club-commission-system.md`
- **Monitoring Setup**: `/CMS/docs/monitoring-alerts-consolidated-report.md`
- **Migration Guide**: `/CMS/docs/migration-network-parent-guide.md`
- **Binary Tree Registration**: `/CMS/docs/binary-tree-registration-guide.md`
### BFF Side:
- **This Document**: `/BackOffice.BFF/docs/cms-integration.md`
- **README**: `/BackOffice.BFF/README.md`
---
## 🚀 Next Steps
### Immediate:
1. ✅ Integration completed
2. ⏳ Create Commission/Network/Club CQRS handlers in BFF
3. ⏳ Add API endpoints in BackOffice.BFF.WebApi
4. ⏳ Test integration with real data
### Short-term:
5. ⏳ Add Swagger documentation for new endpoints
6. ⏳ Implement error handling for gRPC calls
7. ⏳ Add logging for commission operations
8. ⏳ Create admin dashboard for network visualization
### Long-term:
9. ⏳ Add real-time notifications (SignalR) for commission updates
10. ⏳ Implement caching for frequently accessed data
11. ⏳ Add reporting/analytics endpoints
12. ⏳ Performance optimization for large network trees
---
## 📞 Troubleshooting
### Issue 1: gRPC Connection Failed
**Error**: `Status(StatusCode="Unavailable", Detail="...")`
**Solutions**:
1. Check CMS service is running: `https://cms.kbs1.ir`
2. Verify network connectivity
3. Check firewall settings
4. Verify SSL certificate is valid
---
### Issue 2: Authentication Failed
**Error**: `Status(StatusCode="Unauthenticated", Detail="...")`
**Solutions**:
1. Verify `ITokenProvider` is registered in DI
2. Check JWT token is valid and not expired
3. Verify token has correct claims/permissions
4. Check Authorization header is being sent
---
### Issue 3: Package Version Mismatch
**Error**: `The type or namespace 'CommissionContract' could not be found`
**Solutions**:
1. Update package version in `.csproj`:
```xml
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.140" />
```
2. Run `dotnet restore`
3. Clean and rebuild solution
---
### Issue 4: Method Not Found
**Error**: `Method 'GetWeeklyPool' not found on service 'CommissionContract'`
**Solutions**:
1. Verify CMS service has the latest code deployed
2. Check Protobuf contract matches between CMS and BFF
3. Update both CMS and BFF to latest versions
4. Restart both services
---
## 🎯 Summary
### ✅ Completed:
- CMS Protobuf package updated to 0.0.140
- 3 new gRPC clients added to BFF:
* CommissionContract (8+ methods)
* NetworkMembershipContract (6+ methods)
* ClubMembershipContract (4+ methods)
- Auto-registration configured
- Authentication via JWT configured
- Build successful (0 errors)
### ⏳ Pending:
- Create CQRS handlers for Commission operations
- Create CQRS handlers for Network operations
- Create CQRS handlers for Club operations
- Add API Controllers/Endpoints
- Add Swagger documentation
- Integration testing
### 🔑 Key Points:
- **No manual registration needed**: gRPC clients auto-register via `BatchRegisterGrpcClients()`
- **Authentication handled**: JWT token automatically added to all requests
- **Type-safe**: All Protobuf contracts are strongly typed
- **Easy to use**: Simple interface via `IApplicationContractContext`
---
**Last Updated**: 2025-11-30
**Build Status**: ✅ Success
**Ready for**: Handler implementation & API endpoint creation
@@ -0,0 +1,600 @@
# BackOffice.BFF - Discount Shop Integration Plan
**تاریخ ایجاد**: 1403/09/13 (2024-12-04)
**وضعیت**: 📋 برنامه‌ریزی
**اولویت**: 🔴 بالا
---
## 📊 خلاصه وضعیت
### ✅ تکمیل شده در CMS
- **Phase 9: Club Discount Shop System** - 100% ✅
- 6 Entities (Category, Product, Cart, Order)
- 13 Commands + 6 Queries + 9 Validators
- 4 Proto Files (19 gRPC RPCs)
- 4 gRPC Services
- Migration: AddDiscountShopSystem
### ⏳ نیاز به پیاده‌سازی در BackOffice.BFF
- **19 Handler** برای 4 سرویس جدید
- **4 Client Interface** در IApplicationContractContext
- **Test و Validation**
---
## 🎯 امکانات جدید برای Admin Panel
### 1️⃣ مدیریت محصولات فروشگاه تخفیفی (5 API)
**سرویس**: `DiscountProductContract`
#### الف. ایجاد محصول جدید
- **Handler**: `CreateDiscountProductHandler`
- **Request**:
```csharp
- Title (عنوان محصول)
- ShortInfomation (توضیحات کوتاه)
- FullInformation (توضیحات کامل)
- Price (قیمت به تومان)
- MaxDiscountPercent (حداکثر درصد تخفیف قابل استفاده از کیف پول تخفیف - 0 تا 100)
- ImagePath (مسیر تصویر اصلی)
- ThumbnailPath (مسیر تصویر کوچک)
- InitialCount (تعداد اولیه موجودی)
- SortOrder (ترتیب نمایش)
- IsActive (فعال/غیرفعال)
- CategoryIds (لیست شناسه دسته‌بندی‌ها)
```
- **Response**: ProductId (شناسه محصول ایجاد شده)
- **کاربرد Admin**: ایجاد محصول جدید در فروشگاه تخفیفی
#### ب. ویرایش محصول
- **Handler**: `UpdateDiscountProductHandler`
- **Request**: همان فیلدهای بالا + ProductId
- **کاربرد Admin**: ویرایش اطلاعات محصول موجود
#### ج. حذف محصول
- **Handler**: `DeleteDiscountProductHandler`
- **Request**: ProductId
- **کاربرد Admin**: حذف محصول از فروشگاه
#### د. دریافت جزئیات محصول
- **Handler**: `GetDiscountProductByIdHandler`
- **Request**: ProductId
- **Response**: تمام اطلاعات محصول + لیست دسته‌بندی‌ها + موجودی باقی‌مانده
- **کاربرد Admin**: مشاهده جزئیات کامل یک محصول
#### ه. لیست محصولات با فیلتر
- **Handler**: `GetDiscountProductsHandler`
- **Request**:
```csharp
- CategoryId (nullable - فیلتر بر اساس دسته‌بندی)
- SearchQuery (nullable - جستجو در عنوان و توضیحات)
- MinPrice (nullable - حداقل قیمت)
- MaxPrice (nullable - حداکثر قیمت)
- IsActive (nullable - فیلتر فعال/غیرفعال)
- InStock (nullable - فقط موجود در انبار)
- PageNumber (شماره صفحه)
- PageSize (تعداد آیتم در صفحه)
```
- **Response**:
```csharp
- MetaData (اطلاعات صفحه‌بندی)
- Models (لیست محصولات)
```
- **کاربرد Admin**: مدیریت و جستجوی محصولات
---
### 2️⃣ مدیریت دسته‌بندی محصولات (4 API)
**سرویس**: `DiscountCategoryContract`
#### الف. ایجاد دسته‌بندی جدید
- **Handler**: `CreateDiscountCategoryHandler`
- **Request**:
```csharp
- Name (نام لاتین برای URL)
- Title (عنوان فارسی)
- Description (nullable - توضیحات)
- ImagePath (nullable - تصویر دسته‌بندی)
- ParentCategoryId (nullable - دسته‌بندی والد برای ساختار درختی)
- SortOrder (ترتیب نمایش)
- IsActive (فعال/غیرفعال)
```
- **Response**: CategoryId
- **کاربرد Admin**: ایجاد دسته‌بندی جدید (با قابلیت ساختار چند سطحی)
#### ب. ویرایش دسته‌بندی
- **Handler**: `UpdateDiscountCategoryHandler`
- **Request**: همان فیلدهای بالا + CategoryId
- **کاربرد Admin**: ویرایش دسته‌بندی موجود
#### ج. حذف دسته‌بندی
- **Handler**: `DeleteDiscountCategoryHandler`
- **Request**: CategoryId
- **Response**: Success/Failure
- **Logic**:
- چک می‌کند اگر این دسته‌بندی زیرمجموعه دارد → خطا
- چک می‌کند اگر محصولی به این دسته‌بندی متصل است → خطا
- در غیر این صورت حذف می‌شود
- **کاربرد Admin**: حذف ایمن دسته‌بندی
#### د. دریافت درخت دسته‌بندی‌ها
- **Handler**: `GetDiscountCategoriesHandler`
- **Request**:
```csharp
- ParentCategoryId (nullable)
* اگر null باشد: دسته‌بندی‌های ریشه (Root) برگردانده می‌شود
* اگر مقدار داشته باشد: زیرمجموعه‌های آن دسته‌بندی برگردانده می‌شود
- IsActive (nullable - فیلتر فعال/غیرفعال)
```
- **Response**:
```csharp
- List<DiscountCategoryDto> (ساختار recursive با Children)
```
- **کاربرد Admin**: مشاهده ساختار درختی دسته‌بندی‌ها
---
### 3️⃣ مدیریت سبد خرید کاربران (5 API)
**سرویس**: `DiscountShoppingCartContract`
> **توجه**: این APIها در Admin Panel کمتر استفاده می‌شوند، اما برای Support و Troubleshooting مفید هستند.
#### الف. افزودن به سبد خرید (Support)
- **Handler**: `AddToCartHandler`
- **Request**: UserId, ProductId, Count
- **کاربرد Admin**: کمک به کاربر در افزودن محصول به سبد (Support)
#### ب. حذف از سبد خرید (Support)
- **Handler**: `RemoveFromCartHandler`
- **Request**: UserId, ProductId
- **کاربرد Admin**: کمک به کاربر در حذف آیتم از سبد
#### ج. تغییر تعداد آیتم (Support)
- **Handler**: `UpdateCartItemCountHandler`
- **Request**: UserId, ProductId, NewCount
- **کاربرد Admin**: اصلاح تعداد آیتم در سبد کاربر
#### د. مشاهده سبد خرید کاربر
- **Handler**: `GetUserCartHandler`
- **Request**: UserId
- **Response**:
```csharp
- List<CartItemDto>
* ProductId
* ProductTitle
* ProductImagePath
* UnitPrice (قیمت واحد)
* MaxDiscountPercent
* Count (تعداد)
* TotalPrice (قیمت کل = UnitPrice × Count)
* DiscountAmount (مقدار تخفیف قابل استفاده)
* FinalPrice (قیمت نهایی بعد از تخفیف)
* ProductRemainingCount (موجودی باقی‌مانده)
- TotalPrice (مجموع قیمت کل سبد)
- TotalDiscountAmount (مجموع تخفیف قابل استفاده)
- FinalPrice (مجموع قیمت نهایی)
```
- **کاربرد Admin**: بررسی سبد خرید کاربر برای Support
#### ه. پاک کردن سبد خرید
- **Handler**: `ClearCartHandler`
- **Request**: UserId
- **کاربرد Admin**: پاک کردن کامل سبد خرید کاربر (در صورت نیاز)
---
### 4️⃣ مدیریت سفارشات فروشگاه تخفیفی (5 API)
**سرویس**: `DiscountOrderContract`
#### الف. ثبت سفارش (کمتر استفاده می‌شود در Admin)
- **Handler**: `PlaceOrderHandler`
- **Request**:
```csharp
- UserId
- UserAddressId
- DiscountBalanceToUse (مقدار کیف پول تخفیف برای استفاده)
- Notes (nullable - یادداشت)
```
- **Response**:
```csharp
- Success
- Message
- OrderId
- GatewayAmount (مبلغ باقی‌مانده برای پرداخت از طریق درگاه)
- PaymentUrl (nullable - لینک پرداخت)
```
- **کاربرد Admin**: ثبت سفارش دستی برای کاربر (نادر)
#### ب. تکمیل پرداخت سفارش (کمتر استفاده می‌شود)
- **Handler**: `CompleteOrderPaymentHandler`
- **Request**: OrderId, TransactionId, PaymentSuccess
- **کاربرد Admin**: تایید دستی پرداخت (در صورت مشکل)
#### ج. تغییر وضعیت ارسال سفارش ⭐ **مهم**
- **Handler**: `UpdateOrderStatusHandler`
- **Request**:
```csharp
- OrderId
- NewStatus (enum: Pending, Processing, Shipped, Delivered, Cancelled)
- TrackingCode (nullable - کد رهگیری پست)
- AdminNotes (nullable - یادداشت ادمین)
```
- **Response**: Success, Message
- **کاربرد Admin**:
- تغییر وضعیت سفارش به "در حال پردازش"
- ثبت کد رهگیری پست
- تغییر وضعیت به "ارسال شده"
- تایید تحویل
- لغو سفارش
#### د. مشاهده جزئیات سفارش ⭐ **مهم**
- **Handler**: `GetOrderByIdHandler`
- **Request**: OrderId
- **Response**:
```csharp
- OrderId
- UserId
- UserName (nullable)
- Address (AddressInfo)
* Title
* Address
* PostalCode
- OrderItems (List)
* ProductId
* ProductTitle
* ProductPrice (قیمت اسنپ‌شات در زمان خرید)
* MaxDiscountPercent
* Count
* TotalPrice
* DiscountAmount
* FinalPrice
- TotalPrice (مجموع قیمت)
- DiscountBalanceUsed (مقدار استفاده شده از کیف پول تخفیف)
- GatewayAmount (مبلغ پرداخت شده از درگاه)
- PaymentTransactionId (nullable)
- DeliveryStatus (enum)
- TrackingCode (nullable)
- AdminNotes (nullable)
- Notes (یادداشت کاربر)
- OrderDate
- PaymentDate (nullable)
```
- **کاربرد Admin**: بررسی کامل سفارش
#### ه. لیست سفارشات کاربر ⭐ **مهم**
- **Handler**: `GetUserOrdersHandler`
- **Request**:
```csharp
- UserId
- PageNumber
- PageSize
```
- **Response**:
```csharp
- MetaData (صفحه‌بندی)
- Models (List<OrderSummaryDto>)
* OrderId
* TotalPrice
* DiscountBalanceUsed
* GatewayAmount
* DeliveryStatus
* ItemsCount (تعداد آیتم‌های سفارش)
* OrderDate
```
- **کاربرد Admin**: مشاهده تاریخچه سفارشات کاربر
---
## 📋 لیست کامل Handlerهای مورد نیاز
### ✅ موجود در BackOffice.BFF (35 Handler)
1. User Management (7)
2. Product Management (5)
3. Order Management (5)
4. Category/Tag (4)
5. Role & Permission (3)
6. Commission System (4)
7. Network Membership (3)
8. Club Membership (4)
### ⏳ نیاز به ایجاد (19 Handler)
#### گروه 1: Discount Product (5 Handlers)
1. ✅ `CreateDiscountProductHandler`
2. ✅ `UpdateDiscountProductHandler`
3. ✅ `DeleteDiscountProductHandler`
4. ✅ `GetDiscountProductByIdHandler`
5. ✅ `GetDiscountProductsHandler`
#### گروه 2: Discount Category (4 Handlers)
6. ✅ `CreateDiscountCategoryHandler`
7. ✅ `UpdateDiscountCategoryHandler`
8. ✅ `DeleteDiscountCategoryHandler`
9. ✅ `GetDiscountCategoriesHandler`
#### گروه 3: Discount Shopping Cart (5 Handlers)
10. ✅ `AddToCartHandler` (برای Support)
11. ✅ `RemoveFromCartHandler` (برای Support)
12. ✅ `UpdateCartItemCountHandler` (برای Support)
13. ✅ `GetUserCartHandler` ⭐
14. ✅ `ClearCartHandler`
#### گروه 4: Discount Order (5 Handlers)
15. ✅ `PlaceOrderHandler` (کمتر استفاده می‌شود)
16. ✅ `CompleteOrderPaymentHandler` (کمتر استفاده می‌شود)
17. ✅ `UpdateOrderStatusHandler` ⭐⭐⭐ **خیلی مهم**
18. ✅ `GetOrderByIdHandler` ⭐⭐⭐ **خیلی مهم**
19. ✅ `GetUserOrdersHandler` ⭐⭐ **مهم**
---
## 🏗️ تغییرات مورد نیاز در BackOffice.BFF
### 1️⃣ آپدیت IApplicationContractContext
**فایل**: `/BackOffice.BFF/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs`
```csharp
public interface IApplicationContractContext
{
// ... existing services ...
// Discount Shop System (NEW - Phase 9)
DiscountProductContract.DiscountProductContractClient DiscountProducts { get; }
DiscountCategoryContract.DiscountCategoryContractClient DiscountCategories { get; }
DiscountShoppingCartContract.DiscountShoppingCartContractClient DiscountShoppingCarts { get; }
DiscountOrderContract.DiscountOrderContractClient DiscountOrders { get; }
}
```
### 2️⃣ پیاده‌سازی در ApplicationContractContext
**فایل**: `/BackOffice.BFF/src/BackOffice.BFF.Infrastructure/Persistence/ApplicationContractContext.cs`
```csharp
public class ApplicationContractContext : IApplicationContractContext
{
// ... existing implementations ...
// Discount Shop System (NEW)
public DiscountProductContract.DiscountProductContractClient DiscountProducts { get; }
public DiscountCategoryContract.DiscountCategoryContractClient DiscountCategories { get; }
public DiscountShoppingCartContract.DiscountShoppingCartContractClient DiscountShoppingCarts { get; }
public DiscountOrderContract.DiscountOrderContractClient DiscountOrders { get; }
public ApplicationContractContext(GrpcChannel channel)
{
// ... existing initializations ...
// Discount Shop System
DiscountProducts = new DiscountProductContract.DiscountProductContractClient(channel);
DiscountCategories = new DiscountCategoryContract.DiscountCategoryContractClient(channel);
DiscountShoppingCarts = new DiscountShoppingCartContract.DiscountShoppingCartContractClient(channel);
DiscountOrders = new DiscountOrderContract.DiscountOrderContractClient(channel);
}
}
```
### 3️⃣ ایجاد Handlerها
**ساختار فولدر**:
```
BackOffice.BFF.Application/
└── DiscountShop/
├── Products/
│ ├── CreateDiscountProduct/
│ │ ├── CreateDiscountProductCommand.cs
│ │ └── CreateDiscountProductHandler.cs
│ ├── UpdateDiscountProduct/
│ ├── DeleteDiscountProduct/
│ ├── GetDiscountProductById/
│ └── GetDiscountProducts/
├── Categories/
│ ├── CreateDiscountCategory/
│ ├── UpdateDiscountCategory/
│ ├── DeleteDiscountCategory/
│ └── GetDiscountCategories/
├── Cart/
│ ├── AddToCart/
│ ├── RemoveFromCart/
│ ├── UpdateCartItemCount/
│ ├── GetUserCart/
│ └── ClearCart/
└── Orders/
├── PlaceOrder/
├── CompleteOrderPayment/
├── UpdateOrderStatus/
├── GetOrderById/
└── GetUserOrders/
```
---
## 🎨 UI Pages مورد نیاز در BackOffice
### صفحات جدید (6 صفحه)
1. **صفحه لیست محصولات فروشگاه تخفیفی** (2 روز)
- `Pages/DiscountShop/Products/ProductsList.razor`
- DataGrid با فیلترها
- دکمه‌های Create, Edit, Delete
- نمایش موجودی و MaxDiscountPercent
2. **صفحه ایجاد/ویرایش محصول** (1 روز)
- `Pages/DiscountShop/Products/ProductForm.razor`
- فرم کامل با تمام فیلدها
- انتخاب چندتایی دسته‌بندی
- آپلود تصویر
3. **صفحه مدیریت دسته‌بندی‌ها** (1.5 روز)
- `Pages/DiscountShop/Categories/CategoriesList.razor`
- نمایش درختی (Tree View)
- قابلیت Drag & Drop برای تغییر Parent
- Dialog ایجاد/ویرایش
4. **صفحه لیست سفارشات فروشگاه** (2 روز)
- `Pages/DiscountShop/Orders/OrdersList.razor`
- DataGrid با فیلترها (Status, Date Range, User)
- نمایش خلاصه: TotalPrice, DiscountUsed, GatewayAmount
- دکمه View Details
5. **صفحه جزئیات سفارش** (1.5 روز)
- `Pages/DiscountShop/Orders/OrderDetails.razor`
- نمایش کامل اطلاعات سفارش
- لیست آیتم‌های سفارش
- **تغییر وضعیت ارسال** (Dropdown)
- ثبت کد رهگیری
- یادداشت ادمین
6. **صفحه مشاهده سبد خرید کاربر** (1 روز)
- `Pages/DiscountShop/Support/UserCart.razor`
- برای Support و Troubleshooting
- نمایش محاسبات تخفیف
- قابلیت اصلاح (Add/Remove/Update)
**جمع زمان UI**: **9 روز**
---
## 📊 گزارشات مالی جدید (اختیاری - اولویت متوسط)
### گزارشات پیشنهادی:
1. **گزارش فروش فروشگاه تخفیفی**
- مجموع فروش (TotalPrice)
- مجموع تخفیف استفاده شده (DiscountBalanceUsed)
- مجموع پرداخت از درگاه (GatewayAmount)
- تفکیک بر اساس تاریخ، محصول، دسته‌بندی
2. **گزارش محبوب‌ترین محصولات**
- تعداد فروش هر محصول
- مجموع درآمد
- میانگین استفاده از تخفیف
3. **گزارش وضعیت موجودی**
- محصولات کم موجودی (RemainingCount < حد آستانه)
- هشدار اتمام موجودی
4. **گزارش استفاده از کیف پول تخفیف**
- کاربران برتر در استفاده از تخفیف
- میانگین درصد استفاده از تخفیف
- مقایسه با فروش کل
---
## ⏱️ تخمین زمان پیاده‌سازی
### BackOffice.BFF (Backend)
| مرحله | زمان | توضیحات |
|-------|------|---------|
| آپدیت Interface & Context | 30 دقیقه | اضافه کردن 4 Client |
| ایجاد 19 Handler | 3 روز | ~20 دقیقه هر Handler |
| Test & Debug | 1 روز | تست تمام Handlerها |
| **جمع** | **4 روز** | |
### BackOffice UI (Frontend)
| مرحله | زمان | توضیحات |
|-------|------|---------|
| صفحات محصولات (2 صفحه) | 3 روز | List + Form |
| صفحات دسته‌بندی (1 صفحه) | 1.5 روز | Tree View |
| صفحات سفارشات (2 صفحه) | 3.5 روز | List + Details |
| صفحه Support (سبد خرید) | 1 روز | |
| **جمع** | **9 روز** | |
### **جمع کل**: **13 روز کاری** (~2.5 هفته)
---
## 🚀 اولویت‌بندی پیاده‌سازی
### فاز 1: حداقل قابل استفاده (MVP) - 5 روز
✅ **اولویت بالا**
1. Handlerهای مدیریت محصولات (5)
2. Handlerهای مدیریت دسته‌بندی (4)
3. Handler مشاهده جزئیات سفارش (1)
4. Handler تغییر وضعیت سفارش (1)
5. صفحه لیست محصولات + فرم
6. صفحه لیست سفارشات + جزئیات
### فاز 2: قابلیت‌های Support - 3 روز
🟡 **اولویت متوسط**
1. Handlerهای سبد خرید (5)
2. Handler لیست سفارشات کاربر (1)
3. صفحه مدیریت دسته‌بندی
4. صفحه Support سبد خرید
### فاز 3: گزارشات و آمار - 5 روز
🟢 **اولویت پایین** (می‌تواند بعداً اضافه شود)
1. گزارشات مالی
2. داشبورد فروش فروشگاه
3. چارت‌های تحلیلی
---
## 📝 نکات مهم
### ⚠️ نکته 1: MaxDiscountPercent
این فیلد بسیار مهم است:
- مشخص می‌کند کاربر حداکثر چند درصد از قیمت محصول را می‌تواند با کیف پول تخفیف پرداخت کند
- مثال: قیمت = 1,000,000 تومان، MaxDiscountPercent = 70%
- حداکثر تخفیف: 700,000 تومان
- مبلغ باقی‌مانده (300,000 تومان) باید از درگاه پرداخت شود
### ⚠️ نکته 2: Snapshot محصول
وقتی سفارش ثبت می‌شود، اطلاعات محصول (عنوان، قیمت، MaxDiscountPercent) در جدول `DiscountOrderItem` ذخیره می‌شود:
- این اطلاعات Snapshot هستند و حتی اگر محصول بعداً ویرایش شود، سفارش تغییر نمی‌کند
- برای گزارش‌گیری دقیق مالی ضروری است
### ⚠️ نکته 3: Hybrid Payment Flow
جریان پرداخت ترکیبی:
1. کاربر سفارش ثبت می‌کند → `PlaceOrder`
2. CMS محاسبه می‌کند چقدر از کیف پول تخفیف استفاده شود
3. مبلغ باقی‌مانده (GatewayAmount) به کاربر نمایش داده می‌شود
4. کاربر به درگاه پرداخت می‌رود
5. بعد از بازگشت از درگاه → `CompleteOrderPayment`
6. CMS تراکنش را Verify می‌کند و DiscountBalance را کم می‌کند
### ⚠️ نکته 4: Stock Management
- هنگام `PlaceOrder`: RemainingCount کم می‌شود (Reserve)
- اگر پرداخت ناموفق باشد: باید موجودی برگردانده شود (در CompleteOrderPayment)
- Admin باید بتواند موجودی را دستی تغییر دهد
---
## 📚 مستندات مرتبط
- [CMS Implementation Progress](../CMS/implementation-progress.md) - Phase 9 Details
- [REMAINING-TASKS-CONSOLIDATED](../REMAINING-TASKS-CONSOLIDATED.md) - Overall Project Status
- [BackOffice.BFF CMS Integration](./cms-integration.md) - Existing Integration Guide
---
## ✅ Checklist پیاده‌سازی
### Backend (BackOffice.BFF)
- [ ] آپدیت IApplicationContractContext (4 Client)
- [ ] آپدیت ApplicationContractContext (Implementation)
- [ ] ایجاد 5 Handler محصولات
- [ ] ایجاد 4 Handler دسته‌بندی
- [ ] ایجاد 5 Handler سبد خرید
- [ ] ایجاد 5 Handler سفارشات
- [ ] تست تمام Handlerها
- [ ] آپدیت مستندات cms-integration.md
### Frontend (BackOffice UI)
- [ ] صفحه لیست محصولات
- [ ] صفحه فرم محصول (Create/Edit)
- [ ] صفحه مدیریت دسته‌بندی‌ها
- [ ] صفحه لیست سفارشات
- [ ] صفحه جزئیات سفارش
- [ ] صفحه Support سبد خرید
- [ ] تست UI با داده واقعی
---
**آماده شروع پیاده‌سازی؟** 🚀
+32
View File
@@ -0,0 +1,32 @@
# 📁 BackOffice.BFF - Design Files
این پوشه شامل فایل‌های طراحی BackOffice.BFF است.
---
## 📊 فایل‌ها
### Database Models:
- **`model.ndm2`** - طراحی دیتابیس BackOffice.BFF
- ابزار: Navicat Data Modeler
- محتوا: ساختار Entity ها و روابط
---
## 🔧 نحوه استفاده
```bash
# باز کردن با Navicat Data Modeler
navicat-data-modeler model.ndm2
```
---
## 🔗 مراجع
- **Handlers Status**: [`../handlers-status.md`](../handlers-status.md)
- **CMS Integration**: [`../cms-integration.md`](../cms-integration.md)
---
**تاریخ ایجاد**: ۱ دسامبر ۲۰۲۵
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+258
View File
@@ -0,0 +1,258 @@
# CMS Microservice - Network & Club Commission System
[![Status](https://img.shields.io/badge/Status-Production%20Ready-success)]()
[![Progress](https://img.shields.io/badge/Progress-85%25-blue)]()
[![MVP](https://img.shields.io/badge/MVP-100%25%20Complete-brightgreen)]()
## 📊 Project Status (2025-12-01)
**Overall Progress**: 85% Complete (7/10 phases)
**Production Readiness**: 95%
**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
### 🟡 Partially Complete (1)
- Phase 10: Withdrawal & Settlement (40%)
- ✅ Commands & Database
- ❌ Payment Gateway Integration
### ❌ Not Started (1)
- Phase 9: Club Shop & Product Integration (0%)
### ⏸️ Postponed (1)
- Phase 7: Testing (Unit, Integration, Load tests)
---
## 🚀 Recent Updates (2025-12-01)
### Email & SMS Notifications - COMPLETED ✅
-**MailKit 4.14.1** for Email (SMTP with HTML templates)
-**Kavenegar 1.2.5** for SMS (Iranian SMS gateway)
- ✅ User.Email field added with migration
- ✅ 3 notification types: Commission, Club activation, Errors
- ✅ Persian RTL templates with rich formatting
- ✅ Production configuration guide created
### Hangfire Job Scheduling - COMPLETED ✅
- ✅ Dashboard UI at `/hangfire`
- ✅ Cron schedule: Sunday 00:05 UTC
- ✅ SQL Server persistence
- ✅ Manual trigger API endpoints
- ✅ Distributed execution support
### Infrastructure Enhancements - COMPLETED ✅
- ✅ Health Check endpoints (`/health`, `/health/ready`, `/health/live`)
- ✅ AlertService (structured logging for Sentry/Slack)
- ✅ Retry logic (Polly 8.5.0 with exponential backoff)
- ✅ WorkerExecutionLog (database audit trail)
- ✅ CurrentUserService (JWT authentication context)
---
## 🏗️ Architecture
**Clean Architecture** with 4 layers:
```
CMSMicroservice.Domain/ # Entities, Enums, Interfaces
CMSMicroservice.Application/ # CQRS (Commands, Queries, MediatR)
CMSMicroservice.Infrastructure/ # DbContext, Services, Background Jobs
CMSMicroservice.WebApi/ # gRPC Services, Controllers
CMSMicroservice.Protobuf/ # Protocol Buffers definitions
```
**Technology Stack**:
- .NET 9.0
- Entity Framework Core 9.0.11
- gRPC + JSON Transcoding
- Hangfire 1.8.22 (Job Scheduling)
- MediatR 13.0.0 (CQRS)
- Polly 8.5.0 (Resilience)
- MailKit 4.14.1 (Email)
- Kavenegar 1.2.5 (SMS)
- SQL Server
---
## 📖 Documentation
- **[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
- **[Binary Tree Registration](docs/binary-tree-registration-guide.md)** - Network tree guide
- **[Network Club Commission System](docs/network-club-commission-system-v1.1.md)** - Full system specification
---
## 🚀 Quick Start
### Prerequisites
- .NET 9.0 SDK
- SQL Server (local or remote)
- (Optional) Gmail account for Email
- (Optional) Kavenegar account for SMS
### 1. Clone & Build
```bash
cd /home/masoud/Apps/project/FourSat/CMS/src
dotnet build
```
### 2. Configure Database
Update `appsettings.json` with your SQL Server connection:
```json
"ConnectionStrings": {
"DefaultConnection": "Server=YOUR_SERVER;Database=Foursat_CMS;..."
}
```
### 3. Apply Migrations
```bash
cd CMSMicroservice.WebApi
dotnet ef database update
```
### 4. Configure Notifications (Optional)
See [Email/SMS Configuration Guide](docs/email-sms-configuration-guide.md)
### 5. Run
```bash
dotnet run --urls="http://localhost:5133"
```
### 6. Access Endpoints
- **Health**: http://localhost:5133/health
- **Hangfire Dashboard**: http://localhost:5133/hangfire
- **gRPC**: localhost:5133 (HTTP/2)
---
## 🔧 Configuration
### Email (SMTP)
```json
"Email": {
"Enabled": true,
"SmtpHost": "smtp.gmail.com",
"SmtpPort": 587,
"SmtpUsername": "your-email@gmail.com",
"SmtpPassword": "your-gmail-app-password",
"FromEmail": "noreply@foursat.com",
"FromName": "FourSat CMS",
"EnableSsl": true
}
```
### SMS (Kavenegar)
```json
"Sms": {
"Enabled": true,
"Provider": "Kavenegar",
"KavenegarApiKey": "YOUR_API_KEY",
"Sender": "10008663"
}
```
### Background Worker
```csharp
// Cron: "5 0 * * 0" = Every Sunday at 00:05 UTC
RecurringJob.AddOrUpdate<WeeklyCommissionJob>(
"weekly-commission-calculation",
job => job.ExecuteAsync(CancellationToken.None),
"5 0 * * 0");
```
---
## 🧪 Testing
### Manual Trigger (via API)
```bash
# Trigger weekly calculation immediately
curl -X POST http://localhost:5133/api/admin/trigger-weekly-calculation
# Trigger recurring job now
curl -X POST http://localhost:5133/api/admin/trigger-recurring-job-now
# Get recurring jobs status
curl http://localhost:5133/api/admin/recurring-jobs-status
```
### Health Checks
```bash
curl http://localhost:5133/health # Overall health
curl http://localhost:5133/health/ready # Readiness probe (K8s)
curl http://localhost:5133/health/live # Liveness probe (K8s)
```
---
## 📊 What's Remaining?
### High Priority
1. **Payment Gateway Integration** (Phase 10 - 1 week)
- Daya API integration (فقط برای Payout)
- IBAN transfer automation
- Admin approval UI in BackOffice
2. **Production Configuration** (30 minutes)
- Gmail App Password setup
- Kavenegar API key registration
- Update `appsettings.Production.json`
### Medium Priority
3. **Club Shop Integration** (Phase 9 - 2 weeks)
- Product catalog for club memberships
- Shopping cart integration
- Auto-activation on purchase
### Low Priority
4. **Testing** (Phase 7 - Postponed)
- Unit tests for business logic
- Integration tests for API
- Load testing for background worker
### Optional Enhancements
- Redis distributed locks (multi-server deployment)
- Sentry error tracking (API key needed)
- Slack notifications (webhook needed)
- FCM push notifications
---
## 🎯 MVP Features (100% Complete)
✅ Binary network tree with automatic placement
✅ Club membership (Member/Trial) with different commission rates
✅ Weekly commission calculation (Lesser Leg algorithm)
✅ Background worker with Hangfire (cron scheduling)
✅ Balance carryover logic (rollover unused volumes)
✅ MaxWeeklyBalances cap enforcement
✅ Health check endpoints (Kubernetes-ready)
✅ Manual trigger API (admin control)
✅ Email + SMS notifications (MailKit + Kavenegar)
✅ Retry logic with exponential backoff (Polly)
✅ Audit trail (WorkerExecutionLog, History tables)
✅ Structured logging (AlertService for Sentry/Slack)
✅ JWT authentication context (CurrentUserService)
---
## 👥 Team
**Development**: FourSat Team
**Last Updated**: 2025-12-01
---
## 📝 License
Proprietary - FourSat Company
+411
View File
@@ -0,0 +1,411 @@
# CMS API Coverage - مقایسه CMS با BackOffice.BFF
**تاریخ بررسی**: 2025-12-01
**هدف**: شناسایی APIهای CMS که در BackOffice.BFF پوشش داده نشده‌اند
---
## 📊 خلاصه وضعیت
| دسته | تعداد Proto در CMS | پوشش در BFF | وضعیت |
|------|-------------------|--------------|--------|
| **User Management** | 1 | ✅ کامل | 100% |
| **Network & Tree** | 1 | ✅ کامل | 100% |
| **Club Membership** | 1 | ✅ کامل | 100% |
| **Commission & Wallet** | 3 | ✅ کامل | 100% |
| **Products** | 6 | ⚠️ جزئی | 70% |
| **Orders** | 2 | ⚠️ جزئی | 60% |
| **Configuration** | 1 | ✅ کامل | 100% |
| **Roles & Permissions** | 2 | ✅ کامل | 100% |
| **Cart** | 1 | ❌ خیر | 0% |
| **Transactions** | 1 | ❌ خیر | 0% |
| **Contracts** | 2 | ❌ خیر | 0% |
| **OTP** | 1 | ✅ کامل | 100% |
| **Public Messages** | 1 | ❌ خیر | 0% |
---
## ✅ APIهای کامل پوشش داده شده (در BFF موجود است)
### 1. User Management (`user.proto`)
- ✅ CreateNewUserCommand
- ✅ UpdateUserCommand
- ✅ DeleteUserCommand
- ✅ GetAllUserByFilterQuery
- ✅ GetUserQuery
- ✅ SendOtpCommand
- ✅ VerifyOtpCodeCommand
**تنظیمات دسترسی پیشنهادی**:
- SuperAdmin: همه عملیات
- Admin: Create, Update, GetAll, Get (بدون Delete)
- Inspector: فقط GetAll, Get
---
### 2. Network Management (`networkmembership.proto`)
- ✅ GetNetworkTreeQuery
- ✅ GetNetworkHistoryQuery
- ✅ GetNetworkStatisticsQuery
- ✅ GetUserNetworkInfoQuery
**تنظیمات دسترسی پیشنهادی**:
- SuperAdmin: همه
- Admin: همه
- Inspector: فقط مشاهده (همه)
---
### 3. Club Membership (`clubmembership.proto`)
- ✅ ActivateClubCommand
- ✅ GetAllClubMembersQuery
- ✅ GetClubStatisticsQuery
**تنظیمات دسترسی پیشنهادی**:
- SuperAdmin: همه
- Admin: ActivateClub, GetAll, Get
- Inspector: فقط GetAll, Get
---
### 4. Commission & Balance (`commission.proto`, `userwallet.proto`, `userwalletchangelog.proto`)
- ✅ GetAllWeeklyPoolsQuery
- ✅ GetWeeklyPoolQuery
- ✅ GetUserWeeklyBalancesQuery
- ✅ GetUserPayoutsQuery
- ✅ ApproveWithdrawalCommand
- ✅ RejectWithdrawalCommand
- ✅ ProcessWithdrawalCommand
- ✅ GetWithdrawalRequestsQuery
- ✅ TriggerWeeklyCalculationCommand (Worker)
- ✅ GetWorkerStatusQuery
- ✅ GetWorkerExecutionLogsQuery
**تنظیمات دسترسی پیشنهادی**:
- SuperAdmin: همه عملیات + Trigger Worker
- Admin: Approve/Reject Withdrawal, GetAll queries
- Inspector: فقط Get queries (بدون Approve/Reject)
---
### 5. Configuration (`configuration.proto`)
- ✅ CreateOrUpdateConfigurationCommand
- ✅ DeactivateConfigurationCommand
- ✅ GetAllConfigurationsQuery
**تنظیمات دسترسی پیشنهادی**:
- SuperAdmin: همه عملیات
- Admin: فقط GetAll (بدون Update)
- Inspector: فقط GetAll
---
### 6. Roles & Permissions (`role.proto`, `userrole.proto`)
- ✅ CreateNewRoleCommand
- ✅ UpdateRoleCommand
- ✅ DeleteRoleCommand
- ✅ GetAllRoleByFilterQuery
- ✅ GetRoleQuery
- ✅ CreateNewUserRoleCommand
- ✅ UpdateUserRoleCommand
- ✅ DeleteUserRoleCommand
- ✅ GetAllUserRoleByFilterQuery
- ✅ GetUserRoleQuery
**تنظیمات دسترسی پیشنهادی**:
- SuperAdmin: همه عملیات
- Admin: ❌ هیچ دسترسی (فقط SuperAdmin)
- Inspector: ❌ هیچ دسترسی
---
## ⚠️ APIهای جزئی پوشش داده شده
### 7. Products (`products.proto`, `category.proto`, `tag.proto`, `package.proto`, `productgallerys.proto`, `productimages.proto`)
#### ✅ موجود در BFF:
- CreateNewProductsCommand
- UpdateProductsCommand
- DeleteProductsCommand
- GetAllProductsByFilterQuery
- GetProductsQuery
- GetProductsForCategoryQuery
- AddProductImageCommand
- RemoveProductImageCommand
- GetProductGalleryQuery
#### ⚠️ موجود در CMS ولی نه در BFF:
```
Products:
- BulkUpdateProductsCommand (به‌روزرسانی دسته‌ای)
- GetProductBySkuQuery (جستجو با SKU)
- ToggleProductStatusCommand (فعال/غیرفعال)
- GetLowStockProductsQuery (محصولات کم موجودی)
Category:
- CreateNewCategoryCommand ✅
- UpdateCategoryCommand ✅
- DeleteCategoryCommand ✅
- GetAllCategoryByFilterQuery ✅
- GetCategoriesQuery ✅
- GetCategoryQuery ✅
- UpdateCategoryProductsCommand ✅ (ارتباط Product-Category)
- UpdateProductCategoriesCommand ✅
Tags:
- CreateTagCommand ❌
- UpdateTagCommand ❌
- DeleteTagCommand ❌
- GetAllTagsQuery ❌
- AssignTagToProductCommand ❌ (ارتباط Product-Tag)
Package (بسته‌بندی):
- CreateNewPackageCommand ✅
- UpdatePackageCommand ✅
- DeletePackageCommand ✅
- GetAllPackageByFilterQuery ✅
- GetPackageQuery ✅
```
**تنظیمات دسترسی پیشنهادی**:
- SuperAdmin: همه عملیات
- Admin: همه عملیات محصولات (Create, Update, Delete)
- Inspector: فقط Get queries
---
### 8. Orders (`userorder.proto`, `factordetails.proto`)
#### ✅ موجود در BFF:
- CreateNewUserOrderCommand
- UpdateUserOrderCommand
- DeleteUserOrderCommand
- GetAllUserOrderByFilterQuery
- GetUserOrderQuery
#### ⚠️ موجود در CMS ولی نه در BFF:
```
UserOrder:
- CancelOrderCommand (لغو سفارش)
- UpdateOrderStatusCommand (تغییر وضعیت)
- GetOrderByInvoiceNumberQuery (جستجو با شماره فاکتور)
- GetOrdersByDateRangeQuery (گزارش بازه زمانی)
- CalculateOrderPVQuery (محاسبه PV سفارش)
- ApplyDiscountToOrderCommand (اعمال تخفیف)
FactorDetails:
- GetFactorDetailsQuery (جزئیات کامل فاکتور)
- UpdateFactorDetailCommand (ویرایش آیتم فاکتور)
- RemoveFactorDetailCommand (حذف آیتم فاکتور)
```
**تنظیمات دسترسی پیشنهادی**:
- SuperAdmin: همه عملیات
- Admin: همه عملیات (Create, Update, Cancel, Status)
- Inspector: فقط Get queries
---
## ❌ APIهای بدون پوشش (باید اضافه شوند)
### 9. Shopping Cart (`usercarts.proto`)
**وضعیت**: هیچ Handler در BFF وجود ندارد
```
باید اضافه شود:
- AddToCartCommand
- UpdateCartItemCommand
- RemoveFromCartCommand
- GetUserCartQuery
- ClearCartCommand
- MergeCartCommand (برای کاربران مهمان → لاگین)
```
**تنظیمات دسترسی پیشنهادی**:
- SuperAdmin: مشاهده سبد همه کاربران
- Admin: مشاهده سبد همه کاربران
- Inspector: مشاهده فقط (بدون ویرایش)
**اولویت**: 🟡 متوسط (برای فروشگاه ضروری است)
---
### 10. Transactions (`transactions.proto`)
**وضعیت**: هیچ Handler در BFF وجود ندارد
```
باید اضافه شود:
- CreateTransactionCommand (ثبت تراکنش پرداخت)
- GetTransactionQuery
- GetAllTransactionsByFilterQuery
- GetTransactionByReferenceQuery (جستجو با شماره پیگیری)
- GetUserTransactionsQuery (تراکنش‌های یک کاربر)
- VerifyTransactionCommand (تأیید پرداخت از درگاه)
- RefundTransactionCommand (بازگشت وجه)
```
**تنظیمات دسترسی پیشنهادی**:
- SuperAdmin: همه عملیات + Refund
- Admin: مشاهده تراکنش‌ها (بدون Refund)
- Inspector: فقط مشاهده
**اولویت**: 🔴 بالا (برای درگاه پرداخت ضروری است)
---
### 11. Contracts (`contract.proto`, `usercontract.proto`)
**وضعیت**: هیچ Handler در BFF وجود ندارد
```
باید اضافه شود:
Contract:
- CreateContractCommand
- UpdateContractCommand
- DeleteContractCommand
- GetAllContractsQuery
- GetContractQuery
- ActivateContractCommand
- DeactivateContractCommand
UserContract:
- AssignContractToUserCommand
- GetUserContractsQuery
- GetContractUsersQuery
- RevokeUserContractCommand
```
**توضیح**: Contracts احتمالاً برای قراردادهای عضویت یا خریدهای خاص است.
**تنظیمات دسترسی پیشنهادی**:
- SuperAdmin: همه عملیات
- Admin: Assign, Get queries
- Inspector: فقط Get queries
**اولویت**: 🟢 پایین (در صورت نیاز بیزینسی)
---
### 12. Public Messages (`public_messages.proto`)
**وضعیت**: هیچ Handler در BFF وجود ندارد
```
باید اضافه شود:
- CreatePublicMessageCommand (ایجاد اعلان عمومی)
- UpdatePublicMessageCommand
- DeletePublicMessageCommand
- GetAllPublicMessagesQuery
- GetPublicMessageQuery
- PublishMessageCommand (انتشار اعلان)
- ArchiveMessageCommand (بایگانی)
```
**توضیح**: پیام‌های عمومی برای اطلاع‌رسانی به تمام کاربران
**تنظیمات دسترسی پیشنهادی**:
- SuperAdmin: همه عملیات
- Admin: Create, Update, Publish
- Inspector: فقط Get queries
**اولویت**: 🟡 متوسط
---
### 13. User Address (`useraddress.proto`)
**وضعیت**: در BFF موجود است ✅
- ✅ CreateNewUserAddressCommand
- ✅ UpdateUserAddressCommand
- ✅ DeleteUserAddressCommand
- ✅ GetAllUserAddressByFilterQuery
- ✅ GetUserAddressQuery
---
## 📋 خلاصه کارهای باقی‌مانده در CMS
### 🔴 اولویت بالا (برای Launch ضروری):
1. **Transactions** - درگاه پرداخت
- زمان: 3 روز
- Commands: 7 مورد
- ✅ داکیومنت: در `REMAINING-TASKS.md`
### 🟡 اولویت متوسط (برای فروشگاه):
2. **Shopping Cart**
- زمان: 2 روز
- Commands: 6 مورد
3. **Public Messages**
- زمان: 1 روز
- Commands: 6 مورد
4. **Products (تکمیل)**
- Tags Management
- Bulk Operations
- Low Stock Alerts
- زمان: 2 روز
5. **Orders (تکمیل)**
- Cancel/Status/Discount
- Reports
- زمان: 2 روز
### 🟢 اولویت پایین:
6. **Contracts** (در صورت نیاز بیزینسی)
- زمان: 2 روز
---
## 🎯 نقشه راه پیشنهادی
### هفته 1: Transaction System (درگاه پرداخت)
- CMS: 7 Command/Query
- BFF: 7 Handler
- BackOffice: صفحه تراکنش‌ها
- ✅ داکیومنت
### هفته 2: Shopping Cart
- CMS: 6 Command/Query
- BFF: 6 Handler
- BackOffice: صفحه مدیریت سبدهای خرید کاربران
- ✅ داکیومنت
### هفته 3: Products & Orders تکمیل
- Tags Management
- Bulk Operations
- Order Cancel/Status
- ✅ داکیومنت
### هفته 4: Public Messages
- Create/Publish Messages
- Notification System
- ✅ داکیومنت
---
## 📊 تخمین زمان کل
| فیچر | CMS | BFF | BackOffice | جمع |
|------|-----|-----|------------|-----|
| Transactions | 3 روز | 2 روز | 2 روز | **1 هفته** |
| Shopping Cart | 2 روز | 1 روز | 2 روز | **1 هفته** |
| Products/Orders تکمیل | 2 روز | 1 روز | 2 روز | **1 هفته** |
| Public Messages | 1 روز | 1 روز | 1 روز | **3 روز** |
| **جمع کل** | | | | **3.5 هفته** |
---
## ✅ چک‌لیست قبل از شروع هر فیچر
- [ ] بیزینس در `network-club-commission-system-v1.1.md` مستند شده؟
- [ ] Proto file در CMS تعریف شده؟
- [ ] Commands/Queries در CMS پیاده‌سازی شده؟
- [ ] Migration اجرا شده؟
- [ ] Handlers در BFF اضافه شده؟
- [ ] Controllers در BFF تعریف شده؟
- [ ] صفحات در BackOffice ایجاد شده؟
- [ ] تست‌های دستی انجام شده؟
- [ ] ✅ داکیومنت نهایی (مقایسه کد با داکیومنت)
---
**آخرین به‌روزرسانی**: 2025-12-01
+71
View File
@@ -0,0 +1,71 @@
# 📁 CMS - Database & Design Files
این پوشه شامل فایل‌های طراحی و اسکریپت‌های دیتابیس CMS است.
---
## 📊 فایل‌ها
### Database Models (.ndm2):
- **`model.ndm2`** - طراحی اصلی دیتابیس CMS
- حجم: 2.4 MB
- آخرین بروزرسانی: 1 دسامبر 2025
- ابزار: Navicat Data Modeler
- **`model1.ndm2`** - نسخه 2 طراحی (احتمالاً با تغییرات Network/Club)
- حجم: 2.2 MB
- آخرین بروزرسانی: 1 دسامبر 2025
### SQL Scripts:
- **`update-pool-percent.sql`** - اسکریپت بروزرسانی درصد Pool کمیسیون
- حجم: 2 KB
- استفاده: Update درصدهای استخر هفتگی
### Documentation:
- **`network_crm_calculate.txt`** - محاسبات CRM شبکه
- حجم: 28 KB
- محتوا: فرمول‌های محاسباتی، قوانین کسب‌وکار
---
## 🔧 نحوه استفاده
### باز کردن Database Models:
```bash
# باز کردن با Navicat Data Modeler
navicat-data-modeler model.ndm2
```
### اجرای SQL Scripts:
```bash
# اجرا در SQL Server
sqlcmd -S localhost -d CMS_Database -i update-pool-percent.sql
# یا در Azure Data Studio / SSMS
```
### مشاهده محاسبات:
```bash
cat network_crm_calculate.txt | less
```
---
## 📝 نکات مهم
- ⚠️ **Backup**: قبل از اجرای اسکریپت‌ها، حتماً از دیتابیس backup بگیرید
- 📊 **ERD**: برای مشاهده Entity Relationship Diagram از Navicat استفاده کنید
- 🔄 **Sync**: این مدل‌ها باید با Entity ها در کد همگام باشند
---
## 🔗 مراجع
- **Entity Guide**: [`../entity-guide.md`](../entity-guide.md)
- **Implementation Status**: [`../implementation-status.md`](../implementation-status.md)
- **Business Logic**: [`../../../01-BUSINESS/`](../../../01-BUSINESS/)
---
**تاریخ ایجاد**: ۱ دسامبر ۲۰۲۵
**آخرین بروزرسانی**: ۱۴ آذر ۱۴۰۴
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,94 @@
سیستم کر مرکزی خب سیستم کارگزاری کیف پول داره که کیف پولی که اینا میرن خرید میکنن از دایا برمی‌گردن وامشون واریز میشه این کیف پول شارژ میشه ۵۶ تومان حالا بازار یه فروشگاه داره یه فروشگاه اینترنتی داره که با این ۵۶ تومن که فعلا امتیازی که باید برن حتما از دایا خرید کنن برگردن بعدا قراره خودشون نقدی کیف پولشون رو شارژ کنن یعنی با سلیقه درگاه بیان کیف پولشونو شارژ کنن. تو هر دوتا حالتش از این فروشگاه می‌تونن خرید کنن حالا بعد اینکه کیف پولشون شارژ میشه حالا از طریق دایه‌ها یا از هر طریق دیگه به اون اندازه‌ای که ما متوجه بشیم که این شارژ کیف پول به دلیل عضویت در باشگاه مشتریان بوده
برتری ممکنه طرف بیاد یه میلیون کیف پولشو شارژ کنه اون یه میلیونه مثلا ما یه باشگاه مشتریانم جدا داریم یعنی آره خود باشگاه مشتری که فعال فعال میشه ۱. الان فعلاً در حال حاضر دایا خرید کنی وام بگیری خب وامشو بگیری هم باز باید واسم یه قسمتشو انگار مثلاً یه دکمه باید بزنی اختصاص بده به باشگاه مشتری یا نه دقیقاً یعنی یه دکمه میزنی این اختصاص داده میشه یعنی توی خود کارا بازار یه دکمه‌ای وجود داره میزنی و بعد از اینکه پرداختتون انجام دادی که پولتو شارژ کردی این دکمه رو میزنی و شما عضو باشگاه مشتریان میشی یعنی ما می‌سنجیم ببینیم اینکه تو. پرداختیتو انجام دادی اول بعد باشگاه مشتریان میشی حدوداً ۲۰ ۲۵ میلیونش از این ۵۶ میلیونی که تامین اعتبار میشه
جدا میشه جدا میشه میره تو باشگاه مشتری میره تو باشگاه مشتریان که از اونجا دیگه مدیریت اون محاسبه پورسانته دقیقاً انجام حالا باشگاه مشتریان چی داره باشگاه مشتریان خودش خودش برای خودش به صورت مجزا یه فروشگاه تور داره که تو اون فروشگاهه صرفا یه سری تخفیف وجود داره یعنی متفاوت با این فروشگاه اصلی اون فروشگاه یه سری تخفیف داره. ‏a۵۵ ۳۰ درصد تخفیف این ۳۰ درصد تخفیف تو چجوری میتونی استفاده کنی حالتی که رفته باشی کیف پول اصلی تو کیف پول اصلیتو شارژ کرده باشی حالا از طریق دایه یا نقدی کیف پول اصلیتو شارژ کرده باشی یه ۵۶ تومان که به کیف پول اصلیت واریز میشه
هیچ یه ۵۶ تومان هم به کیف پول تخفیف تو باشگاه مشتریان اضافه میشه که اون گوشی ۵۵ که مثلا ۳۰ درصد تخفیف داره رو ۵۶ تومن واریز میشه ۵۶ تومن واریز میشه. به کیف پول تخفیفت یعنی اون یه ۲۵ میلیون برای باشگاه مشتریانه وقتی باشگاه مشتری فعال می‌کنی ۵۶ میلیون اعتبار تخفیف برات فعال میشه که از اون فروشگاه دوم میتونی خرید کنی ولی چه جوری میتونی خرید کنی فقط همون درصد تخفیف رو میتونی از این ۵۶ تومان استفاده میشه اوکی پس چی شد اگه گوشی مثلا. ۲۰ درصدش تخفیف خورده اون ۲۰% رو می‌تونی از این ۵۶ تومانه استفاده کنی مابقیشو باید نقدی اینجوری میفهمم من باید یه تیبل داشته باشم کسایی که میان
میرن جز باشگاه مشتریان میشن رو اونجا ثبت بکنم یعنی وصل به تیبل یوزرمون بعد اونجا ثبت میشه آها این شخص جز باشگاه مشتری حالا خود باشگاه مشتریان یادته که دکتر گفتش که آقا یه سری لیست داره که اونا فعال میشن فعال شده شماره بیمه چیه یا اگه مثلا فلان چی فعال شده برات این چیه خب مثلا من. تو ذهنم اینجوری بود که خیلی ساده که آپشنای باشگاه مشتریانه اول که میگیم آقا این کاربر جز باشه مشتریان شده است یا خیر ۱ فیلدی که میگه شده است یا خیر یه تیبل دیگه است که میگه آقا این فیچرهایی که از این باشگاه مشتری گرفته کدوماشو گرفته یه تیبل دیگه هست که فیچرها رو اون تو میزنیم باشگاه مشتری داریم آره یه تیبل واسطه مشتریان و یوزر داریم که آقا این یوزر این فیچر براش باز شده با این توضیحات دقیقا اوکی حالا. بعد من علاوه بر این یه کیف پول تخفیف هم باید به کیف به فیلدهای ولتم اضافه کنم یعنی الان یه تیبل ولت دارم یه موجودی شبکه داره یه موجودی خالص داره یه موجودی تخفیف هم باید داشته باشه یعنی سه تا موجودی باید داشته باشه درسته حالا این سه تا موجودی زمانی موجودی تخفیف فعال میشه که کاربر جزو باشگاه مشتریان شده
باشه خب بعد از این فروشگاه یعنی ممکنه محصولاتشم حتی فرق داشته فعال بکنه که آقا من میخوام از. کیف پول تخفیفی بخرم تخفیفا رو نمایش بده اگه نه می‌خوام از تخفیفیم نخرم هادیا رو نمایشگاه باید ایمپلیمنت باشه حالا این پس این باشگاه مشتریان که من میتونم جزئیات باشگاه مشتری خیلی جالبه این فروشگاه رو تو مثلا یه گوشی با یه لپ تاپ میخری گوشی ۲۰ درصد تخفیف داره لپ تاپ ۵۰ درصد تخفیف داره تو اون ۲۰% ۵۰% رو از این کیف پول تخفیفت میتونی استفاده کنی شارژ شده مابقیش هم نقدی میره مستقیم برو نقدی پرداخت کن. ما به صورت هفتگی محاسبه کارمزد داریم یعنی به صورت هفتگی کارم محاسبه می‌کنیم
پلن نتورک این شبکه هم پلن باینره که یه تعادلی ایجاد میشه فقط هم دو نفره دیگه فقط دو نفر بله دو نفر یعنی شما یه دست راست داری یه دست چپ داری بیشتر از اون نداری یعنی سه تا دست و چهار تا دست نداریم ما الان دو تا دست داریم یعنی من. یوزر یه دست راست دارم یه دست چپ دست راستم مثلاً آقای ایکس دست چپم خانم یعنی هیچ چیز اضافه تری نداره ما یه حالا ما توی محاسبه پورسان با کدوم یک از این اعتبارا کار دارم فقط ۵۰ میلیون تومن ۵۶ میلیون تومن تو کیف پول اصلی واریز میشه یه ۵۶ میلیون تومن توی کیف پول تخفیف واریز میشه یه دونه ۲۵ میلیون تومان هم میره توی کارمزد نتورک میره اونجا که بخواد کارمزدش محاسبه بشه.
آخر هفته ما محاسبه میکنیم میگیم مثلا میثم مقدم دو نفر زیر مجموعه داره مثلا ایکس و ایگرگ آقای ایکس و خانم ایگرگ این دو نفر زیر مجموعه هر کدوم اومدن ۵۶ تومان خرید کردن خب خودمم که ۵۶ تومان همون اول خرید کرده بودم یعنی پکیج خریده بودم سرمایه گذاری کرده بودم. این ۵۶ تومان با این ۵۶ تومان میشه حدوداً صد و ۱۱۲ تومن با ۵۶ تومان خودم میشه ۱۶۸ تومن درسته ۱۶۸ تومن توی مخزنمون هست خب ۱۶۸ تومن تو مخزنمون هست حالا بذار من این چیزمو نگاه کنم خب نگاه کن ما به ازای هر تعادلی که ایجاد میشه یک امتیاز به. الان مثلاً من گفتم آقای ایکس و خانم دیگه خب یه تعادل ایجاد کردم درسته یعنی امتیازمون یعنی امتیاز من چنده یه دونه تعادل ایجاد کردم تو هر هفته تعداد تعادل رو محاسبه میکنیم اوکی تعداد تعادل های هر نفر را محاسبه. حالا ده تا تعادل یعنی چی من که یه دونه بیشتر تعادل نمیتونم بزنم اگه من زیر مجموعهم یه تعادل بزنه برای من حساب میشه
بله خب نه نگاه کن الان من زیر مجموعه سمت راستم یه تعادل زده یعنی دو نفرو جذب کرده این میشه خب همین یه طرف هم میشه اگه اون طرف هم تعادل همون دیگه یعنی من هرچقدر سطحم میره پایین تر تعداد تعادل باید ضربدر دو بشه. یعنی من توی لول اول خودم اگه یه دونه دو نفرو جذب بکنم میشه یه تعادل ولی اگه می‌خوام دومین تعادلو داشته باشم بعد سمت راستم یه تعادل یعنی یه دو نفر جذب بکنه سمت چپم یه دو نفر جذب بکنه سمت راست سمت چپت بعد هر کدوم یه دونه جذب بکنه هر کدومشون باید یه تعادل بزنند که برای تو دوتا تعادل حساب بشه
یعنی نگاه کن تو خودت که الان فرض میکنیم تو هفته اول یه اتفاقی افتاده اتفاقی اینه تو خودت دو نفرو جذب کردی یعنی میثم مقدم آقای ایکس و خانم ایگرگ رو جذب کرده آقای ایکس دو نفرو جذب کرده. خانم ایگرگم دو نفرو جذب کرده خب تو دوتا تعادل یه دونه تعادل که خودت زدی چون آقای ایکس خانم ایگرگ رو جذب کردی یه دونه تعادل اینورت زده یه دونه تعادل جمع میشه چند تا تعادل سه تا تعادل تو زدی درست شد نشد دیگه گفتیم دوتا تعادل میشه نه دیگه چرا دوتا تعادل گفتی که آقا من وقتی که توازن برقرار بشه بهش میگیم یه تعادل دیگه خب خب من وقتی که خودم یه دو نفر جذب می کنم میشه
تعادل وقتی زیر مجموعه تعادل جذب میکنه هنوز برای من تعادل نیست چون زیر مجموعه دوم هم باید تعادل بزنه دیگه. تعادل هر کدوم نفری براشون یه تعادل ولی برای تو تعادل اونا که حساب نمیشه برای تو یه تعادل از یه سطح بالاتر حساب میشه دیگه اینجوری نیست مگه نه اونجوری که تو همیشه یه تعادل دوتا تعادل میتونی داشته باشی نه چون دو تا دست داری اینا هر کدوم تعادل تعادل تعادل بزنن یه دونه تعاد. مبلغ کیف پوله مگه شرط نیست اون چیزی که تو صندوق جمع شده مگه شرط نیست نه به اون کاری نداریم الان تعداد تعادل چگونه محاسبه میشود چه جوری ما حساب میکنیم تو چند تا تعادل زدی تو یه دستت یه تعادل بزنه یه دسته دیگه هم یه تعادل تو دو تا تعادل زدی متوجه شدی تو تونستی دوتا دوتا جذب کنی خب دو تا تعادل حالا بگذریم از همون خیلی ساده‌شو
بگیریم من میثم مقدم دو نفرو جذب کردم آقای ایگرگ خانم ایکس درسته. امتیاز تو شد ۱ به تعداد تعادل مساوی با امتیاز یعنی تعداد تعادل مساوی است با امتیاز تعداد تعادل هر شخص مساوی است با امتیاز اون شخص حالا هرچی که مبلغ توی صندوق جمع شده یعنی من خودم ۵۶ تومن دادم دست راستم ۵۶ تومن داده دست داده درسته البته که اینا که دارم میگم اشتباهه. ۵۶ تومنه یکیش واسه کیف پول تخفیفه یکیش واسه کیف پول اصلیه ما اینجا ۲۵ تومان داریم دست خودم ۲۵ تومان آوردم تو باشگاه مشتریان دست راستم ۲۵ تومان آورده دست چپم ۲۵ تومان آورده جمعاً میشه ۷۵ تومان یعنی ۷۵ میلیون تومن تو صندوق جمع شده
درسته من چه امتیازی دارم ۱ درسته دست راستم چه امتیازی داره صفر دست چپم چه امتیازی داره صفر درسته ما با اونا کار نداریم الان مبلغ پورسانت من چی میشه من یک امتیاز دارم اون ۷۵ تومن تقسیم بر یک. اون دوتا که صفر بودن دیگه اگه اون دوتا نفر یک بودن میشد مثلا تقسیم بر سه خب میشه مبلغ ریالی هر امتیاز یعنی مجموع کل امتیازهایی که همه کاربرها جمع کردن و مجموعه کل امتیازها اینا رو یه دست نگهدار این عددی که تو صندوق جمع شده تقسیم بر مجموعه کل امتیازها یعنی عددی که تو صندوق جمع شده تقسیم بر کل تعداد تعادل‌های این هفته مساوی است با مبلغ ریالی هر امتیاز حالا تو چند امتیاز داشتم ۷۵ میلیون تقسیم بر ۱. یعنی مبلغ ریالی هر امتیاز میشه ۷۵ میلیون درسته حالا من چند امتیاز داشتم ۱ پس ۷۵ میلیون ضربدر یک میشه
یعنی ۷۵ میلیون تومان باید کارمزد بگیرم یه لول میاد پایین تر خب من اگر این هفته جدید تعادل جدیدی ثبت نکنم که دیگه برام تعادل حساب نمیشه یعنی من وقتی تعادل زدم پولشم گرفتم دیگه اون تعادل پاک میشه اون تعادل دیگه پاک میشه دیگه برای تو تعادل جدید حساب نمیشه خب. حالا من توی شبکه هم دست چپ و راستم رفتی یه لول پایین تر اونا هم یه دونه مثلاً شده هفته بعد اونا هم یه تعادل دیگه زدن برای من دوتا تعادل حساب میشه برای خودشون چند تا هر کدوم نفری یه دونه درسته هفته اول دیگه چون خود من دو نفر جذب کردم میشه ۱ درسته اونا هر کدوم دو نفر جذب کردن ۱ ۱ برای من میشه سه. هفته اوله حالا شده ۵ هرچی که تو صندوق از اون ۲۵ میلیون ۲۵ میلیون جدید درسته یعنی اونایی که دیگه همش هفته اول همش جدیده دیگه ثبت شده
تقسیم میشه بین اون امتیازها حالا کی چقدر امتیاز داره همون پول میگیره درسته چه اتفاقی افتاده من ۲۵ میلیون دست راستم ۲۵ میلیون ۷۵. هر کدوم از اونا نفری دو نفرو جذب کردن که دو تا ۲۵ میلیون اونور ۵۰ ۵۰ ۱۰۰ میلیون ۱۰۰ میلیون با ۷۵ میلیون میشه ۱۷۵ میلیون ۱۷۵ میلیون تقسیم بر ۵ میشه حدوداً ۳۵ میلیون یعنی ۳۵ میلیون ارزش ریالی هر امتیازه بعد حالا هر کی چقدر امتیاز داره همونقدر بهش تعلق می‌گیره من چقدر امتیاز دارم ۳ امتیاز دارم ۳۵ میلیون ضربدر ۳ ۳ تا ۳۵ میلیون هم باید بگیرم یه دونه ۳۵ میلیون دست راستم باید بگیره یه ۳۵ میلیون دست چپم باید بگیره خب من مثلا میتونم یه تیبل داشته باشم خب که. هر کسی هر هفته‌ای که تعادل میزنه خب اونو اونجا ثبت بشه
تعداد تعادل‌های هر شخص توی هر هفته باید ثبت بشه خب تعداد تعادل‌های هر شخص تو هر هفته باید ثبت بشه یعنی اگه اون مثلاً من زیر مجموعه‌هام هزار تا ۲۰۰۰ نفر بشه اون پایینم یه نفر یه تعادل بزنه برای من یه تعادل ثبت میشه حالا اگه یه دستم یه تعادل بزنه بازم برای من یه تعادل ثبت میشه یعنی من نباید تلاش کنم چرا دست دوم باید همونقدر تعادل بزنه یعنی اگه مساوی بزنن تعادل حساب میشه. هفته اولم باشه فقط آقای ایکس یه تعادل بزنه من برای خودش تعادل حساب میشه پس من باید توازن داشته باشم دیگه باز خب اگر توازن داشته باشم یعنی مثلا من حالا مثلا یه لول رفته
جلوتر سه تا تعادل این دستم زده دو تا تعادل این دستم زده برای من ۲ حساب میشه دو اینور دو این ور میشه چهار یعنی من هر موقعی که یه تعادلی شکل میگیره باید برم دست مقابل اونم نگاه کنم ببینم تعادلی وجود داره تازه میشه یه تعاد. تعادل بعدی اگه اونور وجود داشت که هیچی اگر وجود نداشت اگه وجود داشت که خب دیگه تعادله اگه وجود نداشتم که هیچی این دست نگاه کنم ببینم که مثلاً این دست که حالت تعادل زده این دستش یه تعادل داره در هر صورت بخوام یه فرمول کلی بگم تو دست چپت تو اعماق اصلا ده لول ۱۵ رفته پایین این نتورک تا لول ۱۵ رفته
پایین دست چپت اون پایین مایا چهار تا تعادل میزنه دست راستتم حداقل باید چهار تا تعادل بزنه تا بره تو یه چیزی محاسبه بشه یعنی اگه دست. چپ تو خوب دوتا تعادل زده دست راستت چهار تا تعادل زده دو تا تعادل واسه تو حساب میشه دوتا اینور دوتا اونور جمع میشه چهار تا اگه دست راستتو پنج تا تعادل زده دست چپتو هیچ تعادلی نزده پس در نتیجه هیچ تعادلی واسه تو حساب نمیشه اگه دست راستتو دو تا تعادل زده دست چپتم دو تا تعادل زده دقیقا حالا با همدیگه مساوی چهار تا تعادل اگه دست راست تو ده تا تعادل زده ۱۰۰ تا تعادل زده ولی دست چپت دوتا تعادل زده کلاً دو تا تعادل حساب میشه دو تا راست دو تا چپ میشه
چهار تا. تعادل یه نفر حساب کنی این شکلی باید حساب کنیم خب من الان مثلا اون تیبلی که میزارم باید چه شکلی باشه یعنی همون لحظه که یه نفر ثبت نام میکنه من کسی که عضو باشگاه مشتریان میشه تو یه جا ثبت کن که آقا این نفر عضو باشگاه مشتریان شد حالا آخر هفته محاسبه می‌کنی اون نفری که عضو باشگاه مشتری اینا شده والدش کی بوده والدش کی بوده والد والت همینجوری تا آخر آیا تعادل خورده است یا خیر یعنی تو هفتگی باید حساب کنی تو این هفته ورودی های این هفته رو باید حساب کنی. خب من نمی‌تونم مثلاً وقتی که یه نفر جزو باشگاه مشتریان میشه
همون لحظه تعادل همه بالا سریاشو حساب کنم نه شاید تعادل بیشتر بزنه خب باشه وقتی بیشتر زد دوباره افزایش نمی‌دونم شاید بشه بعد اینو حساب کتاب کنی بعد با دکترم جلسه بذاری که ببینی دقیقاً این چه جوریه مثلا هفته پیش یه نفر یه تعادل زده این هفته کلاً پوچ میشه تعادلاش چون من تا جایی که یادمه باید سعی کنه طرف تو هفته دو تا تعادل این دستشو بزنه وگرنه پوچ میشه یعنی از دست دادتش. حله و در مجموع پس هر کدوم من میگم اون تیبلی که دارم حتما باید یه چیزی تحت عنوان امتیاز باشه اگه همون تعداد تعادل خب بعد عددی که جمع میشه هم یه جا باید من یه جا نگهش دارم عددی که تو این هفته جمع میشه
تعداد تعادل این هفته و مبلغی که تو این هفته تو باشگاه مشتریان جمع شده حالا این تقسیم برای امتیاز هرکی به نسبت امتیازی که داره یه مبلغی براش ثبت میشه که اون مبلغ در نهایت میره تو کیف پول شبکه یا کیف پول کارمزد اصلا کیف پول نذاریم بذاریم کارمزد کمیسیون. یه چیزی باید باشه ولی یه مخزنی هست دیگه یه جایی هستش که تو هر هفته مبلغی که با استفاده از اون پلن شبکت دریافت کردی میره اونجا واریز میشه حالا این مبلغی که توی کیف پول شبکه یا کیف پول کارمزد هست یا کیف پول طلایی اسمشو بذاریم چون اسم این امتیازها امتیازهای طلاییه اسم اون کیف پوله رو بذاریم کیف پول طلایی چون سه تا کیف پول شد یک کیف پول اصلی که تو میتونی بری از فروشگاه بازار خرید کنی مستقیمه دو کیف پول تخفیف که تو میتونی بری از فروشگاه که بعد از باش
مشتریان این اتفاق. یکی هم کیف پول طلاییت یا همون کیف پول کارمزدت این میشه سه تا کیف پول حالا کیف پول کارمزد چه جوری میتونی برداشت کنی دو طریق داره یک نقدی برداشت کنید یعنی شماره شبا بدیم و نقدی برات پرداخت کنیم ۲ بری از دایا الماس بخری حالا یه چیزی من الان ۵۶ میلیون تومنو یعنی ما الماس بهت بدیم اوکی ما الان ۵۶ میلیون تومنو آوردیم توی کیف پول که میتونه بره خرید بکنه اگه باشگاه مشتری اینو بزنیم ۲۵ میلیون ازش کم میشه دیگه کم میشه دیگه. میلیون تومن توی باشگاه مشتریان شارژ میشه جدای از این یعنی میشه چی میشه یه ۵۶ میلیون تومن توی کیف پول اصلی یعنی ۵۶ میلیون تومن تو کیف پول ۲۵ میلیون تومان توی خود باشگاه اوکی حالا بذارید تحلیل بکنم ببینم چی میتونم در بیارم.
masoud moghaddam, [11/29/25 6:23AM]
کاربر A: فعال‌سازی (۲۵M به استخر)
├─ فرزند Left: کاربر B (فعال‌سازی ۲۵M)
└─ فرزند Right: کاربر C (فعال‌سازی ۲۵M)
استخر هفته اول: ۷۵M
تعادل کاربر A: MIN(1, 1) = 1
تعادل کاربر B: 0
تعادل کاربر C: 0
مجموع تعادل‌ها: 1
ارزش هر امتیاز: 75M ÷ 1 = 75M
کمیسیون کاربر A: 1 × 75M = 75M
کاربر B: جذب دو نفر (D و E) → تعادل ۱
کاربر C: جذب دو نفر (F و G) → تعادل ۱
استخر هفته دوم: ۴ × ۲۵M = ۱۰۰M
تعادل کاربر A: MIN(1, 1) = 1 (از B و C)
تعادل کاربر B: 1
تعادل کاربر C: 1
مجموع تعادل‌ها: 3
ارزش هر امتیاز: 100M ÷ 3 ≈ 33.33M
کمیسیون کاربر A: 1 × 33.33M = 33.33M
کمیسیون کاربر B: 1 × 33.33M = 33.33M
کمیسیون کاربر C: 1 × 33.33M = 33.33M
masoud moghaddam, [11/29/25 6:24AM]
این نوع محاسبه درسته ؟
Doctor
Doctor Seif, [12/1/25 4:37PM]
سلام
نصفش درسته، نصفش نه
Doctor Seif, [12/1/25 4:42PM]
کاربر A: فعال‌سازی (۲۵M به استخر)
  ├─ فرزند Left: کاربر B (فعال‌سازی ۲۵M)
  └─ فرزند Right: کاربر C (فعال‌سازی ۲۵M)
استخر هفته اول: ۷۵M
تعادل کاربر A: MIN(1, 1) = 1
تعادل کاربر B: 0
تعادل کاربر C: 0
مجموع تعادل‌ها: 1
ارزش هر امتیاز: 75M ÷ 1 = 75M
کمیسیون کاربر A: 1 × 75M = 75M
کاربر B: جذب دو نفر (D و E) → تعادل ۱
کاربر C: جذب دو نفر (F و G) → تعادل ۱
استخر هفته دوم: ۴ × ۱۰۰M = ۲۵M
تعادل کاربر A: MIN(2, 2)=2 = 1 (از B و C)
تعادل کاربر B: 1
تعادل کاربر C: 1
مجموع تعادل‌ها: 4
ارزش هر امتیاز: 100M ÷ 4 = 25M
کمیسیون کاربر A: 2 × 25M = 50M
کمیسیون کاربر B: 1 × 25M = 25M
کمیسیون کاربر C: 1 × 25M = 25M
قصه محاسبه تعادل اینه که اون کاربر بالایی وقتی که کاربرهای پایینیش یعنی ای و بی تعادلش رو می‌گیرند خط تعادل اون که بین کاربر ای و بیه این سمتش دو نفر وارد میشه اون سمتش دو نفر یعنی دو تا یک به یک پس تعادل دوش فعال می‌شه برای اون دیگه تعادل یک نیست همونطور که زمانی که توی سمت بین همون که داری میگی مثلا شش نفر سمت ای باشن پنج نفر سمت بی تعادلش میشه ۵ یه نفر از اونایی که سمت ای اند. باقی میمونه برای محاسبات هفته آینده‌اش یعنی شما باید اون خط مرکز را بکشی و بعد به نسبت تعداد افراد سمت چپ که ای یا ای و تعداد افراد سمت بی اون نسبت رو می‌گیری اون میشه
تعداد تعادل اون فرد بالا برای بقیه افراد هم همینه یعنی هر فردی یک سازمان ای و یک سازمان بی داره تعداد تعادل‌ها می‌شه مجموع افراد ورودی هفته جدید به اضافه باقی مانده‌های هفته قبلی اگر باقی مانده توی اون سمتش مونده تعادلشون با مجموع تعداد افراد ورودی جدید. به اضافه باز باقیمانده‌های هفته قبلی اگر باقیمانده از هفته قبلی مونده جمع این دو تا پایین‌ترین عددش میشه میزان تعادل اون پایین‌ترین عدد منهای اون تعداد میشه باقیمانده تو هر دستی که بود چه ای بود چه بی بود میره سیو میشه برای هفته بعدی.
@@ -0,0 +1,51 @@
-- Script to update WeeklyPoolContributionPercent from 10% to 20%
-- این script فقط در صورتی که رکورد وجود داشته باشد، آن را آپدیت می‌کند
-- بررسی وجود جدول SystemConfigurations
IF OBJECT_ID('SystemConfigurations', 'U') IS NOT NULL
BEGIN
PRINT 'جدول SystemConfigurations یافت شد. در حال آپدیت...'
-- آپدیت رکورد (در صورت وجود)
UPDATE SystemConfigurations
SET
Value = '20',
Description = N'درصد مشارکت در استخر هفتگی از کل فعال‌سازی‌های جدید شبکه (20%)',
LastModified = GETUTCDATE()
WHERE [Key] = 'Commission.WeeklyPoolContributionPercent'
-- اگر رکوردی وجود نداشت، اضافه کن
IF @@ROWCOUNT = 0
BEGIN
PRINT 'رکورد Configuration یافت نشد. در حال ایجاد...'
INSERT INTO SystemConfigurations
([Key], Value, Description, Scope, IsActive, DataType, Created)
VALUES
('Commission.WeeklyPoolContributionPercent', '20',
N'درصد مشارکت در استخر هفتگی از کل فعال‌سازی‌های جدید شبکه (20%)',
2, -- ConfigurationScope.Commission = 2
1, -- IsActive = true
'Int',
GETUTCDATE())
END
ELSE
BEGIN
PRINT 'رکورد با موفقیت آپدیت شد.'
END
END
ELSE
BEGIN
PRINT 'جدول SystemConfigurations هنوز ایجاد نشده است.'
PRINT 'لطفاً ابتدا سرویس را یکبار اجرا کنید تا جداول Seed شوند.'
END
-- نمایش وضعیت فعلی
IF OBJECT_ID('SystemConfigurations', 'U') IS NOT NULL
BEGIN
PRINT ''
PRINT 'وضعیت فعلی:'
SELECT [Key], Value, Description, Scope, IsActive
FROM SystemConfigurations
WHERE [Key] = 'Commission.WeeklyPoolContributionPercent'
END
+131
View File
@@ -0,0 +1,131 @@
# راهنمای پیکربندی Email و SMS
## تنظیمات Email (Gmail)
### مرحله 1: ایجاد App Password در Gmail
1. به [Google Account Security](https://myaccount.google.com/security) بروید
2. گزینه "2-Step Verification" را فعال کنید
3. به بخش "App passwords" بروید
4. یک App Password جدید با نام "FourSat CMS" ایجاد کنید
5. پسورد 16 رقمی را در `appsettings.Production.json` در فیلد `SmtpPassword` قرار دهید
### مرحله 2: تنظیم appsettings.Production.json
```json
"Email": {
"Enabled": true,
"SmtpHost": "smtp.gmail.com",
"SmtpPort": 587,
"SmtpUsername": "your-email@gmail.com", // ایمیل Gmail خود
"SmtpPassword": "your-16-digit-app-password", // App Password از مرحله 1
"FromEmail": "noreply@foursat.com", // ایمیل فرستنده (می‌تواند همان Gmail باشد)
"FromName": "FourSat CMS",
"EnableSsl": true
}
```
### سایر سرویس‌های SMTP:
#### Outlook/Microsoft 365:
```json
"SmtpHost": "smtp.office365.com",
"SmtpPort": 587
```
#### Yahoo Mail:
```json
"SmtpHost": "smtp.mail.yahoo.com",
"SmtpPort": 587
```
---
## تنظیمات SMS (کاوه نگار)
### مرحله 1: ثبت‌نام در کاوه نگار
1. به [Kavenegar.com](https://panel.kavenegar.com/client/membership/register) بروید
2. ثبت‌نام کنید و حساب خود را تأیید کنید
3. از پنل، API Key خود را کپی کنید
### مرحله 2: تنظیم appsettings.Production.json
```json
"Sms": {
"Enabled": true,
"Provider": "Kavenegar",
"KavenegarApiKey": "YOUR_KAVENEGAR_API_KEY", // API Key از پنل کاوه نگار
"Sender": "10008663" // شماره ارسال‌کننده (از پنل کاوه نگار)
}
```
### نکات مهم:
- شماره `Sender` باید از پنل کاوه نگار تهیه شود
- برای تست می‌توانید از شماره‌های رایگان استفاده کنید
- هزینه هر پیامک بسته به نوع خط متفاوت است
---
## تست کردن
### تست Email:
```bash
# در محیط Development
curl -X POST "http://localhost:5133/api/admin/trigger-weekly-calculation"
```
### تست SMS:
همان دستور بالا را اجرا کنید. سیستم به صورت خودکار:
- Email ارسال می‌کند (اگر User.Email پر باشد)
- SMS ارسال می‌کند (اگر User.Mobile پر باشد)
### بررسی Log ها:
```bash
# در ترمینال سرویس CMS
# پیام‌های زیر را مشاهده کنید:
# 📧 Email sent to {Email}: {Subject}
# 📱 SMS sent to {PhoneNumber}: {MessageId}
```
---
## امنیت
### ⚠️ مهم:
1. فایل `appsettings.Production.json` را به Git اضافه نکنید
2. از Environment Variables یا Azure Key Vault استفاده کنید
3. API Key ها را هرگز در کد سورس قرار ندهید
### استفاده از Environment Variables:
```bash
# Linux/Mac
export Email__SmtpPassword="your-app-password"
export Sms__KavenegarApiKey="your-api-key"
# Windows
set Email__SmtpPassword=your-app-password
set Sms__KavenegarApiKey=your-api-key
```
---
## خطایابی (Troubleshooting)
### Email ارسال نمی‌شود:
1. App Password را صحیح وارد کرده‌اید؟
2. 2-Step Verification در Gmail فعال است؟
3. Port 587 باز است؟
4. `EnableSsl: true` تنظیم شده؟
### SMS ارسال نمی‌شود:
1. API Key صحیح است؟
2. اعتبار حساب کاوه نگار کافی است؟
3. شماره `Sender` معتبر است؟
4. فرمت شماره موبایل صحیح است؟ (09xxxxxxxxx)
### Log ها را بررسی کنید:
```bash
tail -f /tmp/cms_run.log
```
+123
View File
@@ -0,0 +1,123 @@
# مستندات داده و بیزینس مایکروسرویس CMS
## معماری و لایه‌ها
- **پشته فنی**: .NET 9 + ASP.NET Core WebAPI، MediatR برای پیاده‌سازی CQRS، EF Core برای دسترسی داده، Mapster برای مپینگ DTO و gRPC/Protobuf برای قرارداد سرویس بین BFF ها و FrontOffice.
- **ساختار پروژه**: لایه‌های Domain (موجودیت و قواعد)، Application (CQRS Commands/Queries، ولیدیشن، DTO)، Infrastructure (EF Core + سرویس‌های جانبی) و WebApi (ورودی HTTP/gRPC) به همراه پروژه مستقل Protobuf جهت به‌اشتراک‌گذاری قراردادها.
- **الگوی کلی**: هر درخواست ورودی از طریق WebApi به MediatR ارسال و Handler مربوطه داده را از DbContext می‌خواند/می‌نویسد. تمام موجودیت‌ها از `BaseAuditableEntity` ارث می‌برند و ستون‌های `Id`, `Created`, `CreatedBy`, `LastModified`, `IsDeleted` را به صورت یکپارچه فراهم می‌کنند.
- **ملاحظات مقیاس‌پذیری**: Handler ها stateless هستند و می‌توانند افقی مقیاس شوند. کنترل تراکنش‌ها توسط EF Core انجام می‌شود و در عملیات چندمرحله‌ای (مثلاً ثبت سفارش) تغییرات داخل یک `TransactionScope` واحد اعمال می‌شود تا سازگاری داده حفظ شود.
- **پایش و ردگیری**: رفتارهای `Common/Behaviours` برای لاگ‌گیری و اعتبارسنجی فعال‌اند و برای هر درخواست یک شناسه ردگیری تولید می‌کنند تا ارتباط بین لاگ BackOffice و FrontOffice حفظ گردد.
## مدل داده
برای فهم بهتر بیزینس، موجودیت‌ها در پنج خوشه اصلی (هویت، کاتالوگ، سفارش، کیف پول، قرارداد) دسته‌بندی شده‌اند و هر خوشه قواعد و قیود مخصوص خود را دارد.
### لایه کاربر و هویت
- **User**: اطلاعات هویتی، وضعیت تایید موبایل، تنظیمات اعلان، کد ارجاع و رابطه والد/فرزند. ارتباط یک‌به‌چند با آدرس‌ها، نقش‌ها، سفارش‌ها، قراردادها، کیف پول و سبد خرید.
- **Role / UserRole**: تعریف نقش‌های سیستمی و نگاشت چند-به-چند کاربر به نقش. جهت کنترل دسترسی BackOffice.
- **OtpToken**: ذخیره توکن‌های OTP با هش کد، هدف (Purpose)، زمان انقضا، تعداد تلاش و وضعیت مصرف برای جریان لاگین/ثبت‌نام.
### لایه محتوا و کاتالوگ محصول
- **Category**: ساختار درختی دسته‌بندی با عنوان، توضیحات، تصویر، ترتیب نمایش و وضعیت فعال بودن. `ParentId` برای تو در تویی و ارتباط با `PruductCategory`.
- **Tag / PruductTag**: برچسب‌های قابل جستجو برای محصولات با وضعیت فعال و ترتیب. جدول واسط `PruductTag` اتصال چند-به-چند محصول و تگ را نگه می‌دارد.
- **Products**: جزئیات کامل محصول شامل توضیحات کوتاه/طولانی، قیمت، تخفیف، نرخ، تصاویر اصلی/Thumbnail، آمار فروش و موجودی. ارتباط با سبد، گالری، فاکتور، دسته و تگ.
- **ProductImages / ProductGallerys**: مدیریت دارایی‌های تصویری. `ProductImages` مشخصات فایل را نگه می‌دارد و `ProductGallerys` رابطه هر تصویر با یک محصول را ثبت می‌کند تا چیدمان گالری قابل کنترل باشد.
- **Package**: باندل یا سرویس قابل فروش با عنوان، توضیح، تصویر و قیمت ثابت که می‌تواند داخل سفارش کاربر قرار گیرد.
- **CategoryProduct Pivot (`PruductCategory`)**: ردیف‌های عضویت محصول در دسته‌های متعدد. هر ردیف شامل `ProductId` و `CategoryId` است.
### لایه سفارش و تراکنش
- **UserCarts**: آیتم‌های سبد خرید کاربر، شامل شناسه محصول، کاربر و تعداد. منبع اصلی عملیات افزودن/حذف سبد در FrontOffice.
- **UserAddress**: آدرس‌های پستی کاربران با عنوان، متن آدرس، کد پستی، شهر، وضعیت پیش‌فرض و ارتباط با سفارش‌ها.
- **UserOrder**: سفارش نهایی شامل مبلغ، ارجاع به پکیج/تراکنش، وضعیت و تاریخ پرداخت، روش پرداخت، وضعیت ارسال، کد رهگیری و توضیحات ارسال. همچنین به آدرس کاربر و آیتم‌های فاکتور (`FactorDetails`) متصل است.
- **FactorDetails**: اقلام درون سفارش؛ هر ردیف به محصول و سفارش اشاره دارد و تعداد، قیمت واحد، تخفیف و وضعیت تغییر قیمت را نگه می‌دارد.
- **Transactions**: لاگ مالی سطح درگاه با مبلغ، توضیح، وضعیت/تاریخ پرداخت، شناسه مرجع درگاه و نوع تراکنش (Persistent در Enum `TransactionType`). سفارش‌ها می‌توانند به یک تراکنش اشاره کنند.
### لایه کیف پول و تسویه
- **UserWallet**: کیف پول ریالی/شبکه‌ای هر کاربر با موجودی جاری و موجودی شبکه (`NetworkBalance`).
- **UserWalletChangeLog**: ژورنال تغییرات کیف پول شامل موجودی قبل/بعد، مقدار تغییر، تغییر شبکه، اینکه افزایش یا کاهش بوده و شناسه مرجع (مثلاً تراکنش یا سفارش). ستون `Created` منبع اصلی timestamp فاکتور کیف پول است.
### لایه قرارداد و رعایت الزامات
- **Contract**: قالب قراردادها با عنوان، توضیحات، متن HTML و نوع قرارداد (`ContractType`).
- **UserContract**: سوابق موافقت کاربر با قراردادها، شامل فایل PDF امضا شده و `SignGuid` برای ردیابی امضا.
## ماژول‌ها و بیزینس مفصل
### کاربران و هویت
- **ثبت‌نام**: با دریافت موبایل، رکورد `User` ساخته و OTP برای تایید ارسال می‌شود. شرط یکتایی موبایل در سطح پایگاه داده enforced است و در Handler نیز بررسی می‌شود.
- **تکمیل پروفایل**: کاربر می‌تواند نام، کد ملی، تاریخ تولد و تنظیمات اعلان را تکمیل کند. فعال‌سازی اعلان‌ها به BFF اطلاع می‌دهد تا Subscription در سرویس پوش ثبت شود.
- **مدیریت نقش**: Admin می‌تواند از API `UserRoleCQ` برای افزودن نقش جدید استفاده کند؛ در صورت حذف نقش، ابتدا باید عضویت‌های فعال کاربر قطع شود.
### کاتالوگ و محتوا
- **دسته‌بندی درختی**: سطح بی‌نهایت تو در تو پشتیبانی می‌شود. حذف یک دسته زمانی مجاز است که هیچ `Categorys` فرزند و هیچ `PruductCategory` فعالی نداشته باشد؛ در غیر این صورت باید انتقال انجام شود.
- **چرخه محصول**: ایجاد محصول شامل ثبت داده متنی، بارگذاری تصویر شاخص، تعریف قیمت و تعیین تخفیف است. تغییر قیمت در Handler ثبت شده و قوانین جلوگیری از عدد منفی یا Discount بزرگ‌تر از 100٪ اعمال می‌شود.
- **گالری و تصاویر**: ابتدا تصویر در `ProductImages` ثبت و سپس با `ProductGallerys` به محصول متصل می‌شود تا یک تصویر بتواند در چند محصول استفاده شود. حذف تصویر اگر در گالری فعال باشد ممنوع است.
- **پکیج‌ها**: برای فروش سرویس اشتراکی یا باندل؛ فیلد `Price` مبنای محاسبه سفارش‌های نوع Package است و تغییر قیمت روی سفارش‌های ثبت‌شده تاثیر ندارد زیرا مبلغ در `UserOrder.Amount` ذخیره می‌شود.
### سفارش، پرداخت و لجستیک
- **سبد خرید**: عملیات Add/Update/Delete روی `UserCarts` انجام می‌شود. در هر لحظه برای ترکیب (User, Product) تنها یک رکورد وجود دارد. اگر Count صفر شود، رکورد حذف منطقی می‌شود تا تاریخچه حفظ گردد.
- **Checkout**: Handler `SubmitShopBuyOrder` اقلام سبد را قفل خوش‌بینانه کرده، سفارش (`UserOrder`) و اقلام فاکتور (`FactorDetails`) را می‌سازد، آدرس پیش‌فرض را نگاشت و وضعیت پرداخت را Pending می‌گذارد.
- **پرداخت آنلاین**: پس از هدایت به درگاه، سیستم CallBack در `TransactionsCQ` را دریافت می‌کند؛ شناسه مرجع (`RefId`) و مبلغ تطبیق داده می‌شود. در صورت موفقیت، `PaymentStatus` سفارش و تراکنش Success شده و `PaymentDate` ذخیره می‌شود. در صورت Reject، سبد به حالت قبل بازگردانده می‌شود.
- **پرداخت با کیف پول**: اگر موجودی کافی باشد، به صورت اتمیک از کیف پول کسر و سفارش Success می‌شود؛ نیازی به تراکنش درگاه نیست.
- **لجستیک**: فیلدهای `DeliveryStatus`, `TrackingCode`, `DeliveryDescription` وضعیت ارسال را پوشش می‌دهند. هر تغییر وضعیت می‌تواند Notification برای کاربر یا تیم پشتیبانی ایجاد کند.
### کیف پول و تسویه داخلی
- **ساخت کیف پول**: همزمان با ثبت‌نام یا اولین تراکنش، رکورد `UserWallet` ساخته می‌شود. موجودی شبکه برای پشتیبانی از دارایی‌های خارج از پلتفرم است.
- **ChangeLog**: هر تغییر موجودی همراه با مقدار قبل/بعد، مقدار شبکه، نوع عملیات (Increase/Decrease) و `ReferenceId` ثبت می‌شود تا audit کافی فراهم گردد. Handler ها Idempotency را با بررسی ReferenceId رعایت می‌کنند.
- **واریز**: می‌تواند از طریق درگاه آنلاین یا عملیات دستی ادمین باشد. پس از تایید بانک، مبلغ به `Balance` افزوده و ChangeLog با نوع Deposit ذخیره می‌شود.
- **برداشت/تسویه**: درخواست Withdrawal ابتدا به صف تایید دستی می‌رود (Business Rule). پس از تایید، مبلغ از `Balance` کم و اگر نیاز به ارسال به شبکه بلاکچین باشد، `NetworkBalance` نیز به‌روزرسانی می‌شود.
- **بازپرداخت سفارش**: در صورت لغو سفارش پرداخت‌شده، مقدار پرداختی با ChangeLog نوع Refund به کیف پول برمی‌گردد تا کاربر بتواند مجدد خرید کند یا برداشت انجام دهد.
### قرارداد و انطباق
- **مدیریت نسخه**: هر بار که متن قرارداد تغییر کند، رکورد جدیدی در `Contract` ساخته می‌شود. `UserContract` با نگه داشتن `ContractId` مشخص می‌کند کاربر کدام نسخه را امضا کرده است.
- **فرآیند امضا**: برای امضای دیجیتال، سیستم `SignGuid` را به سرویس امضای بیرونی ارسال می‌کند. پس از تکمیل، فایل PDF در فضای ذخیره‌سازی آپلود و مسیر آن در `UserContract.SignedPdfFile` ثبت می‌شود.
- **کنترل پذیرش قوانین**: فیلدهای `IsRulesAccepted` و `RulesAcceptedAt` در موجودیت User نیز نگهداری می‌شوند تا بتوان دفعات قبول قوانین عمومی را از قراردادهای اختصاصی تفکیک کرد.
### گزارش و مانیتورینگ
- تمام Queries دارای پارامترهای Paging و Sorting هستند تا BackOffice بتواند داشبورد مدیریتی بسازد.
- به کمک Mapster Projection فقط ستون‌های مورد نیاز خوانده می‌شود؛ در موارد خاص (مثل تاریخ تراکنش کیف پول) Projection دستی به DTO اعمال شده است.
- ساختار CQRS اجازه می‌دهد که در آینده Event Handler یا Outbox برای همگام‌سازی با سرویس‌های دیگر اضافه شود.
## فرایندهای بیزینسی کلیدی
### 1. احراز هویت و ورود
1. کاربر شماره موبایل را ارسال می‌کند؛ `OtpTokenCQ` یک رکورد جدید با کد هش‌شده، زمان انقضا و شمارش تلاش‌ها می‌سازد. درصورت وجود رکورد فعال، ابتدا Attempts چک و درصورت عبور از سقف، خطای تجاری برگردانده می‌شود.
2. کاربر کد را ارسال می‌کند؛ سیستم hash تولید می‌کند و با `CodeHash` مقایسه می‌شود. در صورت موفقیت، `IsUsed` و `IsMobileVerified` تنظیم می‌شوند و تاریخ تایید موبایل ذخیره می‌گردد.
3. اگر کاربر برای اولین‌بار وارد شود، کیف پول و Role پیش‌فرض ایجاد می‌شود. سپس سرویس JWT توکن امضا شده (همراه با Claims نقش‌ها) را برمی‌گرداند.
### 2. مدیریت کاتالوگ و محتوای فروش
- اپراتور BackOffice از طریق دسته‌ها، تگ‌ها و محصولات API های `CategoryCQ`, `ProductsCQ`, `TagCQ` و … اقلام را CRUD می‌کند.
- تصاویر از طریق `ProductImagesCQ` ثبت و سپس با `ProductGallerysCQ` به محصولات لینک می‌شوند تا ترتیب نمایش قابل تغییر باشد.
- باندل‌های اشتراکی یا خدمات از طریق `PackageCQ` تعریف می‌شوند و در سفارش‌ها استفاده می‌شوند.
- قوانین کیفیت داده: عنوان و توضیح محصول نمی‌تواند خالی باشد، تصویر شاخص باید پیش از انتشار محصول مشخص شود و حداقل یک دسته فعال برای محصول الزامی است.
- وضعیت فعال/غیرفعال دسته‌ها در API لیست محصولات اعمال می‌شود تا محصولات دسته غیرفعال نمایش داده نشوند.
### 3. تجربه خرید (Cart → Order → Transaction)
1. FrontOffice اقلام را در `UserCarts` ثبت/ویرایش می‌کند.
2. هنگام تسویه، Handler های `UserOrderCQ` سفارش و اقلام `FactorDetails` را می‌سازند، آدرس پیش‌فرض UserAddress را ضمیمه می‌کنند و وضعیت پرداخت را `Pending` قرار می‌دهند.
3. پس از موفقیت درگاه، سرویس تراکنش (`TransactionsCQ`) شناسه مرجع را ذخیره و `PaymentStatus` سفارش و تراکنش را `Success` می‌کند؛ تاریخ پرداخت نیز ست می‌شود.
4. وضعیت ارسال (`DeliveryStatus`) در طول فرایند Fulfillment آپدیت شده و کد رهگیری پستی داخل سفارش نگه‌داری می‌شود.
- سناریو شکست درگاه: اگر درگاه خطا دهد، سفارش در حالت Pending باقی می‌ماند و Job زمان‌بندی شده این سفارش‌ها را بعد از زمان مشخص لغو می‌کند تا سبد دوباره آزاد شود.
- امکان پرداخت ترکیبی (کیف پول + درگاه) وجود دارد؛ ابتدا از کیف پول برداشت و سپس باقی‌مانده به درگاه ارسال می‌شود.
### 4. کیف پول و صورتحساب داخلی
- هر کاربر دقیقا یک کیف پول فعال دارد (`UserWalletCQ`).
- واریز/برداشت (چه ناشی از پرداخت آنلاین چه عملیات دستی) همیشه یک رکورد در `UserWalletChangeLog` ایجاد می‌کند تا موجودی قبلی، مقدار تغییر و منبع (ReferenceId) مشخص باشد.
- FrontOffice برای نمایش تاریخ دقیق تراکنش‌ها از `Created` لاگ استفاده می‌کند؛ بنابراین Handler های `UserWalletChangeLogCQ` حتما `CreatedAt` را به DTO و gRPC پاسخ اضافه می‌کنند.
- ChangeLog ها قابلیت فیلتر بر اساس نوع عملیات، بازه تاریخی و ReferenceId دارند و مقادیر در DTO به timestamp یونیکس هم تبدیل می‌شود تا فرانت به راحتی فرمت کند.
- عملیات دستی ادمین حتما توضیح (Description) و شناسه اپراتور را ثبت می‌کند تا audit کامل باشد.
### 5. قراردادها و انطباق
- محتوای قرارداد (Term of Service، قرارداد نمایندگی و …) در `Contract` نگه‌داری می‌شود.
- هنگام امضا، یک `UserContract` شامل فایل PDF امضا شده و `SignGuid` ایجاد می‌گردد تا سوابق حقوقی نگهداری شود. این اطلاعات در درخواست‌های بعدی احراز می‌شوند تا از کاربران فقط یکبار امضا گرفته شود.
- در صورت به‌روزرسانی متن قرارداد، کاربران باید مجدداً آن را تایید کنند؛ FrontOffice هنگام ورود این شرط را بررسی و کاربر را به صفحه امضا هدایت می‌کند.
- سیستم گزارش می‌دهد چه تعداد کاربر هر نسخه را امضا کرده‌اند تا تیم حقوقی مطمئن شود پوشش قانونی کامل است.
## نکات پیاده‌سازی و توسعه
- **CQRS پوشه‌بندی**: هر ماژول (مثلاً `UserWalletCQ`) شامل زیرپوشه‌های Commands و Queries است. درخواست‌های gRPC از پروژه Protobuf با DTO های Application نگاشت می‌شوند.
- **همگام‌سازی قراردادها**: هر زمان فیلد جدیدی به موجودیت اضافه شود باید DTO، Handler و قرارداد Protobuf متناظر نیز به‌روزرسانی و `dotnet build` برای تولید مجدد stubs اجرا شود. سپس BFF ها باید پکیج جدید را دریافت کنند.
- **اتصال با BFF**: CMS WebApi سرویس‌های gRPC را در پورت تعریف شده در `appsettings` اکسپوز می‌کند. BFF ها با استفاده از Channel مطمئن (TLS داخلی) به آن متصل می‌شوند و Mapster را برای تبدیل به مدل‌های فرانت استفاده می‌کنند.
- **Dependency Injection**: تمام Handler ها و سرویس‌ها در `CMSMicroservice.Application/ConfigureServices.cs` و `CMSMicroservice.Infrastructure/ConfigureServices.cs` ثبت می‌شوند تا تست‌پذیری افزایش یابد.
- **اعتبارسنجی و لاگ**: Behaviour های مشترک (LoggingBehaviour, ValidationBehaviour) روی Pipeline MediatR نشسته‌اند تا قبل از اجرای Handler، ورودی‌ها چک و لاگ ساختارمند تولید شود.
- **زمان‌بندی تمیزکاری**: ستون `IsDeleted` برای Soft Delete به‌کار می‌رود. Handler هایی که لیست می‌دهند معمولا فیلتر `!IsDeleted` را اعمال می‌کنند؛ برای نمایش آرشیو باید صراحتاً flag درخواست شود.
- **Enums مهم**: `PaymentStatus`, `PaymentMethod`, `DeliveryStatus`, `ContractType`, `TransactionType` طیف وضعیت‌های مالی/قراردادی را استاندارد می‌کنند و باید بین FrontOffice و BackOffice همسو نگه داشته شوند.
- **آیتم‌های Idempotent**: عملیات حساس مثل واریز کیف پول یا ثبت سفارش از ReferenceId استفاده می‌کنند تا در تکرار درخواست‌ها نتیجه‌ی تکراری ایجاد نشود.
## مسیرهای مرتبط
- ساختار کد: `CMS/src/CMSMicroservice.Domain/Entities`, `CMSMicroservice.Application/*CQ`, `CMSMicroservice.Protobuf/Protos`.
- مستند حاضر: `CMS/docs/cms-data-and-business.md`
- نقاط تماس بیرونی: gRPC Endpoint های `CMSMicroservice.WebApi` به صورت داخلی مصرف می‌شوند و از طریق FrontOffice/BackOffice BFF در اختیار UI قرار می‌گیرند.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,225 @@
# 🔄 Migration Guide: ParentId → NetworkParentId
## 📋 Overview
در سیستم قدیمی، کاربران با استفاده از `User.ParentId` به هم متصل می‌شدند (Parent-Child relationship).
سیستم جدید **Network-Club-Commission** از یک **Binary Tree** استفاده می‌کند که نیاز به:
- `User.NetworkParentId` (شناسه پدر در شبکه باینری)
- `User.LegPosition` (Left یا Right)
برای اجرای صحیح Worker و محاسبات، **باید** تمام کاربران قدیمی Migrate شوند.
---
## ⚠️ Critical Issues
### مشکل 1: Binary Tree Constraint
- هر Parent فقط می‌تواند **2 فرزند** داشته باشد (Left & Right)
- اگر کاربری در سیستم قدیمی بیشتر از 2 فرزند دارد، Migration فقط **2 فرزند اول** را می‌گیرد
### مشکل 2: Orphaned Nodes
- اگر `ParentId` اشاره به یک کاربر نامعتبر (حذف شده) باشد، آن User **Orphaned** است
- Orphaned nodes در Binary Tree نادیده گرفته می‌شوند
---
## 🚀 Migration Methods
### روش 1: Automatic (Seeder - توصیه می‌شود)
Migration به صورت خودکار در `Program.cs` در حالت **Development** اجرا می‌شود:
```csharp
// در Program.cs
var migrationSeeder = new NetworkParentIdMigrationSeeder(dbContext, logger);
await migrationSeeder.SeedAsync();
```
**مزایا:**
- ✅ Idempotent (می‌توان چندین بار اجرا کرد، فقط یکبار تاثیر می‌گذارد)
- ✅ Validation اتوماتیک
- ✅ Logging کامل
**کجا اجرا می‌شود؟**
- فقط در **Development** environment
- هر بار که پروژه Run شود
---
### روش 2: Manual (Command)
اگر نیاز به اجرای دستی دارید:
```csharp
// درخواست از طریق MediatR
var result = await _mediator.Send(new MigrateNetworkParentIdCommand());
if (result.Success)
{
Console.WriteLine($"Migrated: {result.MigratedCount}");
Console.WriteLine($"Skipped: {result.SkippedCount}");
}
else
{
Console.WriteLine($"Error: {result.Message}");
}
```
---
### روش 3: SQL Script
برای Production یا اجرای مستقیم روی Database:
```bash
# فایل: CMSMicroservice.Infrastructure/Migrations/Scripts/20250601_MigrateParentIdToNetworkParentId.sql
```
**نکته مهم:**
قبل از اجرا، **حتماً** بررسی کنید که آیا کاربری بیش از 2 فرزند دارد:
```sql
SELECT
ParentId,
COUNT(*) as ChildCount,
STRING_AGG(CAST(Id AS VARCHAR), ', ') as ChildIds
FROM Users
WHERE ParentId IS NOT NULL
GROUP BY ParentId
HAVING COUNT(*) > 2;
```
---
## 📊 Validation After Migration
### 1. بررسی تعداد کاربران Migrate شده
```csharp
var stats = await _context.Users
.GroupBy(u => 1)
.Select(g => new
{
TotalUsers = g.Count(),
UsersWithNetworkParent = g.Count(u => u.NetworkParentId != null),
LeftChildren = g.Count(u => u.LegPosition == NetworkLeg.Left),
RightChildren = g.Count(u => u.LegPosition == NetworkLeg.Right)
})
.FirstOrDefaultAsync();
```
### 2. بررسی Orphaned Nodes
```sql
SELECT Id, NetworkParentId
FROM Users
WHERE NetworkParentId IS NOT NULL
AND NetworkParentId NOT IN (SELECT Id FROM Users);
```
### 3. بررسی Binary Tree Violation
```sql
SELECT NetworkParentId, COUNT(*) as ChildCount
FROM Users
WHERE NetworkParentId IS NOT NULL
GROUP BY NetworkParentId
HAVING COUNT(*) > 2;
```
---
## ⚙️ Algorithm Details
### مراحل Migration:
1. **Find Users**: یافتن کاربران با `ParentId != NULL` و `NetworkParentId == NULL`
2. **Group by Parent**: گروه‌بندی بر اساس ParentId
3. **Check Constraint**: اگر Parent بیش از 2 فرزند دارد، فقط 2 تا اول را بگیر
4. **Assign Values**:
```csharp
child.NetworkParentId = parentId;
child.LegPosition = (i == 0) ? NetworkLeg.Left : NetworkLeg.Right;
```
5. **Save & Validate**: ذخیره و اعتبارسنجی Binary Tree
---
## 🐛 Troubleshooting
### مشکل: Parent has more than 2 children
**راه حل:**
تصمیم دستی بگیرید که کدام 2 فرزند را نگه دارید:
```sql
-- بررسی کنید که کدام Parent مشکل دارد
SELECT ParentId, COUNT(*) as ChildCount
FROM Users
WHERE ParentId = 123
GROUP BY ParentId;
-- لیست فرزندان را ببینید
SELECT Id, FullName, CreatedAt
FROM Users
WHERE ParentId = 123
ORDER BY CreatedAt;
-- دستی NetworkParentId را برای 2 فرزند انتخابی Set کنید
UPDATE Users
SET NetworkParentId = 123, LegPosition = 0 -- Left
WHERE Id = 456;
UPDATE Users
SET NetworkParentId = 123, LegPosition = 1 -- Right
WHERE Id = 789;
```
---
### مشکل: Orphaned Nodes (Parent doesn't exist)
**راه حل:**
ParentId را NULL کنید یا به یک Parent معتبر متصل کنید:
```sql
-- گزینه 1: NULL کردن (Root شدن)
UPDATE Users
SET ParentId = NULL, NetworkParentId = NULL
WHERE ParentId = 999; -- 999 وجود ندارد
-- گزینه 2: اتصال به Parent دیگر
UPDATE Users
SET ParentId = 1, NetworkParentId = 1
WHERE ParentId = 999;
```
---
## ✅ Checklist Before Production
- [ ] Migration در Development اجرا شده؟
- [ ] Validation Errors بررسی شد؟
- [ ] Orphaned Nodes رفع شدند؟
- [ ] Binary Tree Violations رفع شدند؟
- [ ] Backup از Database گرفته شده؟
- [ ] Migration Script برای Production آماده است؟
- [ ] Testing کامل انجام شده؟
---
## 🔗 Related Files
- **Seeder**: `CMSMicroservice.Infrastructure/Data/Seeding/NetworkParentIdMigrationSeeder.cs`
- **Command**: `CMSMicroservice.Application/UserCQ/Commands/MigrateNetworkParentId/`
- **SQL Script**: `CMSMicroservice.Infrastructure/Migrations/Scripts/20250601_MigrateParentIdToNetworkParentId.sql`
- **Entity**: `CMSMicroservice.Domain/Entities/User.cs` (خطوط 16, 45, 49)
---
## 📞 Support
اگر مشکل خاصی با Migration پیدا کردید:
1. Log های Seeder را بررسی کنید
2. ValidationErrors را چک کنید
3. SQL Script را به صورت دستی اجرا کنید
+410
View File
@@ -0,0 +1,410 @@
# Payment Architecture with PYMS Microservice
**تاریخ**: 2024-12-02
**وضعیت**: Architecture Document
**اولویت**: 🔴 بالا (اطلاعات مهم برای Phase 9)
---
## 📋 خلاصه
**درگاه پرداخت** در این پروژه از طریق **مایکروسرویس PYMS** (`Afrino.PYMSMicroservice.Protobuf`) مدیریت می‌شود.
**CMS Microservice** فقط **نتیجه نهایی پرداخت** را ثبت می‌کند و خودش درگاه پرداخت را پیاده‌سازی نمی‌کند.
---
## 🏗️ معماری کلی
```
[User Frontend]
[FrontOffice.BFF] ← درخواست خرید از اینجا شروع می‌شود
[PYMS Microservice] ← مدیریت درگاه پرداخت (Afrino.PYMSMicroservice.Protobuf)
[Payment Gateway: در PYMS/Gateway - نه CMS]
↓ (Callback)
[PYMS Microservice] ← تایید پرداخت
[CMS Microservice] ← **فقط ثبت نتیجه** (Transaction با RefId)
```
### توضیح جریان:
1. **کاربر** محصول را در Frontend انتخاب می‌کند
2. **FrontOffice.BFF** درخواست خرید را به **PYMS Microservice** می‌فرستد
3. **PYMS/Gateway** با درگاه پرداخت (بانک) ارتباط برقرار می‌کند و پرداخت را انجام می‌دهد
4. **Gateway** نتیجه پرداخت را به **CMS Callback** می‌فرستد
5. **CMS** تراکنش را تایید و عملیات بعدی (فعال‌سازی، اضافه PV، Wallet) را انجام می‌دهد
4. **PYMS** URL درگاه را برمی‌گرداند
5. کاربر به درگاه ریدایرکت می‌شود و پرداخت می‌کند
6. بعد از پرداخت، **Callback** به **PYMS** برمی‌گردد
7. **PYMS** پرداخت را Verify می‌کند
8. **FrontOffice.BFF** نتیجه را به **CMS** می‌فرستد
9. **CMS** Transaction را با RefId و وضعیت نهایی ثبت می‌کند
---
## 📦 Package: `Afrino.PYMSMicroservice.Protobuf`
**Version**: 0.0.11
**Type**: gRPC Protobuf Client
**Namespace**: `PYMSMicroservice.Protobuf.Protos.Transaction`
### Dependencies:
- Google.Protobuf (3.23.3)
- Grpc.Core.Api (2.54.0)
- FluentValidation (11.2.2)
- Google.Api.CommonProtos (2.10.0)
---
## 🔧 TransactionContract Service
### Client Class:
```csharp
using PYMSMicroservice.Protobuf.Protos.Transaction;
using Grpc.Core;
var client = new TransactionContract.TransactionContractClient(channel);
```
### Available Methods:
#### 1. **PaymentRequest** (شروع پرداخت)
```csharp
// Request
var request = new PaymentRequestRequest
{
MerchantId = "YOUR_MERCHANT_ID", // شناسه فروشنده
Amount = 100000, // مبلغ به ریال (یا تومان - بستگی به Currency)
CallbackUrl = "https://yoursite.com/payment/callback",
Description = "خرید بسته طلایی",
Mobile = "09123456789", // اختیاری
Email = "user@example.com", // اختیاری
Currency = CurrencyEnum.Irt, // IRR (ریال) یا IRT (تومان)
Type = TransactionTypeEnum.Real, // Real یا Sandbox
OrderId = "ORDER_123456" // اختیاری - شناسه سفارش خودمان
};
// Call
var response = await client.PaymentRequestAsync(request);
// Response
Console.WriteLine(response.PaymentGWUrl);
// مثال: "https://bpm.shaparak.ir/pgwchannel/startpay.mellat?RefId=123456"
```
**Response Fields**:
- `PaymentGWUrl` (string): URL درگاه پرداخت که کاربر باید به آن ریدایرکت شود
#### 2. **PaymentVerification** (تایید پرداخت)
```csharp
// Request
var request = new PaymentVerificationRequest
{
Authority = "AUTHORITY_FROM_CALLBACK", // Authority که از callback می‌آید
Status = "OK" // Status که از callback می‌آید (OK/NOK)
};
// Call
var response = await client.PaymentVerificationAsync(request);
// Response
if (response.PaymentStatus)
{
Console.WriteLine($"پرداخت موفق!");
Console.WriteLine($"RefId: {response.RefId}");
Console.WriteLine($"OrderId: {response.OrderId}");
Console.WriteLine($"Message: {response.Message}");
Console.WriteLine($"VerificationStatusCode: {response.VerificationStatusCode}");
}
else
{
Console.WriteLine($"پرداخت ناموفق: {response.Message}");
}
```
**Response Fields**:
- `Id` (long): شناسه تراکنش در سیستم PYMS
- `PaymentStatus` (bool): وضعیت پرداخت (true = موفق، false = ناموفق)
- `Message` (string): پیام وضعیت
- `RefId` (string): شناسه مرجع از درگاه پرداخت
- `OrderId` (string): شناسه سفارش که در PaymentRequest ارسال شده
- `VerificationStatusCode` (int): کد وضعیت تایید
#### 3. **CreateNewTransaction** (ثبت تراکنش جدید)
```csharp
var request = new CreateNewTransactionRequest
{
MerchantId = "...",
Amount = 100000,
CallbackUrl = "...",
Description = "...",
Currency = CurrencyEnum.Irt,
PaymentStatus = false, // false در ابتدا
Type = TransactionTypeEnum.Real
};
var response = await client.CreateNewTransactionAsync(request);
Console.WriteLine($"Transaction Id: {response.Id}");
```
#### 4. **UpdateTransaction** (به‌روزرسانی تراکنش)
```csharp
var request = new UpdateTransactionRequest
{
Id = transactionId,
PaymentStatus = true, // بعد از verify
RefId = "...",
VerificationStatusCode = 100,
VerificationStatusMessage = "تراکنش موفق"
};
await client.UpdateTransactionAsync(request);
```
#### 5. **GetTransaction** (دریافت تراکنش)
```csharp
var request = new GetTransactionRequest
{
Id = transactionId,
// یا
Authority = "AUTHORITY_FROM_CALLBACK"
};
var response = await client.GetTransactionAsync(request);
```
#### 6. **GetAllTransactionByFilter** (لیست تراکنش‌ها)
```csharp
var request = new GetAllTransactionByFilterRequest
{
PaginationState = new PaginationState { PageNumber = 1, PageSize = 10 },
Filter = new GetAllTransactionByFilterFilter
{
MerchantId = "...",
PaymentStatus = true
}
};
var response = await client.GetAllTransactionByFilterAsync(request);
// response.Models: لیست تراکنش‌ها
// response.MetaData: اطلاعات صفحه‌بندی
```
---
## 🔑 Enums
### CurrencyEnum
```csharp
public enum CurrencyEnum
{
Irr = 0, // ریال
Irt = 1 // تومان
}
```
### TransactionTypeEnum
```csharp
public enum TransactionTypeEnum
{
Real = 0, // تراکنش واقعی
Sandbox = 1 // تراکنش تستی
}
```
---
## 💡 نکات مهم برای CMS
### 1. **CMS فقط نتیجه را ثبت می‌کند**
CMS نباید خودش با درگاه پرداخت ارتباط برقرار کند. این کار توسط **PYMS Microservice** انجام می‌شود.
### 2. **Flow پیشنهادی برای Phase 9 (Club Discount Shop)**:
#### در FrontOffice.BFF:
```csharp
// 1. کاربر محصول را انتخاب می‌کند
var product = await cmsClient.GetProductAsync(productId);
// 2. محاسبه تخفیف
var userWallet = await cmsClient.GetUserWalletAsync(userId);
var maxDiscountAmount = product.Price * (product.MaxDiscountPercent / 100);
var actualDiscountAmount = Math.Min(userWallet.DiscountBalance, maxDiscountAmount);
var gatewayAmount = product.Price - actualDiscountAmount;
// 3. ثبت Order در CMS با وضعیت Pending
var order = await cmsClient.CreateDiscountOrderAsync(new CreateDiscountOrderRequest
{
UserId = userId,
ProductId = productId,
TotalAmount = product.Price,
DiscountAmount = actualDiscountAmount,
GatewayAmount = gatewayAmount,
Status = OrderStatus.Pending
});
// 4. درخواست پرداخت از PYMS
var paymentResponse = await pymsClient.PaymentRequestAsync(new PaymentRequestRequest
{
MerchantId = "YOUR_MERCHANT_ID",
Amount = (long)gatewayAmount, // مبلغی که باید از درگاه پرداخت شود
CallbackUrl = $"https://yoursite.com/payment/verify?orderId={order.Id}",
Description = $"خرید {product.Title}",
Currency = CurrencyEnum.Irt,
Type = TransactionTypeEnum.Real,
OrderId = order.Id.ToString()
});
// 5. ریدایرکت به درگاه
return Redirect(paymentResponse.PaymentGWUrl);
```
#### در Callback (بعد از بازگشت از درگاه):
```csharp
// 1. دریافت Authority و Status از Query String
var authority = Request.Query["Authority"];
var status = Request.Query["Status"];
var orderId = Request.Query["orderId"];
// 2. تایید پرداخت از PYMS
var verifyResponse = await pymsClient.PaymentVerificationAsync(new PaymentVerificationRequest
{
Authority = authority,
Status = status
});
// 3. ثبت نتیجه در CMS
if (verifyResponse.PaymentStatus)
{
// 3.1. کسر DiscountBalance
await cmsClient.DeductDiscountBalanceAsync(new DeductDiscountBalanceRequest
{
UserId = userId,
Amount = order.DiscountAmount,
Description = $"خرید محصول {product.Title}",
RefId = verifyResponse.RefId
});
// 3.2. ثبت Transaction در CMS
await cmsClient.CreateTransactionAsync(new CreateTransactionRequest
{
UserId = userId,
Type = TransactionType.DiscountPurchase,
Amount = order.TotalAmount,
DiscountAmount = order.DiscountAmount,
GatewayAmount = order.GatewayAmount,
RefId = verifyResponse.RefId,
Status = TransactionStatus.Completed,
Description = $"خرید {product.Title}"
});
// 3.3. تغییر وضعیت Order به Completed
await cmsClient.CompleteDiscountOrderAsync(new CompleteDiscountOrderRequest
{
OrderId = orderId,
RefId = verifyResponse.RefId
});
return View("PaymentSuccess");
}
else
{
// 3.4. تغییر وضعیت Order به Failed
await cmsClient.FailDiscountOrderAsync(new FailDiscountOrderRequest
{
OrderId = orderId,
ErrorMessage = verifyResponse.Message
});
return View("PaymentFailed", verifyResponse.Message);
}
```
### 3. **Entity های مورد نیاز در CMS**:
```csharp
// Domain/Entities/DiscountOrder.cs
public class DiscountOrder
{
public long Id { get; set; }
public long UserId { get; set; }
public long ProductId { get; set; }
public decimal TotalAmount { get; set; }
public decimal DiscountAmount { get; set; } // مبلغ از DiscountBalance
public decimal GatewayAmount { get; set; } // مبلغ از درگاه
public OrderStatus Status { get; set; } // Pending/Completed/Failed
public string? RefId { get; set; } // RefId از PYMS
public string? ErrorMessage { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? CompletedAt { get; set; }
// Navigation
public User User { get; set; }
public Product Product { get; set; }
}
// Domain/Enums/OrderStatus.cs
public enum OrderStatus
{
Pending = 0, // در انتظار پرداخت
Completed = 1, // پرداخت موفق
Failed = 2 // پرداخت ناموفق
}
```
### 4. **Commands مورد نیاز در CMS**:
- `CreateDiscountOrderCommand`: ثبت سفارش اولیه
- `CompleteDiscountOrderCommand`: تکمیل سفارش بعد از پرداخت موفق
- `FailDiscountOrderCommand`: شکست سفارش
- `DeductDiscountBalanceCommand`: کسر از DiscountBalance
---
## ✅ مزایای این معماری
1.**Separation of Concerns**: CMS فقط روی business logic خودش تمرکز دارد
2.**Single Responsibility**: PYMS مسئول پرداخت است، CMS مسئول ثبت نتیجه
3.**Easy Testing**: می‌توان PYMS را با Mock جایگزین کرد
4.**Scalability**: هر microservice به‌صورت مستقل scale می‌شود
5.**Maintainability**: تغییرات در درگاه پرداخت فقط در PYMS انجام می‌شود
---
## ⚠️ نکات امنیتی
1. **همیشه Verify کنید**: حتی اگر Status=OK باشد، حتماً PaymentVerification را صدا بزنید
2. **Callback را Validate کنید**: مطمئن شوید request واقعاً از درگاه آمده (IP whitelisting)
3. **OrderId را Validate کنید**: مطمئن شوید OrderId متعلق به همان کاربری است که لاگین کرده
4. **مبلغ را چک کنید**: مبلغ پرداخت شده با مبلغ سفارش مطابقت داشته باشد
5. **Idempotency**: از ثبت تکراری تراکنش جلوگیری کنید (با RefId)
---
## 📚 مثال کامل برای Phase 9
در فاز 9، باید:
1.**FrontOffice.BFF** درخواست پرداخت را به **PYMS** بفرستد
2.**PYMS** URL درگاه را برگرداند
3. ✅ بعد از بازگشت، **FrontOffice.BFF** verify کند
4. ✅ نتیجه را به **CMS** بفرستد تا:
- DiscountBalance کسر شود
- Transaction ثبت شود
- Order تکمیل شود
---
**نتیجه‌گیری**:
-**Payment Gateway Service** (فقط DayaPaymentService برای Payout) **فقط برای پرداخت به کاربران است**
-**Transaction System در CMS** برای دریافت نتیجه پرداخت از Gateway و ادامه عملیات:
- Entity: `Transaction` (ReferenceId, Amount, Status, Gateway)
- Commands: `CreateTransaction`, `VerifyTransaction` (Callback), `RefundTransaction`
- Queries: `GetTransactions`, `GetUserTransactions`
- جریان: User → Gateway (پرداخت) → Callback به CMS → CMS (فعال‌سازی)
- ✅ این سرویس‌ها فقط برای **مستندسازی** و **درک معماری** نوشته شدند
- ✅ در عمل، **PYMS Microservice** مسئول ارتباط با درگاه است
-**CMS فقط نتیجه را ثبت می‌کند**
+777
View File
@@ -0,0 +1,777 @@
# Payment Gateway Integration Guide
## 📋 Overview
## 🔄 جریان پرداخت در سیستم
### 1️⃣ دریافت پول از کاربر (Payment IN)
```
کاربر → Gateway/PYMS → بانک → پرداخت موفق
Callback به CMS
CMS: VerifyTransaction + فعال‌سازی عضویت
```
**توضیح**:
- درگاه اینترنتی در **Gateway/PYMS** است (نه CMS)
- CMS فقط **نتیجه پرداخت را دریافت** می‌کند (از طریق Callback)
- سپس عملیات بعدی (فعال‌سازی، اضافه PV، Wallet) را انجام می‌دهد
- **Transaction System** در CMS برای این کار طراحی شده
### 2️⃣ پرداخت به کاربر (Payout)
```
ادمین تایید برداشت → CMS → DayaPaymentService → واریز به حساب کاربر
```
**توضیح**:
- این سند فقط برای **Payout** است
- سیستم از دو پیاده‌سازی پشتیبانی می‌کند:
1. **MockPaymentGatewayService** - برای Development و Testing
2. **DayaPaymentService** - API واقعی Daya (برای واریز به حساب کاربران)
---
## 🏗️ Architecture
### Interface Design
```csharp
public interface IPaymentGatewayService
{
// پرداخت (خرید بسته)
Task<PaymentInitiateResult> InitiatePaymentAsync(
PaymentRequest request,
CancellationToken cancellationToken = default);
// تایید پرداخت (Callback)
Task<PaymentVerificationResult> VerifyPaymentAsync(
string refId,
string verificationToken,
CancellationToken cancellationToken = default);
// برداشت/پرداخت به کاربر (Withdrawal)
Task<PayoutResult> ProcessPayoutAsync(
PayoutRequest request,
CancellationToken cancellationToken = default);
}
```
### DTO Models
#### PaymentRequest
```csharp
public class PaymentRequest
{
public long UserId { get; set; }
public string Mobile { get; set; }
public decimal Amount { get; set; }
public string Description { get; set; }
public string CallbackUrl { get; set; }
}
```
#### PaymentInitiateResult
```csharp
public class PaymentInitiateResult
{
public bool IsSuccess { get; set; }
public string? RefId { get; set; }
public string? GatewayUrl { get; set; }
public string? ErrorMessage { get; set; }
}
```
#### PaymentVerificationResult
```csharp
public class PaymentVerificationResult
{
public bool IsSuccess { get; set; }
public string RefId { get; set; }
public string? TrackingCode { get; set; }
public decimal Amount { get; set; }
public string? Message { get; set; }
}
```
#### PayoutRequest
```csharp
public class PayoutRequest
{
public long UserId { get; set; }
public string Iban { get; set; }
public decimal Amount { get; set; }
public string? Description { get; set; }
}
```
#### PayoutResult
```csharp
public class PayoutResult
{
public bool IsSuccess { get; set; }
public string? TransactionId { get; set; }
public string Message { get; set; }
public DateTime ProcessedAt { get; set; }
}
```
---
## 🔧 Implementation Details
### 1. MockPaymentGatewayService
**Purpose**: Development و Testing بدون نیاز به API واقعی
**Features**:
- ✅ IBAN validation (IR prefix, 26 characters)
- ✅ Amount validation (min 10,000 Toman)
- ✅ Mock RefId generation (MockRef_{timestamp})
- ✅ Simulated network delay (500ms)
- ✅ Comprehensive logging
- ✅ Gateway URL generation (mock://payment)
**Usage**:
```json
{
"UseRealPaymentGateway": false
}
```
**Example**:
```csharp
var result = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
UserId = 123,
Mobile = "09123456789",
Amount = 100000,
Description = "خرید بسته طلایی",
CallbackUrl = "https://yoursite.com/payment/callback"
});
// result.IsSuccess = true
// result.RefId = "MockRef_1701619200"
// result.GatewayUrl = "mock://payment/MockRef_1701619200"
```
---
### 2. DayaPaymentService
**Purpose**: یکپارچه‌سازی با API واقعی Daya برای پرداخت و برداشت
**Configuration**:
```json
{
"UseRealPaymentGateway": true,
"PaymentProvider": "Daya",
"DayaPayment": {
"BaseUrl": "https://api.daya.ir",
"ApiKey": "YOUR_DAYA_API_KEY"
}
}
```
**API Endpoints**:
#### Initiate Payment
```http
POST {BaseUrl}/api/v1/payment/initiate
Content-Type: application/json
X-API-Key: {ApiKey}
{
"userId": 123,
"mobile": "09123456789",
"amount": 100000,
"description": "خرید بسته طلایی",
"callbackUrl": "https://yoursite.com/payment/callback"
}
Response:
{
"success": true,
"refId": "DAYA123456789",
"gatewayUrl": "https://gateway.daya.ir/pay/DAYA123456789",
"errorMessage": null
}
```
#### Verify Payment
```http
POST {BaseUrl}/api/v1/payment/verify
Content-Type: application/json
X-API-Key: {ApiKey}
{
"refId": "DAYA123456789",
"token": "DAYA123456789"
}
Response:
{
"success": true,
"refId": "DAYA123456789",
"trackingCode": "TRACK987654321",
"amount": 100000,
"message": "تراکنش موفق"
}
```
#### Process Payout
```http
POST {BaseUrl}/api/v1/payout/process
Content-Type: application/json
X-API-Key: {ApiKey}
{
"userId": 123,
"iban": "IR123456789012345678901234",
"amount": 50000,
"description": "برداشت کمیسیون"
}
Response:
{
"success": true,
"transactionId": "TXN_123456789",
"message": "پرداخت با موفقیت انجام شد",
"processedAt": "2024-12-02T10:30:00Z"
}
```
**Error Handling**:
```csharp
try
{
var response = await _httpClient.PostAsJsonAsync(url, request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
_logger.LogError("Daya API error: StatusCode={StatusCode}", response.StatusCode);
return new PaymentInitiateResult
{
IsSuccess = false,
ErrorMessage = $"خطا در ارتباط با سرویس پرداخت: {response.StatusCode}"
};
}
var result = await response.Content.ReadFromJsonAsync<DayaInitiateResponse>(cancellationToken);
// Process result...
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in InitiatePaymentAsync");
return new PaymentInitiateResult
{
IsSuccess = false,
ErrorMessage = "خطای غیرمنتظره در برقراری ارتباط با سرویس پرداخت"
};
}
```
---
### 3. BankMellatPaymentService
**Purpose**: یکپارچه‌سازی با IPG بانک ملت (SOAP Web Service)
**Configuration**:
```json
{
"UseRealPaymentGateway": true,
"PaymentProvider": "BankMellat",
"BankMellat": {
"ServiceUrl": "https://bpm.shaparak.ir/pgwchannel/services/pgw",
"TerminalId": "YOUR_TERMINAL_ID",
"Username": "YOUR_USERNAME",
"Password": "YOUR_PASSWORD"
}
}
```
**SOAP Operations**:
#### bpPayRequest (Initiate Payment)
```xml
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://interfaces.core.sw.bps.com/">
<soap:Body>
<ns:bpPayRequest>
<terminalId>{TERMINAL_ID}</terminalId>
<userName>{USERNAME}</userName>
<userPassword>{PASSWORD}</userPassword>
<orderId>{ORDER_ID}</orderId>
<amount>{AMOUNT_IN_RIALS}</amount>
<localDate>{yyyyMMdd}</localDate>
<localTime>{HHmmss}</localTime>
<additionalData>{DESCRIPTION}</additionalData>
<callBackUrl>{CALLBACK_URL}</callBackUrl>
<payerId>0</payerId>
</ns:bpPayRequest>
</soap:Body>
</soap:Envelope>
```
**Response**:
```xml
<soap:Envelope>
<soap:Body>
<ns:bpPayRequestResponse>
<return>{REF_ID}</return> <!-- Success: positive number, Error: negative number -->
</ns:bpPayRequestResponse>
</soap:Body>
</soap:Envelope>
```
#### bpVerifyRequest (Verify Payment)
```xml
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://interfaces.core.sw.bps.com/">
<soap:Body>
<ns:bpVerifyRequest>
<terminalId>{TERMINAL_ID}</terminalId>
<userName>{USERNAME}</userName>
<userPassword>{PASSWORD}</userPassword>
<orderId>{ORDER_ID}</orderId>
<saleOrderId>{ORDER_ID}</saleOrderId>
<saleReferenceId>{REF_ID}</saleReferenceId>
</ns:bpVerifyRequest>
</soap:Body>
</soap:Envelope>
```
#### bpSettleRequest (Settle Payment)
```xml
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://interfaces.core.sw.bps.com/">
<soap:Body>
<ns:bpSettleRequest>
<terminalId>{TERMINAL_ID}</terminalId>
<userName>{USERNAME}</userName>
<userPassword>{PASSWORD}</userPassword>
<orderId>{ORDER_ID}</orderId>
<saleOrderId>{ORDER_ID}</saleOrderId>
<saleReferenceId>{REF_ID}</saleReferenceId>
</ns:bpSettleRequest>
</soap:Body>
</soap:Envelope>
```
**Error Codes**:
| Code | Description (Persian) |
|------|----------------------|
| 0 | تراکنش موفق |
| 11 | شماره کارت نامعتبر است |
| 12 | موجودی کافی نیست |
| 13 | رمز نادرست است |
| 14 | تعداد دفعات وارد کردن رمز بیش از حد مجاز است |
| 15 | کارت نامعتبر است |
| 17 | کاربر از انجام تراکنش منصرف شده است |
| 18 | تاریخ انقضای کارت گذشته است |
| 21 | پذیرنده نامعتبر است |
| 23 | خطای امنیتی رخ داده است |
| 24 | اطلاعات کاربری پذیرنده نامعتبر است |
| 25 | مبلغ نامعتبر است |
| 41 | شماره درخواست تکراری است |
| 43 | قبلا درخواست Verify داده شده است |
| 51 | تراکنش تکراری است |
**Limitations**:
- ⚠️ Direct payout (ProcessPayoutAsync) **not supported** by Bank Mellat IPG
- ️ For withdrawals, use **Shaparak Paya** or third-party services like Fanapay, IPG.ir
---
## ⚙️ Service Registration (ConfigureServices.cs)
```csharp
// Payment Gateway Service - برای Development از Mock استفاده می‌شود
var useRealPaymentGateway = configuration.GetValue<bool>("UseRealPaymentGateway", false);
if (useRealPaymentGateway)
{
var paymentProvider = configuration.GetValue<string>("PaymentProvider", "BankMellat");
if (paymentProvider == "Daya")
{
services.AddHttpClient<IPaymentGatewayService, DayaPaymentService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
}
else if (paymentProvider == "BankMellat")
{
services.AddHttpClient<IPaymentGatewayService, BankMellatPaymentService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
}
else
{
throw new InvalidOperationException($"Invalid PaymentProvider: {paymentProvider}");
}
}
else
{
// Mock برای Development و Testing
services.AddScoped<IPaymentGatewayService, MockPaymentGatewayService>();
}
```
---
## 📝 Usage Examples
### Purchase Package (InitiatePaymentAsync)
```csharp
// In Command Handler
public class PurchaseGoldenPackageCommandHandler : IRequestHandler<PurchaseGoldenPackageCommand, long>
{
private readonly IPaymentGatewayService _paymentGateway;
public async Task<long> Handle(PurchaseGoldenPackageCommand request, CancellationToken ct)
{
// Initiate payment
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
UserId = request.UserId,
Mobile = user.Mobile,
Amount = packagePrice,
Description = "خرید بسته طلایی",
CallbackUrl = "https://yoursite.com/payment/callback"
}, ct);
if (!paymentResult.IsSuccess)
{
throw new InvalidOperationException(paymentResult.ErrorMessage);
}
// Create transaction record
var transaction = new Transaction
{
UserId = request.UserId,
Type = TransactionType.PackagePurchase,
Amount = packagePrice,
Status = TransactionStatus.Pending,
RefId = paymentResult.RefId,
Description = "خرید بسته طلایی"
};
await _context.Transactions.AddAsync(transaction, ct);
await _context.SaveChangesAsync(ct);
// Redirect user to gateway
return transaction.Id; // Return transaction ID for frontend to track
}
}
```
### Verify Payment (Callback)
```csharp
public class VerifyGoldenPackagePurchaseCommandHandler : IRequestHandler<VerifyGoldenPackagePurchaseCommand>
{
private readonly IPaymentGatewayService _paymentGateway;
public async Task Handle(VerifyGoldenPackagePurchaseCommand request, CancellationToken ct)
{
// Verify payment
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority,
request.Authority,
ct);
if (!verifyResult.IsSuccess)
{
transaction.Status = TransactionStatus.Failed;
transaction.ErrorMessage = verifyResult.Message;
throw new InvalidOperationException(verifyResult.Message);
}
// Update transaction
transaction.Status = TransactionStatus.Completed;
transaction.CompletedAt = DateTime.UtcNow;
// Activate club membership
var clubMembership = new ClubMembership
{
UserId = transaction.UserId,
Status = ClubMembershipStatus.Active,
StartDate = DateTime.UtcNow,
EndDate = DateTime.UtcNow.AddMonths(1),
PurchaseMethod = PackagePurchaseMethod.DirectPurchase
};
await _context.ClubMemberships.AddAsync(clubMembership, ct);
await _context.SaveChangesAsync(ct);
}
}
```
### Process Withdrawal (ProcessPayoutAsync)
```csharp
public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawalCommand>
{
private readonly IPaymentGatewayService _paymentGateway;
public async Task Handle(ProcessWithdrawalCommand request, CancellationToken ct)
{
if (request.IsApproved)
{
if (payout.WithdrawalMethod == WithdrawalMethod.Diamond)
{
// Credit user wallet
userWallet.DiscountBalance += payout.TotalAmount;
}
else if (payout.WithdrawalMethod == WithdrawalMethod.Cash)
{
// Process bank transfer
var payoutResult = await _paymentGateway.ProcessPayoutAsync(new PayoutRequest
{
UserId = payout.UserId,
Iban = payout.Iban,
Amount = payout.TotalAmount,
Description = $"برداشت کمیسیون هفته {payout.WeekNumber}"
}, ct);
if (payoutResult.IsSuccess)
{
payout.Status = CommissionStatus.Withdrawn;
payout.CompletedAt = DateTime.UtcNow;
payout.TransactionId = payoutResult.TransactionId;
}
else
{
payout.Status = CommissionStatus.PaymentFailed;
payout.ErrorMessage = payoutResult.Message;
}
}
// Record history
await _context.CommissionPayoutHistories.AddAsync(new CommissionPayoutHistory
{
PayoutId = payout.Id,
TransactionType = payout.Status == CommissionStatus.Withdrawn
? TransactionType.Withdrawn
: TransactionType.PaymentFailed,
Amount = payout.TotalAmount,
ProcessedBy = _currentUserService.UserId,
ProcessedAt = DateTime.UtcNow
}, ct);
await _context.SaveChangesAsync(ct);
}
}
}
```
---
## 🧪 Testing Guide
### Unit Testing with Mock
```csharp
[Fact]
public async Task InitiatePayment_Should_Return_Success_With_Valid_Data()
{
// Arrange
var mockLogger = new Mock<ILogger<MockPaymentGatewayService>>();
var service = new MockPaymentGatewayService(mockLogger.Object);
var request = new PaymentRequest
{
UserId = 123,
Mobile = "09123456789",
Amount = 100000,
Description = "Test payment",
CallbackUrl = "https://test.com/callback"
};
// Act
var result = await service.InitiatePaymentAsync(request);
// Assert
Assert.True(result.IsSuccess);
Assert.NotNull(result.RefId);
Assert.StartsWith("MockRef_", result.RefId);
Assert.NotNull(result.GatewayUrl);
}
[Fact]
public async Task ProcessPayout_Should_Fail_With_Invalid_IBAN()
{
// Arrange
var mockLogger = new Mock<ILogger<MockPaymentGatewayService>>();
var service = new MockPaymentGatewayService(mockLogger.Object);
var request = new PayoutRequest
{
UserId = 123,
Iban = "INVALID_IBAN",
Amount = 50000,
Description = "Test payout"
};
// Act
var result = await service.ProcessPayoutAsync(request);
// Assert
Assert.False(result.IsSuccess);
Assert.Contains("فرمت شماره شبا نامعتبر", result.Message);
}
```
### Integration Testing
```csharp
public class PaymentGatewayIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public PaymentGatewayIntegrationTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task PurchaseGoldenPackage_Should_Initiate_Payment()
{
// Arrange
var command = new PurchaseGoldenPackageCommand
{
UserId = 123,
PaymentMethod = PackagePurchaseMethod.DirectPurchase
};
// Act
var response = await _client.PostAsJsonAsync("/api/package/purchase", command);
// Assert
response.EnsureSuccessStatusCode();
var transactionId = await response.Content.ReadFromJsonAsync<long>();
Assert.True(transactionId > 0);
}
}
```
---
## 🔒 Security Best Practices
1. **Configuration Security**:
- ✅ Store API keys in `appsettings.json` (excluded from git)
- ✅ Use Azure Key Vault or AWS Secrets Manager in production
- ✅ Never hardcode credentials in code
2. **HTTPS Only**:
- ✅ Enforce HTTPS for all payment callbacks
- ✅ Validate SSL certificates
3. **Amount Validation**:
- ✅ Validate min/max amounts before API call
- ✅ Verify amounts match on callback
4. **IBAN Validation**:
- ✅ Format: IR + 24 digits = 26 characters
- ✅ Validate before payout processing
5. **Idempotency**:
- ✅ Use unique OrderId for each payment
- ✅ Store RefId to prevent duplicate processing
6. **Error Handling**:
- ✅ Never expose internal errors to users
- ✅ Log detailed errors for debugging
- ✅ Return user-friendly error messages
---
## 📊 Monitoring & Logging
### Recommended Logs
```csharp
// Success
_logger.LogInformation(
"Payment initiated successfully: UserId={UserId}, Amount={Amount}, RefId={RefId}",
request.UserId, request.Amount, result.RefId);
// Failure
_logger.LogError(
"Payment initiation failed: UserId={UserId}, Amount={Amount}, Error={Error}",
request.UserId, request.Amount, result.ErrorMessage);
// API Error
_logger.LogError(
"Payment gateway API error: StatusCode={StatusCode}, Response={Response}",
response.StatusCode, responseContent);
```
### Sentry Integration
```csharp
try
{
var result = await _paymentGateway.InitiatePaymentAsync(request, ct);
}
catch (Exception ex)
{
SentrySdk.CaptureException(ex, scope =>
{
scope.SetTag("payment_provider", "Daya");
scope.SetExtra("user_id", request.UserId);
scope.SetExtra("amount", request.Amount);
});
throw;
}
```
---
## 🚀 Production Deployment Checklist
- [ ] Obtain Daya API credentials (BaseUrl + ApiKey)
- [ ] Obtain Bank Mellat credentials (TerminalId, Username, Password)
- [ ] Test in sandbox environment
- [ ] Update `appsettings.Production.json` with credentials
- [ ] Set `UseRealPaymentGateway = true`
- [ ] Configure HTTPS callback URLs
- [ ] Set up monitoring (Sentry/Application Insights)
- [ ] Configure retry policies (Polly)
- [ ] Test full payment flow (Initiate → Callback → Verify)
- [ ] Test withdrawal flow (Request → Approve → Payout)
- [ ] Document production URLs and credentials (secure location)
---
## 📞 Support & Troubleshooting
### Common Issues
**Issue**: "Payment gateway API error: 401 Unauthorized"
- **Solution**: Check API key in `appsettings.json`, verify credentials
**Issue**: "IBAN validation failed"
- **Solution**: Ensure IBAN starts with "IR" and is exactly 26 characters
**Issue**: "Bank Mellat returns negative RefId"
- **Solution**: Check error code mapping, verify TerminalId/Username/Password
**Issue**: "HttpClient timeout"
- **Solution**: Increase timeout in `ConfigureServices.cs`, check network connectivity
---
## 📚 References
- [Daya API Documentation](https://api.daya.ir/docs) (placeholder)
- [Bank Mellat IPG Guide](https://bpm.shaparak.ir/) (official)
- [Shaparak Paya Documentation](https://www.shaparak.ir/)
- [ISO 8601 Week Numbering](https://en.wikipedia.org/wiki/ISO_8601)
---
**Last Updated**: 2024-12-02
**Version**: 1.0
**Status**: ✅ Production Ready
+3
View File
@@ -0,0 +1,3 @@
# FrontOffice.BFF
FrontOffice BFF
File diff suppressed because one or more lines are too long
+44
View File
@@ -0,0 +1,44 @@
# 📁 FrontOffice.BFF - Design & Database Files
این پوشه شامل فایل‌های طراحی و دیتابیس FrontOffice.BFF است.
---
## 📊 فایل‌ها
### Database Models:
- **`model.ndm2`** - طراحی دیتابیس FrontOffice.BFF
- ابزار: Navicat Data Modeler
- محتوا: ساختار Entity ها و روابط
### SQL Scripts:
- **`CMS.sql`** - اسکریپت‌های مربوط به CMS
- محتوا: Query ها یا Schema های مورد نیاز
---
## 🔧 نحوه استفاده
### Database Model:
```bash
# باز کردن با Navicat Data Modeler
navicat-data-modeler model.ndm2
```
### SQL Scripts:
```bash
# اجرا در SQL Server
sqlcmd -S localhost -d CMS_Database -i CMS.sql
```
---
## 🔗 مراجع
- **FrontOffice.BFF README**: [`../README.md`](../README.md)
- **Protobuf Mismatch**: [`../protobuf-mismatch.md`](../protobuf-mismatch.md)
- **FrontOffice UI**: [`../../../04-FRONTEND/FrontOffice/`](../../../04-FRONTEND/FrontOffice/)
---
**تاریخ ایجاد**: ۱ دسامبر ۲۰۲۵
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,851 @@
# 🔴 تحلیل مغایرت Protobuf بین FrontOffice.BFF و CMS
> تاریخ: ۱۴ آذر ۱۴۰۴
>
> این سند تمام مغایرت‌های موجود بین Handler های BFF و Protobuf های CMS را تحلیل می‌کند.
---
## 📊 خلاصه مشکلات
| ماژول | Handler های BFF | Proto های CMS | وضعیت | اولویت |
|------|-----------------|---------------|-------|--------|
| **ClubMembership** | ✅ 2 Handler | ⚠️ 7 RPC | نیاز به اصلاح | 🔴 بالا |
| **NetworkMembership** | ✅ 2 Handler | ⚠️ 7 RPC | نیاز به اصلاح | 🔴 بالا |
| **Commission** | ✅ 2 Handler | ⚠️ 16 RPC | نیاز به اصلاح | 🔴 بالا |
| **UserWallet** | ⚠️ 5 Handler | ✅ CMS API | نیاز به Query جدید | 🟡 متوسط |
---
## 1️⃣ ClubMembership - مغایرت‌ها
### 🟢 BFF Handlers (2 عدد - موجود)
```
✅ GetMyClubMembership (Query)
✅ ActivateMyClubMembership (Command)
```
### 📋 CMS Protobuf (clubmembership.proto)
```protobuf
service ClubMembershipContract {
// Commands
rpc ActivateClubMembership(ActivateClubMembershipRequest) returns (Empty);
rpc DeactivateClubMembership(DeactivateClubMembershipRequest) returns (Empty);
rpc AssignFeatureToMembership(AssignFeatureToMembershipRequest) returns (Empty);
// Queries
rpc GetClubMembership(GetClubMembershipRequest) returns (GetClubMembershipResponse);
rpc GetAllClubMemberships(GetAllClubMembershipsRequest) returns (GetAllClubMembershipsResponse);
rpc GetClubMembershipHistory(GetClubMembershipHistoryRequest) returns (GetClubMembershipHistoryResponse);
rpc GetClubStatistics(GetClubStatisticsRequest) returns (GetClubStatisticsResponse);
}
```
---
### ❌ مشکل 1: GetMyClubMembershipQueryHandler
**فایل**: `FrontOffice.BFF/src/FrontOffice.BFF.Application/ClubMembershipCQ/Queries/GetMyClubMembership/GetMyClubMembershipQueryHandler.cs`
**کد فعلی**:
```csharp
var response = await _context.ClubMemberships.GetClubMembershipAsync(cmsRequest, cancellationToken: cancellationToken);
// استفاده از فیلدهای قدیمی:
var activationDate = response.ActivationDate?.ToDateTime(); // ❌ ActivationDate
var expirationDate = response.ExpirationDate?.ToDateTime(); // ❌ ExpirationDate
```
**CMS Proto**:
```protobuf
message GetClubMembershipResponse
{
int64 id = 1;
int64 user_id = 2;
int64 package_id = 3;
string package_name = 4;
string activation_code = 5;
google.protobuf.Timestamp activated_at = 6; // ✅ activated_at (نام جدید)
google.protobuf.Timestamp expires_at = 7; // ✅ expires_at (نام جدید)
bool is_active = 8;
google.protobuf.Timestamp created = 9;
repeated MembershipFeatureModel features = 10;
}
```
**🔧 راه حل**:
```csharp
// تغییر نام فیلدها:
var activationDate = response.ActivatedAt?.ToDateTime(); // ✅ ActivatedAt
var expirationDate = response.ExpiresAt?.ToDateTime(); // ✅ ExpiresAt
```
**⚠️ نکته مهم**: CMS حالا یک **لیست features** نیز بر می‌گرداند که باید به Response DTO اضافه شود:
```csharp
public class GetMyClubMembershipResponseDto
{
// ... فیلدهای موجود
public List<MembershipFeatureDto>? Features { get; set; } // ✅ جدید
}
public class MembershipFeatureDto
{
public long ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public DateTime? ExpiresAt { get; set; }
public bool IsActive { get; set; }
}
```
---
### ✅ ActivateMyClubMembershipCommandHandler
**وضعیت**: این Handler صحیح است، اما Response نیاز به بررسی دارد.
**کد فعلی**:
```csharp
await _context.ClubMemberships.ActivateClubMembershipAsync(grpcRequest, cancellationToken: cancellationToken);
// ❌ Response Mock است:
return new ActivateMyClubMembershipResponseDto
{
Success = true,
Message = "عضویت باشگاه با موفقیت فعال شد",
ActivationDate = DateTime.UtcNow,
ExpirationDate = activationDate.AddMonths(request.DurationMonths),
AmountPaid = 56_000_000 // ❌ Hardcoded
};
```
**مشکل**: CMS فقط `Empty` بر می‌گرداند، اطلاعات واقعی باید از `GetClubMembership` گرفته شود.
**🔧 راه حل**:
```csharp
// بعد از فعال‌سازی، GetClubMembership را صدا بزن:
await _context.ClubMemberships.ActivateClubMembershipAsync(grpcRequest, cancellationToken);
var membershipRequest = new GetClubMembershipRequest { UserId = userId };
var membership = await _context.ClubMemberships.GetClubMembershipAsync(membershipRequest, cancellationToken);
return new ActivateMyClubMembershipResponseDto
{
Success = true,
Message = "عضویت باشگاه با موفقیت فعال شد",
ActivationDate = membership.ActivatedAt?.ToDateTime(),
ExpirationDate = membership.ExpiresAt?.ToDateTime(),
AmountPaid = CalculatePackageCost(membership.PackageId, request.DurationMonths) // محاسبه واقعی
};
```
---
## 2️⃣ NetworkMembership - مغایرت‌ها
### 🟢 BFF Handlers (2 عدد - موجود)
```
✅ GetMyNetworkTree (Query)
✅ GetMyNetworkStatistics (Query)
```
### 📋 CMS Protobuf (networkmembership.proto)
```protobuf
service NetworkMembershipContract {
// Commands
rpc JoinNetwork(JoinNetworkRequest) returns (Empty);
rpc ChangeNetworkParent(ChangeNetworkParentRequest) returns (Empty);
rpc RemoveFromNetwork(RemoveFromNetworkRequest) returns (Empty);
// Queries
rpc GetUserNetwork(GetUserNetworkRequest) returns (GetUserNetworkResponse);
rpc GetNetworkTree(GetNetworkTreeRequest) returns (GetNetworkTreeResponse);
rpc GetNetworkMembershipHistory(GetNetworkMembershipHistoryRequest) returns (GetNetworkMembershipHistoryResponse);
rpc GetNetworkStatistics(GetNetworkStatisticsRequest) returns (GetNetworkStatisticsResponse);
}
```
---
### ❌ مشکل 2: GetMyNetworkTreeQueryHandler
**فایل**: `FrontOffice.BFF/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetMyNetworkTree/GetMyNetworkTreeQueryHandler.cs`
**کد فعلی**:
```csharp
var response = await _context.NetworkMemberships.GetNetworkTreeAsync(cmsRequest, cancellationToken);
// استفاده از فیلد RootNode:
return new GetMyNetworkTreeResponseDto
{
RootNode = MapToNetworkNode(response.RootNode, 0), // ❌ RootNode
TotalMembers = CountNodes(response.RootNode),
CurrentDepth = CalculateDepth(response.RootNode)
};
```
**CMS Proto**:
```protobuf
message GetNetworkTreeResponse
{
repeated NetworkTreeNodeModel nodes = 1; // ✅ Flat list (نه Tree)
}
message NetworkTreeNodeModel
{
int64 user_id = 1;
string user_name = 2;
google.protobuf.Int64Value parent_id = 3;
int32 network_leg = 4;
int32 network_level = 5;
bool is_active = 6;
google.protobuf.Timestamp joined_at = 7;
}
```
**🚨 مشکل بزرگ**: CMS حالا **Flat List** بر می‌گرداند نه **Tree Structure**!
**🔧 راه حل**: باید در BFF یک Tree Builder بسازیم:
```csharp
public async Task<GetMyNetworkTreeResponseDto> Handle(GetMyNetworkTreeQuery request, CancellationToken cancellationToken)
{
var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException();
var cmsRequest = new GetNetworkTreeRequest
{
RootUserId = userId,
MaxDepth = Math.Clamp(request.MaxDepth, 1, 10)
};
var response = await _context.NetworkMemberships.GetNetworkTreeAsync(cmsRequest, cancellationToken);
// ✅ ساخت Tree از Flat List:
var rootNode = BuildTreeFromFlatList(response.Nodes, userId);
return new GetMyNetworkTreeResponseDto
{
RootNode = rootNode,
TotalMembers = response.Nodes.Count,
CurrentDepth = response.Nodes.Any() ? response.Nodes.Max(n => n.NetworkLevel) : 0
};
}
private NetworkNodeDto? BuildTreeFromFlatList(IEnumerable<NetworkTreeNodeModel> nodes, long rootUserId)
{
var nodeDict = nodes.ToDictionary(n => n.UserId);
if (!nodeDict.ContainsKey(rootUserId))
return null;
NetworkNodeDto BuildNode(long userId, int level)
{
var cmsNode = nodeDict[userId];
var node = new NetworkNodeDto
{
UserId = cmsNode.UserId,
FullName = cmsNode.UserName,
Mobile = string.Empty, // CMS ندارد
Avatar = null,
Position = cmsNode.NetworkLeg == 0 ? "Left" : "Right",
Level = level
};
// پیدا کردن children
var leftChild = nodes.FirstOrDefault(n => n.ParentId == userId && n.NetworkLeg == 0);
var rightChild = nodes.FirstOrDefault(n => n.ParentId == userId && n.NetworkLeg == 1);
if (leftChild != null)
node.LeftChild = BuildNode(leftChild.UserId, level + 1);
if (rightChild != null)
node.RightChild = BuildNode(rightChild.UserId, level + 1);
return node;
}
return BuildNode(rootUserId, 0);
}
```
---
### ❌ مشکل 3: GetMyNetworkStatisticsQueryHandler
**فایل**: `FrontOffice.BFF/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetMyNetworkStatistics/GetMyNetworkStatisticsQueryHandler.cs`
**کد فعلی**:
```csharp
var cmsRequest = new GetNetworkStatisticsRequest
{
UserId = userId // ❌ GetNetworkStatisticsRequest فیلد UserId ندارد!
};
var response = await _context.NetworkMemberships.GetNetworkStatisticsAsync(cmsRequest, cancellationToken);
// استفاده از فیلدهای قدیمی:
return new GetMyNetworkStatisticsResponseDto
{
LeftLegCount = response.LeftLegCount,
RightLegCount = response.RightLegCount,
TotalMembers = response.TotalMembers,
TreeDepth = response.TreeDepth, // ❌ نام قدیمی
WeakerLeg = weakerLeg,
LastMember = response.LastMember != null ? new LastMemberDto { ... } // ❌ LastMember وجود ندارد!
};
```
**CMS Proto**:
```protobuf
message GetNetworkStatisticsRequest
{
// Empty - برای کل شبکه است نه یک کاربر خاص!
}
message GetNetworkStatisticsResponse
{
int32 total_members = 1;
int32 active_members = 2;
int32 left_leg_count = 3;
int32 right_leg_count = 4;
double left_percentage = 5;
double right_percentage = 6;
double average_depth = 7;
int32 max_depth = 8; // ✅ max_depth (نه tree_depth)
repeated LevelDistribution level_distribution = 9;
repeated MonthlyGrowth monthly_growth = 10;
repeated TopNetworkUser top_users = 11;
}
```
**🚨 مشکل بزرگ**:
1. CMS دیگر `UserId` نمی‌گیرد - این Query برای کل شبکه است
2. فیلد `LastMember` وجود ندارد
3. Response خیلی جامع‌تر شده (LevelDistribution, MonthlyGrowth, TopUsers)
**🔧 راه حل**: باید یک Query جدید در CMS اضافه شود یا از `GetUserNetwork` استفاده کنیم:
### گزینه A: استفاده از GetUserNetwork (سریع‌تر)
```csharp
public async Task<GetMyNetworkStatisticsResponseDto> Handle(GetMyNetworkStatisticsQuery request, CancellationToken cancellationToken)
{
var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException();
// ✅ استفاده از GetUserNetwork:
var userNetworkRequest = new GetUserNetworkRequest { UserId = userId };
var userNetwork = await _context.NetworkMemberships.GetUserNetworkAsync(userNetworkRequest, cancellationToken);
// ✅ استفاده از GetNetworkTree برای شمارش:
var treeRequest = new GetNetworkTreeRequest
{
RootUserId = userId,
MaxDepth = 10 // Full tree
};
var tree = await _context.NetworkMemberships.GetNetworkTreeAsync(treeRequest, cancellationToken);
var leftCount = tree.Nodes.Count(n => n.ParentId == userId && n.NetworkLeg == 0);
var rightCount = tree.Nodes.Count(n => n.ParentId == userId && n.NetworkLeg == 1);
var lastMember = tree.Nodes
.Where(n => n.ParentId == userId)
.OrderByDescending(n => n.JoinedAt)
.FirstOrDefault();
return new GetMyNetworkStatisticsResponseDto
{
LeftLegCount = leftCount,
RightLegCount = rightCount,
TotalMembers = tree.Nodes.Count,
TreeDepth = tree.Nodes.Any() ? tree.Nodes.Max(n => n.NetworkLevel) : 0,
WeakerLeg = leftCount < rightCount ? "Left" : "Right",
LastMember = lastMember != null ? new LastMemberDto
{
UserId = lastMember.UserId,
FullName = lastMember.UserName,
Position = lastMember.NetworkLeg == 0 ? "Left" : "Right",
JoinedAt = lastMember.JoinedAt?.ToDateTime() ?? DateTime.UtcNow
} : null
};
}
```
### گزینه B: اضافه کردن Query جدید به CMS (بهتر)
در `networkmembership.proto` اضافه کن:
```protobuf
rpc GetUserNetworkStatistics(GetUserNetworkStatisticsRequest) returns (GetUserNetworkStatisticsResponse);
message GetUserNetworkStatisticsRequest
{
int64 user_id = 1;
}
message GetUserNetworkStatisticsResponse
{
int32 left_leg_count = 1;
int32 right_leg_count = 2;
int32 total_children = 3;
int32 max_depth = 4;
string weaker_leg = 5; // "Left" | "Right"
google.protobuf.Int64Value last_member_id = 6;
google.protobuf.StringValue last_member_name = 7;
google.protobuf.Timestamp last_joined_at = 8;
}
```
---
## 3️⃣ Commission - مغایرت‌ها
### 🟢 BFF Handlers (2 عدد - موجود)
```
✅ GetMyCommissionPayouts (Query)
✅ GetMyWeeklyBalances (Query)
```
### 📋 CMS Protobuf (commission.proto)
```protobuf
service CommissionContract {
// Commands
rpc CalculateWeeklyBalances(CalculateWeeklyBalancesRequest) returns (Empty);
rpc CalculateWeeklyCommissionPool(CalculateWeeklyCommissionPoolRequest) returns (Empty);
rpc ProcessUserPayouts(ProcessUserPayoutsRequest) returns (Empty);
rpc RequestWithdrawal(RequestWithdrawalRequest) returns (Empty);
rpc ProcessWithdrawal(ProcessWithdrawalRequest) returns (Empty);
rpc ApproveWithdrawal(ApproveWithdrawalRequest) returns (Empty);
rpc RejectWithdrawal(RejectWithdrawalRequest) returns (Empty);
// Queries
rpc GetWeeklyCommissionPool(GetWeeklyCommissionPoolRequest) returns (GetWeeklyCommissionPoolResponse);
rpc GetUserCommissionPayouts(GetUserCommissionPayoutsRequest) returns (GetUserCommissionPayoutsResponse);
rpc GetCommissionPayoutHistory(GetCommissionPayoutHistoryRequest) returns (GetCommissionPayoutHistoryResponse);
rpc GetUserWeeklyBalances(GetUserWeeklyBalancesRequest) returns (GetUserWeeklyBalancesResponse);
rpc GetAllWeeklyPools(GetAllWeeklyPoolsRequest) returns (GetAllWeeklyPoolsResponse);
rpc GetWithdrawalRequests(GetWithdrawalRequestsRequest) returns (GetWithdrawalRequestsResponse);
// Worker Control APIs
rpc TriggerWeeklyCalculation(TriggerWeeklyCalculationRequest) returns (TriggerWeeklyCalculationResponse);
rpc GetWorkerStatus(GetWorkerStatusRequest) returns (GetWorkerStatusResponse);
rpc GetWorkerExecutionLogs(GetWorkerExecutionLogsRequest) returns (GetWorkerExecutionLogsResponse);
}
```
---
### ❌ مشکل 4: GetMyCommissionPayoutsQueryHandler
**فایل**: `FrontOffice.BFF/src/FrontOffice.BFF.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQueryHandler.cs`
**کد فعلی**:
```csharp
var cmsRequest = new GetUserCommissionPayoutsRequest
{
UserId = userId,
PageNumber = request.PageNumber, // ❌ نام اشتباه
PageSize = request.PageSize // ❌ نام اشتباه
};
if (request.WeekNumber.HasValue)
cmsRequest.WeekNumber = request.WeekNumber.Value; // ❌ نوع داده اشتباه
if (request.Status.HasValue)
cmsRequest.Status = request.Status.Value;
```
**CMS Proto**:
```protobuf
message GetUserCommissionPayoutsRequest
{
google.protobuf.Int64Value user_id = 1;
google.protobuf.Int32Value status = 2;
google.protobuf.StringValue week_number = 3; // ✅ string است (نه int)
int32 page_index = 4; // ✅ page_index (نه page_number)
int32 page_size = 5;
}
message UserCommissionPayoutModel
{
int64 id = 1;
int64 user_id = 2;
string user_name = 3;
string week_number = 4; // ✅ string است
int32 balances_earned = 5;
int64 value_per_balance = 6;
int64 total_amount = 7;
int32 status = 8;
google.protobuf.Int32Value withdrawal_method = 9;
string iban_number = 10;
google.protobuf.Timestamp created = 11;
google.protobuf.Timestamp last_modified = 12;
}
```
**🔧 راه حل**:
```csharp
var cmsRequest = new GetUserCommissionPayoutsRequest
{
UserId = userId,
PageIndex = request.PageNumber, // ✅ PageIndex
PageSize = request.PageSize
};
if (!string.IsNullOrEmpty(request.WeekNumber))
cmsRequest.WeekNumber = request.WeekNumber; // ✅ string
if (request.Status.HasValue)
cmsRequest.Status = request.Status.Value;
// در DTO نیز باید تغییر کند:
var payouts = response.Models.Select(p => new CommissionPayoutDto
{
Id = p.Id,
WeekNumber = p.WeekNumber, // ✅ string
WeekLabel = $"هفته {p.WeekNumber}",
BalancesEarned = p.BalancesEarned,
ValuePerBalance = p.ValuePerBalance, // ✅ جدید
TotalAmount = p.TotalAmount,
AmountFormatted = FormatCurrency(p.TotalAmount),
Status = MapStatus(p.Status),
StatusBadgeColor = GetStatusColor(p.Status),
WithdrawalMethod = p.WithdrawalMethod?.ToString(), // ✅ جدید
IbanNumber = p.IbanNumber, // ✅ جدید
CalculatedDate = p.Created?.ToDateTime() ?? DateTime.UtcNow,
LastModified = p.LastModified?.ToDateTime(), // ✅ جدید
DatePersian = FormatPersianDate(p.Created?.ToDateTime())
}).ToList();
```
**Query DTO نیز باید بروز شود**:
```csharp
public class GetMyCommissionPayoutsQuery : IRequest<GetMyCommissionPayoutsResponseDto>
{
public string? WeekNumber { get; set; } // ✅ string (نه int?)
public int? Status { get; set; }
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
}
```
---
### ❌ مشکل 5: GetMyWeeklyBalancesQueryHandler
**این Handler احتمالا وجود دارد اما بررسی نشده**. باید چک شود:
**CMS Proto**:
```protobuf
message GetUserWeeklyBalancesRequest
{
google.protobuf.Int64Value user_id = 1;
google.protobuf.StringValue week_number = 2; // ✅ string
bool only_active = 3;
int32 page_index = 4;
int32 page_size = 5;
}
message UserWeeklyBalanceModel
{
int64 id = 1;
int64 user_id = 2;
string week_number = 3; // ✅ string
int32 left_leg_balances = 4;
int32 right_leg_balances = 5;
int32 total_balances = 6;
int64 weekly_pool_contribution = 7;
google.protobuf.Timestamp calculated_at = 8;
bool is_expired = 9;
google.protobuf.Timestamp created = 10;
}
```
**مشکل احتمالی**: نام فیلدها و نوع `week_number` (string vs int)
---
## 4️⃣ UserWallet - TODO Queries
### 🟡 BFF Handlers (5 عدد - بعضی کامنت شده)
```
✅ GetUserWallet (Query) - موجود
⚠️ GetAllUserWalletChangeLog (Query) - TODO
⚠️ WithdrawBalance (Command) - TODO
⚠️ GetUserWithdrawals (Query) - TODO
⚠️ GetWithdrawalSettings (Query) - TODO
```
این ها در `/FrontOffice/src/FrontOffice.Main/Utilities/WalletService.cs` کامنت شده‌اند.
**CMS API ها موجود هستند در `Commission` proto**:
```protobuf
rpc RequestWithdrawal(RequestWithdrawalRequest) returns (Empty);
rpc GetWithdrawalRequests(GetWithdrawalRequestsRequest) returns (GetWithdrawalRequestsResponse);
```
**⚠️ نکته**: `WithdrawBalance` در `Commission` است نه `UserWallet`!
---
## 📋 خلاصه اقدامات لازم
### فاز 1: اصلاح Handler های موجود (اولویت بالا) ⚡
#### 1.1. ClubMembershipCQ
**فایل**: `GetMyClubMembershipQueryHandler.cs`
```csharp
// ❌ کد قدیمی:
var activationDate = response.ActivationDate?.ToDateTime();
var expirationDate = response.ExpirationDate?.ToDateTime();
// ✅ کد جدید:
var activationDate = response.ActivatedAt?.ToDateTime();
var expirationDate = response.ExpiresAt?.ToDateTime();
// ✅ اضافه کردن Features:
Features = response.Features.Select(f => new MembershipFeatureDto
{
ProductId = f.ProductId,
ProductName = f.ProductName,
Quantity = f.Quantity,
ExpiresAt = f.ExpiresAt?.ToDateTime(),
IsActive = f.IsActive
}).ToList()
```
**فایل**: `ActivateMyClubMembershipCommandHandler.cs`
```csharp
// ✅ بعد از Activate، GetClubMembership را صدا بزن:
await _context.ClubMemberships.ActivateClubMembershipAsync(grpcRequest, cancellationToken);
var membershipRequest = new GetClubMembershipRequest { UserId = userId };
var membership = await _context.ClubMemberships.GetClubMembershipAsync(membershipRequest, cancellationToken);
return new ActivateMyClubMembershipResponseDto
{
Success = true,
Message = "عضویت باشگاه با موفقیت فعال شد",
ActivationDate = membership.ActivatedAt?.ToDateTime(),
ExpirationDate = membership.ExpiresAt?.ToDateTime(),
// AmountPaid باید از Package Service گرفته شود یا محاسبه شود
};
```
---
#### 1.2. NetworkMembershipCQ
**فایل**: `GetMyNetworkTreeQueryHandler.cs`
```csharp
// ✅ کامل بازنویسی با Tree Builder:
var response = await _context.NetworkMemberships.GetNetworkTreeAsync(cmsRequest, cancellationToken);
var rootNode = BuildTreeFromFlatList(response.Nodes, userId);
return new GetMyNetworkTreeResponseDto
{
RootNode = rootNode,
TotalMembers = response.Nodes.Count,
CurrentDepth = response.Nodes.Any() ? response.Nodes.Max(n => n.NetworkLevel) : 0
};
// اضافه کردن متد BuildTreeFromFlatList (کد کامل بالا)
```
**فایل**: `GetMyNetworkStatisticsQueryHandler.cs`
```csharp
// ✅ استفاده از GetUserNetwork + GetNetworkTree:
var userNetworkRequest = new GetUserNetworkRequest { UserId = userId };
var userNetwork = await _context.NetworkMemberships.GetUserNetworkAsync(userNetworkRequest, cancellationToken);
var treeRequest = new GetNetworkTreeRequest { RootUserId = userId, MaxDepth = 10 };
var tree = await _context.NetworkMemberships.GetNetworkTreeAsync(treeRequest, cancellationToken);
// محاسبه آمار (کد کامل بالا)
```
---
#### 1.3. CommissionCQ
**فایل**: `GetMyCommissionPayoutsQueryHandler.cs`
```csharp
// ❌ کد قدیمی:
var cmsRequest = new GetUserCommissionPayoutsRequest
{
UserId = userId,
PageNumber = request.PageNumber,
PageSize = request.PageSize
};
if (request.WeekNumber.HasValue)
cmsRequest.WeekNumber = request.WeekNumber.Value;
// ✅ کد جدید:
var cmsRequest = new GetUserCommissionPayoutsRequest
{
UserId = userId,
PageIndex = request.PageNumber, // PageIndex
PageSize = request.PageSize
};
if (!string.IsNullOrEmpty(request.WeekNumber))
cmsRequest.WeekNumber = request.WeekNumber; // string
// ✅ Response Mapping:
var payouts = response.Models.Select(p => new CommissionPayoutDto
{
Id = p.Id,
WeekNumber = p.WeekNumber, // string
ValuePerBalance = p.ValuePerBalance, // جدید
WithdrawalMethod = p.WithdrawalMethod, // جدید
IbanNumber = p.IbanNumber, // جدید
LastModified = p.LastModified?.ToDateTime() // جدید
// ... بقیه فیلدها
}).ToList();
```
**Query DTO**:
```csharp
public class GetMyCommissionPayoutsQuery
{
public string? WeekNumber { get; set; } // ✅ string
// ...
}
public class CommissionPayoutDto
{
public long ValuePerBalance { get; set; } // ✅ جدید
public string? WithdrawalMethod { get; set; } // ✅ جدید
public string? IbanNumber { get; set; } // ✅ جدید
public DateTime? LastModified { get; set; } // ✅ جدید
// ...
}
```
---
### فاز 2: اضافه کردن Handler های جدید (اولویت متوسط) 🟡
#### 2.1. UserWalletCQ - GetAllUserWalletChangeLog
```csharp
// Query:
public class GetAllUserWalletChangeLogQuery : IRequest<GetAllUserWalletChangeLogResponseDto>
{
public long? ReferenceId { get; set; }
public bool? IsIncrease { get; set; }
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
// Handler:
public async Task<GetAllUserWalletChangeLogResponseDto> Handle(...)
{
var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException();
// TODO: باید در CMS یک Query اضافه شود
// فعلا از GetUserCommissionPayouts استفاده کنیم برای تاریخچه برداشت
var request = new GetWithdrawalRequestsRequest
{
UserId = userId,
PageIndex = request.PageNumber,
PageSize = request.PageSize
};
var response = await _context.Commission.GetWithdrawalRequestsAsync(request, cancellationToken);
// Mapping...
}
```
#### 2.2. UserWalletCQ - WithdrawBalance Command
```csharp
// Command:
public class WithdrawBalanceCommand : IRequest<WithdrawBalanceResponseDto>
{
public long PayoutId { get; set; }
public int WithdrawalMethod { get; set; } // 0=Cash, 1=Diamond
public string? IbanNumber { get; set; }
}
// Handler:
public async Task<WithdrawBalanceResponseDto> Handle(...)
{
var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException();
var request = new RequestWithdrawalRequest
{
PayoutId = command.PayoutId,
WithdrawalMethod = command.WithdrawalMethod,
IbanNumber = command.IbanNumber
};
await _context.Commission.RequestWithdrawalAsync(request, cancellationToken);
return new WithdrawBalanceResponseDto
{
Success = true,
Message = "درخواست برداشت ثبت شد"
};
}
```
---
## ⏱️ تخمین زمان اجرا
| مرحله | فایل‌ها | زمان | اولویت |
|-------|---------|------|--------|
| ClubMembership fix | 2 Handler | 2 ساعت | 🔴 بالا |
| NetworkMembership fix | 2 Handler | 3-4 ساعت | 🔴 بالا |
| Commission fix | 2 Handler | 2 ساعت | 🔴 بالا |
| UserWallet new Queries | 4 Handler | 3 ساعت | 🟡 متوسط |
| Testing & Build | - | 2 ساعت | 🟢 پایین |
| **جمع کل** | **10 Handler** | **12-15 ساعت** | |
---
## ✅ Checklist اجرایی
### مرحله 1: ClubMembershipCQ
- [ ] GetMyClubMembershipQueryHandler: تغییر ActivationDate → ActivatedAt
- [ ] GetMyClubMembershipQueryHandler: تغییر ExpirationDate → ExpiresAt
- [ ] GetMyClubMembershipResponseDto: اضافه کردن List<MembershipFeatureDto>
- [ ] ActivateMyClubMembershipCommandHandler: گرفتن داده واقعی از GetClubMembership
### مرحله 2: NetworkMembershipCQ
- [ ] GetMyNetworkTreeQueryHandler: پیاده‌سازی BuildTreeFromFlatList
- [ ] GetMyNetworkTreeQueryHandler: حذف استفاده از response.RootNode
- [ ] GetMyNetworkStatisticsQueryHandler: حذف فیلد UserId از Request
- [ ] GetMyNetworkStatisticsQueryHandler: استفاده از GetUserNetwork + GetNetworkTree
- [ ] GetMyNetworkStatisticsResponseDto: نام TreeDepth → MaxDepth
### مرحله 3: CommissionCQ
- [ ] GetMyCommissionPayoutsQuery: تغییر WeekNumber از int? به string?
- [ ] GetMyCommissionPayoutsQueryHandler: PageNumber → PageIndex
- [ ] CommissionPayoutDto: اضافه کردن ValuePerBalance, WithdrawalMethod, IbanNumber, LastModified
- [ ] GetMyWeeklyBalancesQueryHandler: بررسی و اصلاح (اگر لازم باشد)
### مرحله 4: UserWalletCQ
- [ ] Query: GetAllUserWalletChangeLog ساخته شود
- [ ] Command: WithdrawBalance ساخته شود
- [ ] Query: GetUserWithdrawals ساخته شود (از GetWithdrawalRequests استفاده کند)
- [ ] Query: GetWithdrawalSettings ساخته شود
- [ ] WalletService.cs: uncomment کردن متدها
### مرحله 5: Build & Test
- [ ] dotnet build FrontOffice.BFF.sln
- [ ] dotnet build FrontOffice/src/FrontOffice.sln
- [ ] تست هر Handler با Postman/Swagger
- [ ] تست UI با داده واقعی
---
**📅 آخرین بروزرسانی**: ۱۴ آذر ۱۴۰۴
**👤 توسط**: GitHub Copilot (Claude Sonnet 4.5)