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:
@@ -0,0 +1,967 @@
|
||||
# Package Purchase System - سیستم خرید پکیج طلایی
|
||||
|
||||
**تاریخ ایجاد:** 2024-12-02
|
||||
**وضعیت:** در حال طراحی
|
||||
**اولویت:** 🔴 بسیار بالا
|
||||
|
||||
---
|
||||
|
||||
## 📋 فهرست
|
||||
|
||||
1. [مقدمه](#مقدمه)
|
||||
2. [سه سناریوی اصلی](#سه-سناریوی-اصلی)
|
||||
3. [Entity Changes](#entity-changes)
|
||||
4. [Business Rules](#business-rules)
|
||||
5. [Flow Diagrams](#flow-diagrams)
|
||||
6. [Commands & Handlers](#commands--handlers)
|
||||
7. [تسکهای پیادهسازی](#تسک-های-پیاده-سازی)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 مقدمه
|
||||
|
||||
سیستم خرید پکیج طلایی سه سناریوی مختلف دارد که باید به درستی از هم تفکیک شوند:
|
||||
|
||||
### هدف کلی:
|
||||
- **سناریو 1 و 2**: خرید پکیج طلایی (56 میلیون تومان) → امکان فعالسازی باشگاه مشتریان
|
||||
- **سناریو 3**: شارژ عادی کیف پول تخفیفی → فقط برای خرید از فروشگاه تخفیفی
|
||||
|
||||
### نکات کلیدی:
|
||||
1. کاربر فقط **یک بار** میتواند پکیج طلایی خریداری کند (سناریو 1 یا 2)
|
||||
2. بعد از خرید پکیج، کاربر **باید خودش** دکمه فعالسازی باشگاه را بزند
|
||||
3. فعالسازی باشگاه **نیاز به تایید Admin ندارد**
|
||||
4. عضویت در شبکه (NetworkMembership) **جدا** از عضویت در باشگاه (ClubMembership) است
|
||||
5. کمیسیونها **فقط بعد** از فعالسازی باشگاه محاسبه میشوند
|
||||
|
||||
---
|
||||
|
||||
## 🔄 سه سناریوی اصلی
|
||||
|
||||
### 📌 سناریو 1: دریافت وام دایا (DayaLoan)
|
||||
|
||||
```
|
||||
کاربر → درخواست وام از دایا → دایا وام را تایید میکند
|
||||
↓
|
||||
شارژ Balance در UserWallet (56,000,000 تومان)
|
||||
↓
|
||||
ثبت UserWalletChangeLog (Amount: +56M, Type: DepositExternal1)
|
||||
↓
|
||||
ثبت Transaction (Type: DepositExternal1, RefId: شماره قرارداد دایا)
|
||||
↓
|
||||
ثبت UserOrder (PackageId: پکیج طلایی, TransactionId: xxx, Amount: 56M)
|
||||
↓
|
||||
کاربر میتواند با این 56M از فروشگاه عادی خرید کند
|
||||
↓
|
||||
[کاربر باید خودش دکمه "فعالسازی باشگاه مشتریان" را بزند]
|
||||
↓
|
||||
ثبت/بهروزرسانی ClubMembership (IsActive: true, PurchaseMethod: DayaLoan)
|
||||
↓
|
||||
شروع محاسبه کمیسیونها
|
||||
```
|
||||
|
||||
**نکات:**
|
||||
- `Transaction.Type` = `DepositExternal1` (وام دایا)
|
||||
- `Transaction.RefId` = شماره قرارداد دایا
|
||||
- `UserOrder.PackageId` پر میشود
|
||||
- `User.PackagePurchaseMethod` = `DayaLoan`
|
||||
|
||||
---
|
||||
|
||||
### 📌 سناریو 2: خرید پکیج طلایی از درگاه (Direct Purchase)
|
||||
|
||||
```
|
||||
کاربر → انتخاب پکیج طلایی (56M) → کلیک "پرداخت"
|
||||
↓
|
||||
ثبت UserOrder (PackageId: پکیج طلایی, Amount: 56M, PaymentStatus: Pending)
|
||||
↓
|
||||
Redirect به درگاه بانکی (IPG)
|
||||
↓
|
||||
کاربر پرداخت میکند و بر میگردد
|
||||
↓
|
||||
Verify پرداخت با بانک
|
||||
↓
|
||||
شارژ Balance در UserWallet (56,000,000 تومان)
|
||||
↓
|
||||
ثبت UserWalletChangeLog (Amount: +56M, Type: DepositIpg)
|
||||
↓
|
||||
ثبت Transaction (Type: DepositIpg, RefId: کد پیگیری بانک)
|
||||
↓
|
||||
بهروزرسانی UserOrder (TransactionId: xxx, PaymentStatus: Success)
|
||||
↓
|
||||
کاربر میتواند با این 56M از فروشگاه عادی خرید کند
|
||||
↓
|
||||
[کاربر باید خودش دکمه "فعالسازی باشگاه مشتریان" را بزند]
|
||||
↓
|
||||
ثبت/بهروزرسانی ClubMembership (IsActive: true, PurchaseMethod: DirectPurchase)
|
||||
↓
|
||||
شروع محاسبه کمیسیونها
|
||||
```
|
||||
|
||||
**نکات:**
|
||||
- `Transaction.Type` = `DepositIpg` (پرداخت از درگاه)
|
||||
- `Transaction.RefId` = کد پیگیری بانک
|
||||
- `UserOrder.PackageId` پر میشود
|
||||
- `User.PackagePurchaseMethod` = `DirectPurchase`
|
||||
|
||||
---
|
||||
|
||||
### 📌 سناریو 3: شارژ عادی کیف پول تخفیفی (Regular Wallet Charge)
|
||||
|
||||
```
|
||||
کاربر → انتخاب مبلغ دلخواه → کلیک "شارژ کیف پول"
|
||||
↓
|
||||
Redirect به درگاه بانکی (IPG)
|
||||
↓
|
||||
کاربر پرداخت میکند و بر میگردد
|
||||
↓
|
||||
Verify پرداخت با بانک
|
||||
↓
|
||||
شارژ DiscountBalance در UserWallet (مبلغ دلخواه)
|
||||
↓
|
||||
ثبت UserWalletChangeLog (Amount: +xxx, Type: DiscountWalletCharge)
|
||||
↓
|
||||
ثبت Transaction (Type: DiscountWalletCharge, RefId: کد پیگیری بانک)
|
||||
↓
|
||||
کاربر میتواند فقط از فروشگاه تخفیفی خرید کند
|
||||
↓
|
||||
[هیچ ارتباطی با باشگاه مشتریان ندارد]
|
||||
```
|
||||
|
||||
**نکات:**
|
||||
- `Transaction.Type` = `DiscountWalletCharge`
|
||||
- `Transaction.RefId` = کد پیگیری بانک
|
||||
- **PackageId در هیچ جا ثبت نمیشود**
|
||||
- فقط `DiscountBalance` شارژ میشود، نه `Balance`
|
||||
- هیچ `UserOrder` با `PackageId` ثبت نمیشود
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Entity Changes
|
||||
|
||||
### 1️⃣ **Enum جدید: `PackagePurchaseMethod`**
|
||||
|
||||
```csharp
|
||||
namespace CMSMicroservice.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// نحوه خرید پکیج طلایی توسط کاربر
|
||||
/// </summary>
|
||||
public enum PackagePurchaseMethod
|
||||
{
|
||||
/// <summary>
|
||||
/// هنوز پکیج خریداری نکرده
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// از طریق وام دایا
|
||||
/// </summary>
|
||||
DayaLoan = 1,
|
||||
|
||||
/// <summary>
|
||||
/// از طریق پرداخت مستقیم درگاه بانکی
|
||||
/// </summary>
|
||||
DirectPurchase = 2
|
||||
}
|
||||
```
|
||||
|
||||
**محل:** `CMS/src/CMSMicroservice.Domain/Enums/PackagePurchaseMethod.cs`
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ **تغییرات `User` Entity**
|
||||
|
||||
```csharp
|
||||
// اضافه کردن این فیلد به User.cs:
|
||||
|
||||
/// <summary>
|
||||
/// نحوه خرید پکیج طلایی (برای جلوگیری از خرید مجدد)
|
||||
/// </summary>
|
||||
public PackagePurchaseMethod PackagePurchaseMethod { get; set; } = PackagePurchaseMethod.None;
|
||||
```
|
||||
|
||||
**منطق:**
|
||||
- وقتی کاربر سناریو 1 یا 2 را انجام میدهد، این فیلد تغییر میکند
|
||||
- اگر `PackagePurchaseMethod != None` باشد، کاربر نمیتواند دوباره پکیج خریداری کند
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ **تغییرات `ClubMembership` Entity**
|
||||
|
||||
```csharp
|
||||
// اضافه کردن این فیلد به ClubMembership.cs:
|
||||
|
||||
/// <summary>
|
||||
/// نحوه خرید پکیج که منجر به فعالسازی باشگاه شد
|
||||
/// </summary>
|
||||
public PackagePurchaseMethod PurchaseMethod { get; set; }
|
||||
```
|
||||
|
||||
**منطق:**
|
||||
- وقتی کاربر دکمه "فعالسازی باشگاه" را میزند، این فیلد از `User.PackagePurchaseMethod` کپی میشود
|
||||
- برای گزارشگیری و تحلیل: چند نفر از طریق وام دایا و چند نفر از طریق خرید مستقیم عضو شدند
|
||||
|
||||
---
|
||||
|
||||
### 4️⃣ **تغییرات `TransactionType` Enum**
|
||||
|
||||
```csharp
|
||||
// فعلاً موجود است:
|
||||
public enum TransactionType
|
||||
{
|
||||
Buy = 0,
|
||||
DepositIpg = 1, // پرداخت از درگاه (سناریو 2)
|
||||
DepositExternal1 = 2, // وام دایا (سناریو 1)
|
||||
Withdraw = 3,
|
||||
NetworkCommission = 10,
|
||||
ClubActivation = 11,
|
||||
DiscountWalletCharge = 12 // شارژ کیف پول تخفیفی (سناریو 3) ✅
|
||||
}
|
||||
```
|
||||
|
||||
**نکته:** `DiscountWalletCharge` از قبل وجود دارد، پس نیازی به تغییر نیست.
|
||||
|
||||
---
|
||||
|
||||
## 📐 Business Rules
|
||||
|
||||
### قانون 1: یک کاربر فقط یک بار میتواند پکیج طلایی خریداری کند
|
||||
|
||||
```csharp
|
||||
// Check قبل از خرید پکیج:
|
||||
if (user.PackagePurchaseMethod != PackagePurchaseMethod.None)
|
||||
{
|
||||
throw new ValidationException("شما قبلاً پکیج طلایی را خریداری کردهاید.");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### قانون 2: فعالسازی باشگاه فقط با موجودی اصلی (Balance) امکانپذیر است
|
||||
|
||||
```csharp
|
||||
// Check موقع فعالسازی باشگاه:
|
||||
var userWallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == userId);
|
||||
|
||||
if (userWallet.Balance < 56_000_000)
|
||||
{
|
||||
throw new ValidationException("برای فعالسازی باشگاه مشتریان باید حداقل 56 میلیون تومان موجودی اصلی داشته باشید.");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### قانون 3: فعالسازی باشگاه فقط برای کسانی که پکیج خریدهاند
|
||||
|
||||
```csharp
|
||||
// Check موقع فعالسازی باشگاه:
|
||||
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
|
||||
{
|
||||
throw new ValidationException("برای فعالسازی باشگاه مشتریان ابتدا باید پکیج طلایی خریداری کنید.");
|
||||
}
|
||||
|
||||
// پیدا کردن UserOrder مربوط به پکیج:
|
||||
var packageOrder = await _context.UserOrders
|
||||
.FirstOrDefaultAsync(o =>
|
||||
o.UserId == userId &&
|
||||
o.PackageId != null &&
|
||||
o.PaymentStatus == PaymentStatus.Success
|
||||
);
|
||||
|
||||
if (packageOrder == null)
|
||||
{
|
||||
throw new ValidationException("سفارش پکیج طلایی یافت نشد.");
|
||||
}
|
||||
|
||||
// پیدا کردن Transaction مربوطه:
|
||||
var transaction = await _context.Transactions
|
||||
.FirstOrDefaultAsync(t => t.Id == packageOrder.TransactionId);
|
||||
|
||||
if (transaction == null ||
|
||||
(transaction.Type != TransactionType.DepositIpg &&
|
||||
transaction.Type != TransactionType.DepositExternal1))
|
||||
{
|
||||
throw new ValidationException("تراکنش معتبر برای فعالسازی باشگاه یافت نشد.");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### قانون 4: NetworkMembership جدا از ClubMembership است
|
||||
|
||||
- **NetworkMembership**: موقع ثبتنام کاربر خودکار ایجاد میشود (با `ParentId`)
|
||||
- **ClubMembership**: فقط وقتی کاربر دکمه "فعالسازی باشگاه" را بزند ایجاد میشود
|
||||
- کاربر میتواند زیرمجموعه بگیرد بدون اینکه جزو باشگاه باشد (ولی سیاستگذاری میکنیم که قبل از گرفتن زیرمجموعه باید باشگاه را فعال کرده باشد)
|
||||
|
||||
---
|
||||
|
||||
### قانون 5: محاسبه کمیسیون فقط بعد از فعالسازی باشگاه
|
||||
|
||||
```csharp
|
||||
// در محاسبه کمیسیون:
|
||||
var clubMembership = await _context.ClubMemberships
|
||||
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsActive);
|
||||
|
||||
if (clubMembership == null)
|
||||
{
|
||||
// این کاربر کمیسیون نمیگیرد چون جزو باشگاه نیست
|
||||
return;
|
||||
}
|
||||
|
||||
// ادامه محاسبه کمیسیون...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Flow Diagrams
|
||||
|
||||
### 🔹 Flow 1: خرید پکیج از درگاه (سناریو 2)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ FrontOffice UI (کاربر) │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ انتخاب پکیج طلایی (56M) │
|
||||
└────────────┬─────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ PurchaseGoldenPackageCommand │
|
||||
│ - بررسی User.PackagePurchaseMethod │
|
||||
│ - ثبت UserOrder (Pending) │
|
||||
│ - Redirect به درگاه │
|
||||
└────────────┬─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ درگاه بانکی (IPG) │
|
||||
│ کاربر پرداخت میکند │
|
||||
└────────────┬─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ VerifyGoldenPackagePurchaseCommand │
|
||||
│ - Verify با بانک │
|
||||
│ - شارژ UserWallet.Balance (56M) │
|
||||
│ - ثبت Transaction (DepositIpg) │
|
||||
│ - ثبت UserWalletChangeLog │
|
||||
│ - Set User.PackagePurchaseMethod │
|
||||
│ = DirectPurchase │
|
||||
│ - بهروزرسانی UserOrder (Success) │
|
||||
└────────────┬─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ کاربر میتواند از فروشگاه عادی │
|
||||
│ خرید کند (با Balance) │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔹 Flow 2: فعالسازی باشگاه مشتریان
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ FrontOffice UI (کاربر وارد شده) │
|
||||
│ کاربر دکمه "فعالسازی باشگاه" را میزند │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ ActivateClubMembershipCommand │
|
||||
│ │
|
||||
│ 1. بررسی User.PackagePurchaseMethod │
|
||||
│ → باید != None باشد │
|
||||
│ │
|
||||
│ 2. بررسی UserWallet.Balance │
|
||||
│ → باید >= 56M باشد │
|
||||
│ │
|
||||
│ 3. پیدا کردن UserOrder با PackageId │
|
||||
│ → PaymentStatus = Success │
|
||||
│ │
|
||||
│ 4. پیدا کردن Transaction │
|
||||
│ → Type = DepositIpg یا │
|
||||
│ DepositExternal1 │
|
||||
│ │
|
||||
│ 5. ثبت/بهروزرسانی ClubMembership │
|
||||
│ - IsActive = true │
|
||||
│ - ActivatedAt = DateTime.Now │
|
||||
│ - PurchaseMethod = کپی از User │
|
||||
│ │
|
||||
└────────────┬─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ کاربر جزو باشگاه مشتریان شد │
|
||||
│ کمیسیونها شروع به محاسبه میکنند │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔹 Flow 3: شارژ کیف پول تخفیفی (سناریو 3)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ FrontOffice UI (کاربر) │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ انتخاب مبلغ دلخواه │
|
||||
│ (برای فروشگاه تخفیفی) │
|
||||
└────────────┬─────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ ChargeDiscountWalletCommand │
|
||||
│ - Redirect به درگاه │
|
||||
└────────────┬─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ درگاه بانکی (IPG) │
|
||||
│ کاربر پرداخت میکند │
|
||||
└────────────┬─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ VerifyDiscountWalletChargeCommand │
|
||||
│ - Verify با بانک │
|
||||
│ - شارژ UserWallet.DiscountBalance │
|
||||
│ - ثبت Transaction │
|
||||
│ (Type: DiscountWalletCharge) │
|
||||
│ - ثبت UserWalletChangeLog │
|
||||
└────────────┬─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ کاربر میتواند از فروشگاه تخفیفی │
|
||||
│ خرید کند (با DiscountBalance) │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**نکته:** در این سناریو هیچ `UserOrder` با `PackageId` ثبت نمیشود.
|
||||
|
||||
---
|
||||
|
||||
## 💻 Commands & Handlers
|
||||
|
||||
### 1️⃣ `PurchaseGoldenPackageCommand`
|
||||
|
||||
**مسئولیت:** ایجاد سفارش پکیج طلایی و Redirect به درگاه
|
||||
|
||||
```csharp
|
||||
public class PurchaseGoldenPackageCommand : IRequest<PaymentInitiateResult>
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
}
|
||||
|
||||
public class PurchaseGoldenPackageCommandHandler
|
||||
: IRequestHandler<PurchaseGoldenPackageCommand, PaymentInitiateResult>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
|
||||
public async Task<PaymentInitiateResult> Handle(
|
||||
PurchaseGoldenPackageCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. بررسی User
|
||||
var user = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
|
||||
// 2. بررسی اینکه قبلاً پکیج نخریده باشد
|
||||
if (user.PackagePurchaseMethod != PackagePurchaseMethod.None)
|
||||
{
|
||||
throw new ValidationException("شما قبلاً پکیج طلایی را خریداری کردهاید.");
|
||||
}
|
||||
|
||||
// 3. پیدا کردن پکیج طلایی
|
||||
var goldenPackage = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.Title.Contains("طلایی"), cancellationToken);
|
||||
|
||||
if (goldenPackage == null)
|
||||
throw new NotFoundException("پکیج طلایی یافت نشد.");
|
||||
|
||||
// 4. ایجاد UserOrder
|
||||
var order = new UserOrder
|
||||
{
|
||||
UserId = user.Id,
|
||||
PackageId = goldenPackage.Id,
|
||||
Amount = goldenPackage.Price, // 56,000,000
|
||||
PaymentStatus = PaymentStatus.Pending,
|
||||
DeliveryStatus = DeliveryStatus.None,
|
||||
UserAddressId = 0 // پکیج نیاز به آدرس ندارد
|
||||
};
|
||||
|
||||
_context.UserOrders.Add(order);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 5. Redirect به درگاه
|
||||
var paymentRequest = new PaymentRequest
|
||||
{
|
||||
Amount = order.Amount,
|
||||
OrderId = order.Id.ToString(),
|
||||
CallbackUrl = "https://yourdomain.com/verify-golden-package",
|
||||
Description = $"خرید پکیج طلایی"
|
||||
};
|
||||
|
||||
var result = await _paymentGateway.InitiatePaymentAsync(paymentRequest);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ `VerifyGoldenPackagePurchaseCommand`
|
||||
|
||||
**مسئولیت:** Verify پرداخت و شارژ کیف پول
|
||||
|
||||
```csharp
|
||||
public class VerifyGoldenPackagePurchaseCommand : IRequest<bool>
|
||||
{
|
||||
public long OrderId { get; set; }
|
||||
public string Authority { get; set; } // از درگاه
|
||||
}
|
||||
|
||||
public class VerifyGoldenPackagePurchaseCommandHandler
|
||||
: IRequestHandler<VerifyGoldenPackagePurchaseCommand, bool>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
|
||||
public async Task<bool> Handle(
|
||||
VerifyGoldenPackagePurchaseCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. پیدا کردن Order
|
||||
var order = await _context.UserOrders
|
||||
.Include(o => o.Package)
|
||||
.Include(o => o.User)
|
||||
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
|
||||
|
||||
if (order == null)
|
||||
throw new NotFoundException(nameof(UserOrder), request.OrderId);
|
||||
|
||||
// 2. Verify با بانک
|
||||
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
||||
request.Authority,
|
||||
order.Amount
|
||||
);
|
||||
|
||||
if (!verifyResult.IsSuccess)
|
||||
{
|
||||
order.PaymentStatus = PaymentStatus.Failed;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. شارژ کیف پول
|
||||
var wallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == order.UserId, cancellationToken);
|
||||
|
||||
wallet.Balance += order.Amount; // 56,000,000
|
||||
|
||||
// 4. ثبت Transaction
|
||||
var transaction = new Transactions
|
||||
{
|
||||
Amount = order.Amount,
|
||||
Description = "خرید پکیج طلایی از درگاه",
|
||||
PaymentStatus = PaymentStatus.Success,
|
||||
PaymentDate = DateTime.Now,
|
||||
RefId = verifyResult.RefId,
|
||||
Type = TransactionType.DepositIpg
|
||||
};
|
||||
|
||||
_context.Transactions.Add(transaction);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 5. ثبت ChangeLog
|
||||
var changeLog = new UserWalletChangeLog
|
||||
{
|
||||
UserId = order.UserId,
|
||||
Amount = order.Amount,
|
||||
ChangeType = WalletChangeType.Deposit,
|
||||
Description = "شارژ موجودی از پکیج طلایی",
|
||||
BalanceBefore = wallet.Balance - order.Amount,
|
||||
BalanceAfter = wallet.Balance
|
||||
};
|
||||
|
||||
_context.UserWalletChangeLogs.Add(changeLog);
|
||||
|
||||
// 6. بهروزرسانی Order
|
||||
order.TransactionId = transaction.Id;
|
||||
order.PaymentStatus = PaymentStatus.Success;
|
||||
order.PaymentDate = DateTime.Now;
|
||||
order.PaymentMethod = PaymentMethod.Online;
|
||||
|
||||
// 7. تغییر User.PackagePurchaseMethod
|
||||
order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ `ActivateClubMembershipCommand`
|
||||
|
||||
**مسئولیت:** فعالسازی عضویت در باشگاه مشتریان
|
||||
|
||||
```csharp
|
||||
public class ActivateClubMembershipCommand : IRequest<bool>
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
}
|
||||
|
||||
public class ActivateClubMembershipCommandHandler
|
||||
: IRequestHandler<ActivateClubMembershipCommand, bool>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public async Task<bool> Handle(
|
||||
ActivateClubMembershipCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. بررسی User
|
||||
var user = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
|
||||
// 2. بررسی اینکه پکیج خریده باشد
|
||||
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
|
||||
{
|
||||
throw new ValidationException(
|
||||
"برای فعالسازی باشگاه مشتریان ابتدا باید پکیج طلایی خریداری کنید."
|
||||
);
|
||||
}
|
||||
|
||||
// 3. بررسی موجودی
|
||||
var wallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == user.Id, cancellationToken);
|
||||
|
||||
if (wallet.Balance < 56_000_000)
|
||||
{
|
||||
throw new ValidationException(
|
||||
"برای فعالسازی باشگاه مشتریان باید حداقل 56 میلیون تومان موجودی اصلی داشته باشید."
|
||||
);
|
||||
}
|
||||
|
||||
// 4. بررسی UserOrder
|
||||
var packageOrder = await _context.UserOrders
|
||||
.FirstOrDefaultAsync(o =>
|
||||
o.UserId == user.Id &&
|
||||
o.PackageId != null &&
|
||||
o.PaymentStatus == PaymentStatus.Success,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (packageOrder == null)
|
||||
{
|
||||
throw new ValidationException("سفارش پکیج طلایی یافت نشد.");
|
||||
}
|
||||
|
||||
// 5. بررسی Transaction
|
||||
var transaction = await _context.Transactions
|
||||
.FirstOrDefaultAsync(t => t.Id == packageOrder.TransactionId, cancellationToken);
|
||||
|
||||
if (transaction == null ||
|
||||
(transaction.Type != TransactionType.DepositIpg &&
|
||||
transaction.Type != TransactionType.DepositExternal1))
|
||||
{
|
||||
throw new ValidationException("تراکنش معتبر برای فعالسازی باشگاه یافت نشد.");
|
||||
}
|
||||
|
||||
// 6. بررسی اینکه قبلاً فعال نکرده باشد
|
||||
var existingMembership = await _context.ClubMemberships
|
||||
.FirstOrDefaultAsync(c => c.UserId == user.Id, cancellationToken);
|
||||
|
||||
if (existingMembership != null && existingMembership.IsActive)
|
||||
{
|
||||
throw new ValidationException("شما قبلاً عضو باشگاه مشتریان هستید.");
|
||||
}
|
||||
|
||||
// 7. ثبت یا بهروزرسانی ClubMembership
|
||||
if (existingMembership == null)
|
||||
{
|
||||
existingMembership = new ClubMembership
|
||||
{
|
||||
UserId = user.Id,
|
||||
IsActive = true,
|
||||
ActivatedAt = DateTime.Now,
|
||||
InitialContribution = 56_000_000,
|
||||
TotalEarned = 0,
|
||||
PurchaseMethod = user.PackagePurchaseMethod
|
||||
};
|
||||
|
||||
_context.ClubMemberships.Add(existingMembership);
|
||||
}
|
||||
else
|
||||
{
|
||||
existingMembership.IsActive = true;
|
||||
existingMembership.ActivatedAt = DateTime.Now;
|
||||
existingMembership.PurchaseMethod = user.PackagePurchaseMethod;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4️⃣ `ChargeDiscountWalletCommand` (سناریو 3)
|
||||
|
||||
**مسئولیت:** شارژ کیف پول تخفیفی
|
||||
|
||||
```csharp
|
||||
public class ChargeDiscountWalletCommand : IRequest<PaymentInitiateResult>
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public long Amount { get; set; }
|
||||
}
|
||||
|
||||
public class ChargeDiscountWalletCommandHandler
|
||||
: IRequestHandler<ChargeDiscountWalletCommand, PaymentInitiateResult>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
|
||||
public async Task<PaymentInitiateResult> Handle(
|
||||
ChargeDiscountWalletCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. بررسی User
|
||||
var user = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
|
||||
// 2. بررسی مبلغ (حداقل 10,000 تومان)
|
||||
if (request.Amount < 10_000)
|
||||
{
|
||||
throw new ValidationException("حداقل مبلغ شارژ 10,000 تومان است.");
|
||||
}
|
||||
|
||||
// 3. Redirect به درگاه
|
||||
var paymentRequest = new PaymentRequest
|
||||
{
|
||||
Amount = request.Amount,
|
||||
OrderId = $"DISCOUNT_{user.Id}_{DateTime.Now:yyyyMMddHHmmss}",
|
||||
CallbackUrl = "https://yourdomain.com/verify-discount-wallet",
|
||||
Description = $"شارژ کیف پول تخفیفی"
|
||||
};
|
||||
|
||||
var result = await _paymentGateway.InitiatePaymentAsync(paymentRequest);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5️⃣ `VerifyDiscountWalletChargeCommand` (سناریو 3)
|
||||
|
||||
**مسئولیت:** Verify و شارژ DiscountBalance
|
||||
|
||||
```csharp
|
||||
public class VerifyDiscountWalletChargeCommand : IRequest<bool>
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public long Amount { get; set; }
|
||||
public string Authority { get; set; }
|
||||
}
|
||||
|
||||
public class VerifyDiscountWalletChargeCommandHandler
|
||||
: IRequestHandler<VerifyDiscountWalletChargeCommand, bool>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
|
||||
public async Task<bool> Handle(
|
||||
VerifyDiscountWalletChargeCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. پیدا کردن User
|
||||
var user = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
|
||||
// 2. Verify با بانک
|
||||
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
||||
request.Authority,
|
||||
request.Amount
|
||||
);
|
||||
|
||||
if (!verifyResult.IsSuccess)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. شارژ DiscountBalance
|
||||
var wallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == user.Id, cancellationToken);
|
||||
|
||||
wallet.DiscountBalance += request.Amount;
|
||||
|
||||
// 4. ثبت Transaction
|
||||
var transaction = new Transactions
|
||||
{
|
||||
Amount = request.Amount,
|
||||
Description = "شارژ کیف پول تخفیفی",
|
||||
PaymentStatus = PaymentStatus.Success,
|
||||
PaymentDate = DateTime.Now,
|
||||
RefId = verifyResult.RefId,
|
||||
Type = TransactionType.DiscountWalletCharge
|
||||
};
|
||||
|
||||
_context.Transactions.Add(transaction);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 5. ثبت ChangeLog
|
||||
var changeLog = new UserWalletChangeLog
|
||||
{
|
||||
UserId = user.Id,
|
||||
Amount = request.Amount,
|
||||
ChangeType = WalletChangeType.Deposit,
|
||||
Description = "شارژ موجودی تخفیفی",
|
||||
BalanceBefore = wallet.DiscountBalance - request.Amount,
|
||||
BalanceAfter = wallet.DiscountBalance
|
||||
};
|
||||
|
||||
_context.UserWalletChangeLogs.Add(changeLog);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 تسکهای پیادهسازی
|
||||
|
||||
### Phase 1: Entity Changes (1 روز)
|
||||
|
||||
1. **ایجاد `PackagePurchaseMethod` Enum**
|
||||
- محل: `CMSMicroservice.Domain/Enums/PackagePurchaseMethod.cs`
|
||||
- مقادیر: None, DayaLoan, DirectPurchase
|
||||
|
||||
2. **اضافه کردن فیلد به `User`**
|
||||
- فیلد: `PackagePurchaseMethod PackagePurchaseMethod`
|
||||
- مقدار پیشفرض: `PackagePurchaseMethod.None`
|
||||
|
||||
3. **اضافه کردن فیلد به `ClubMembership`**
|
||||
- فیلد: `PackagePurchaseMethod PurchaseMethod`
|
||||
|
||||
4. **ایجاد Migration**
|
||||
```bash
|
||||
dotnet ef migrations add AddPackagePurchaseMethod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Commands (2 روز)
|
||||
|
||||
1. **`PurchaseGoldenPackageCommand`**
|
||||
- بررسی `User.PackagePurchaseMethod`
|
||||
- ثبت `UserOrder` با `PackageId`
|
||||
- Redirect به درگاه
|
||||
|
||||
2. **`VerifyGoldenPackagePurchaseCommand`**
|
||||
- Verify پرداخت
|
||||
- شارژ `Balance`
|
||||
- ثبت `Transaction` (DepositIpg)
|
||||
- Set `User.PackagePurchaseMethod = DirectPurchase`
|
||||
|
||||
3. **`ActivateClubMembershipCommand`**
|
||||
- چکهای امنیتی (UserOrder + Transaction)
|
||||
- ثبت/بهروزرسانی `ClubMembership`
|
||||
|
||||
4. **`ChargeDiscountWalletCommand` + `VerifyDiscountWalletChargeCommand`**
|
||||
- شارژ `DiscountBalance`
|
||||
- ثبت `Transaction` (DiscountWalletCharge)
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: بهروزرسانی DayaLoan Flow (0.5 روز)
|
||||
|
||||
- تغییر `ProcessDayaLoanCommandHandler`:
|
||||
```csharp
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Unit Tests (1 روز)
|
||||
|
||||
1. تست `PurchaseGoldenPackageCommand`:
|
||||
- کاربری که قبلاً پکیج خریده → باید خطا بدهد
|
||||
- کاربر جدید → باید Order ایجاد شود
|
||||
|
||||
2. تست `ActivateClubMembershipCommand`:
|
||||
- کاربر بدون پکیج → خطا
|
||||
- کاربر با موجودی کمتر از 56M → خطا
|
||||
- کاربر معتبر → موفق
|
||||
|
||||
3. تست `VerifyDiscountWalletChargeCommand`:
|
||||
- پرداخت موفق → `DiscountBalance` افزایش یابد
|
||||
- پرداخت ناموفق → هیچ تغییری نکند
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Documentation (0.5 روز)
|
||||
|
||||
- بهروزرسانی `implementation-progress.md`
|
||||
- لینک از `REMAINING-TASKS-CONSOLIDATED.md`
|
||||
|
||||
---
|
||||
|
||||
## 📊 خلاصه Timeline
|
||||
|
||||
| Phase | عنوان | زمان |
|
||||
|-------|-------|------|
|
||||
| 1 | Entity Changes | 1 روز |
|
||||
| 2 | Commands & Handlers | 2 روز |
|
||||
| 3 | DayaLoan Flow Update | 0.5 روز |
|
||||
| 4 | Unit Tests | 1 روز |
|
||||
| 5 | Documentation | 0.5 روز |
|
||||
| **جمع** | | **5 روز** |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 مراجع
|
||||
|
||||
- [DayaLoan Integration](./daya-loan-integration.md)
|
||||
- [Manual Payment System](./manual-payment-system.md)
|
||||
- [Implementation Progress](./implementation-progress.md)
|
||||
- [REMAINING-TASKS](../REMAINING-TASKS-CONSOLIDATED.md)
|
||||
|
||||
---
|
||||
|
||||
**تاریخ آخرین بهروزرسانی:** 2024-12-02
|
||||
**نویسنده:** GitHub Copilot
|
||||
**وضعیت:** ✅ تایید شده توسط کاربر
|
||||
Reference in New Issue
Block a user