# 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; /// /// نحوه خرید پکیج طلایی توسط کاربر /// public enum PackagePurchaseMethod { /// /// هنوز پکیج خریداری نکرده /// None = 0, /// /// از طریق وام دایا /// DayaLoan = 1, /// /// از طریق پرداخت مستقیم درگاه بانکی /// DirectPurchase = 2 } ``` **محل:** `CMS/src/CMSMicroservice.Domain/Enums/PackagePurchaseMethod.cs` --- ### 2️⃣ **تغییرات `User` Entity** ```csharp // اضافه کردن این فیلد به User.cs: /// /// نحوه خرید پکیج طلایی (برای جلوگیری از خرید مجدد) /// public PackagePurchaseMethod PackagePurchaseMethod { get; set; } = PackagePurchaseMethod.None; ``` **منطق:** - وقتی کاربر سناریو 1 یا 2 را انجام می‌دهد، این فیلد تغییر می‌کند - اگر `PackagePurchaseMethod != None` باشد، کاربر نمی‌تواند دوباره پکیج خریداری کند --- ### 3️⃣ **تغییرات `ClubMembership` Entity** ```csharp // اضافه کردن این فیلد به ClubMembership.cs: /// /// نحوه خرید پکیج که منجر به فعالسازی باشگاه شد /// 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 { public long UserId { get; set; } } public class PurchaseGoldenPackageCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IPaymentGatewayService _paymentGateway; public async Task 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 { public long OrderId { get; set; } public string Authority { get; set; } // از درگاه } public class VerifyGoldenPackagePurchaseCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IPaymentGatewayService _paymentGateway; public async Task 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 { public long UserId { get; set; } } public class ActivateClubMembershipCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; public async Task 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 { public long UserId { get; set; } public long Amount { get; set; } } public class ChargeDiscountWalletCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IPaymentGatewayService _paymentGateway; public async Task 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 { public long UserId { get; set; } public long Amount { get; set; } public string Authority { get; set; } } public class VerifyDiscountWalletChargeCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IPaymentGatewayService _paymentGateway; public async Task 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 **وضعیت:** ✅ تایید شده توسط کاربر