Files
docs/business/package-purchase-system.md
T

35 KiB

Package Purchase System - سیستم خرید پکیج طلایی

تاریخ ایجاد: 2024-12-02
وضعیت: در حال طراحی
اولویت: 🔴 بسیار بالا


📋 فهرست

  1. مقدمه
  2. سه سناریوی اصلی
  3. Entity Changes
  4. Business Rules
  5. Flow Diagrams
  6. 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

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

// اضافه کردن این فیلد به User.cs:

/// <summary>
/// نحوه خرید پکیج طلایی (برای جلوگیری از خرید مجدد)
/// </summary>
public PackagePurchaseMethod PackagePurchaseMethod { get; set; } = PackagePurchaseMethod.None;

منطق:

  • وقتی کاربر سناریو 1 یا 2 را انجام می‌دهد، این فیلد تغییر می‌کند
  • اگر PackagePurchaseMethod != None باشد، کاربر نمی‌تواند دوباره پکیج خریداری کند

3️⃣ تغییرات ClubMembership Entity

// اضافه کردن این فیلد به ClubMembership.cs:

/// <summary>
/// نحوه خرید پکیج که منجر به فعالسازی باشگاه شد
/// </summary>
public PackagePurchaseMethod PurchaseMethod { get; set; }

منطق:

  • وقتی کاربر دکمه "فعالسازی باشگاه" را می‌زند، این فیلد از User.PackagePurchaseMethod کپی می‌شود
  • برای گزارش‌گیری و تحلیل: چند نفر از طریق وام دایا و چند نفر از طریق خرید مستقیم عضو شدند

4️⃣ تغییرات TransactionType Enum

// فعلاً موجود است:
public enum TransactionType
{
    Buy = 0,
    DepositIpg = 1,            // پرداخت از درگاه (سناریو 2)
    DepositExternal1 = 2,      // وام دایا (سناریو 1)
    Withdraw = 3,
    NetworkCommission = 10,
    ClubActivation = 11,
    DiscountWalletCharge = 12  // شارژ کیف پول تخفیفی (سناریو 3) ✅
}

نکته: DiscountWalletCharge از قبل وجود دارد، پس نیازی به تغییر نیست.


📐 Business Rules

قانون 1: یک کاربر فقط یک بار می‌تواند پکیج طلایی خریداری کند

// Check قبل از خرید پکیج:
if (user.PackagePurchaseMethod != PackagePurchaseMethod.None)
{
    throw new ValidationException("شما قبلاً پکیج طلایی را خریداری کرده‌اید.");
}

قانون 2: فعالسازی باشگاه فقط با موجودی اصلی (Balance) امکان‌پذیر است

// Check موقع فعالسازی باشگاه:
var userWallet = await _context.UserWallets
    .FirstOrDefaultAsync(w => w.UserId == userId);

if (userWallet.Balance < 56_000_000)
{
    throw new ValidationException("برای فعالسازی باشگاه مشتریان باید حداقل 56 میلیون تومان موجودی اصلی داشته باشید.");
}

قانون 3: فعالسازی باشگاه فقط برای کسانی که پکیج خریده‌اند

// 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: محاسبه کمیسیون فقط بعد از فعالسازی باشگاه

// در محاسبه کمیسیون:
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 به درگاه

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 پرداخت و شارژ کیف پول

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

مسئولیت: فعالسازی عضویت در باشگاه مشتریان

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)

مسئولیت: شارژ کیف پول تخفیفی

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

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

    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:
    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 روز

🔗 مراجع


تاریخ آخرین به‌روزرسانی: 2024-12-02
نویسنده: GitHub Copilot
وضعیت: تایید شده توسط کاربر