Files
docs/01-BUSINESS/package-purchase-system.md
T
masoodafar-web 119e870a26 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
2025-12-04 17:32:31 +03:30

968 lines
35 KiB
Markdown

# 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
**وضعیت:** ✅ تایید شده توسط کاربر