# 🪄 پلن پیاده‌سازی کیف‌پول جادویی > **مرجع:** [MAGIC-WALLET-SPEC](./MAGIC-WALLET-SPEC.md) > **تخمین کل:** ~۷ روز کاری > **وضعیت:** ✅ کامل — همه ۶ فاز پیاده‌سازی و مرج شده > **وابستگی مستقل:** تکمیل ChargeDiscountWallet (ربطی به جادویی ندارد) ✅ --- ## فازبندی ### فاز ۱ — مدل داده و Migration (روز ۱) **هدف:** زیرساخت دیتابیس و enum‌ها ``` فایل‌های تغییری: ├── UserWallet.cs → + WalletMode, MagicTotalDeposited, MagicTotalCredited, MagicActivatedAt, MagicCompletedAt ├── WalletMode.cs → enum جدید (Normal=0, Magic=1) ├── TransactionType.cs → + MagicWalletDeposit=14, MagicWalletBonus=15 ├── SystemConstants.cs → + MagicWalletMultiplier, MagicWalletMaxDeposit, MagicWalletMaxCredit ├── UserWalletConfiguration.cs → EF config برای فیلدهای جدید ├── ClubMembershipCycle.cs → 🆕 entity جدید (حل مشکل تاریخ کمیسیون) ├── ClubMembershipCycleConfiguration.cs → EF config ├── Migration: AddMagicWalletFields → dotnet ef migrations add └── Migration: AddClubMembershipCycle → dotnet ef migrations add + data seed ``` **تست:** Migration اجرا بشه، فیلدها در DB ایجاد بشن، default‌ها درست باشن. هر ClubMembership موجود یه رکورد Cycle=1 داشته باشه. --- ### فاز ۲ — Trigger ورود/خروج Magic (روز ۲) **هدف:** State Machine خودکار ``` فایل‌های تغییری: ├── SubmitShopBuyOrderCommandHandler.cs │ ├── بعد از کسر Balance: check ورود به Magic │ └── بعد از کسر Balance: check خروج از Magic │ ├── ActivateClubMembershipCommandHandler.cs │ ├── ActivatedAt فقط بار اول ست بشه (دیگه overwrite نشه) │ └── هر بار یک ClubMembershipCycle جدید اضافه بشه │ ├── (Optional) Domain Event: WalletModeChangedEvent │ └── برای لاگ و نوتیفیکیشن │ └── User.cs (یا UserWallet) └── + PurchaseCycleCount (int) — تعداد دور خرید پکیج ``` **تست:** - سناریو ۱: Balance=0 بعد از خرید → WalletMode=Magic ✅ - سناریو ۲: بدون پکیج + Balance=0 → نباید Magic بشه ❌ - سناریو ۳: Magic + Balance=0 + **TotalDeposited=50M** (سقف پر نشده) → **هنوز Magic!** نباید خارج بشه ❌ - سناریو ۴: Magic + Balance=0 + **TotalDeposited=100M** (سقف پر) → خروج ✅ - سناریو ۵: Magic + Balance=30M + TotalDeposited=100M → **هنوز Magic!** (بالانس داره) ❌ - سناریو ۶: خروج از Magic → خرید مجدد پکیج → Balance=0 → Magic مجدد با **سقف ریست‌شده** ✅ - سناریو ۷: دور دوم → TotalDeposited, TotalCredited = 0 (ریست) ✅ --- ### فاز ۳ — API شارژ جادویی (روز ۳-۴) **هدف:** مسیر کامل شارژ از درگاه با ضریب ×2.5 ``` فایل‌های جدید: ├── InitiateMagicChargeCommand.cs ├── InitiateMagicChargeCommandHandler.cs ├── InitiateMagicChargeCommandValidator.cs ├── VerifyMagicChargeCommand.cs ├── VerifyMagicChargeCommandHandler.cs ├── MagicWalletController.cs → GET /api/wallet/verify-magic-charge │ ├── userwallet.proto → + InitiateMagicCharge, GetMagicWalletStatus RPCs └── UserWalletService.cs → implement new RPCs نکات مهم: ├── هر شارژ = ۲ تراکنش (Deposit + Bonus) ├── هر شارژ = ۱ WalletChangeLog (اجباری) ├── Validation: WalletMode==Magic && TotalDeposited+Amount <= Cap └── Callback: /api/wallet/verify-magic-charge → redirect FrontOffice ``` **تست:** - واریز 10M → Balance += 25M, TotalDeposited += 10M ✅ - واریز بیشتر از سقف → خطا ❌ - واریز در Normal Mode → خطا ❌ - ۲ تراکنش + ۱ لاگ ثبت شده ✅ --- ### فاز ۴ — غیرفعال‌سازی کمیسیون + تاریخ Cycle (روز ۴.۵) **هدف:** کاربرهای Magic از کمیسیون خارج بشن + تاریخ کمیسیون از Cycle بخونه ``` فایل‌های تغییری: ├── CalculateWeeklyBalancesCommandHandler.cs │ ├── فیلتر: WHERE wallet.WalletMode != Magic │ └── تاریخ: ActivatedAt → ClubMembershipCycle.PackagePurchasedAt │ ├── sp_CalculateWeeklyBalances.sql │ ├── + JOIN UserWallets WHERE WalletMode = 0 │ └── WHERE cm.ActivatedAt → cc.PackagePurchasedAt (AND cc.IsCurrentCycle = 1) │ └── WeekRepository (اگه date range query داره) └── آپدیت query ``` **تست:** - کاربر Magic در محاسبات هفتگی شرکت نکنه ✅ - کاربر دور ۲ (پکیج مجدد): با تاریخ PackagePurchasedAt جدید امتیاز بگیره ✅ - تاریخ اصلی ActivatedAt تغییر نکرده باشه ✅ --- ### فاز ۵ — صفحات FrontOffice (روز ۵-۶) **هدف:** UI شارژ جادویی + نمایش وضعیت ``` فایل‌های جدید: ├── Pages/Profile/MagicWallet.razor → فرم شارژ + پروگرس‌بار سقف ├── Pages/Profile/MagicWallet.razor.cs → code-behind ├── Pages/Profile/MagicPaymentCallback.razor → نتیجه پرداخت └── Pages/Profile/MagicPaymentCallback.razor.cs فایل‌های تغییری: ├── WalletService.cs → + InitiateMagicChargeAsync, GetMagicWalletStatusAsync ├── RouteConstants.cs → + MagicWallet, MagicPaymentCallback ├── Pages/Profile/Index.razor → بنر Magic Mode ├── Pages/Profile/Wallet.razor → پروگرس سقف + لینک شارژ └── NavMenu / Sidebar → لینک شرطی به صفحه جادویی ``` **UI شارژ جادویی:** ``` ┌──────────────────────────────────────────────┐ │ 🪄 کیف‌پول جادویی │ │ │ │ وضعیت: فعال ✅ │ │ مجموع واریزی: 30M / 100M تومان │ │ ██████████░░░░░░░░░░░░░░░░░░░░ 30% │ │ مجموع اعتبار دریافتی: 75M تومان │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ مبلغ واریز: [________] تومان │ │ │ │ اعتبار دریافتی: 0 × 2.5 = 0 تومان │ │ │ │ باقیمانده سقف: 70M تومان │ │ │ │ │ │ │ │ [ 🔒 پرداخت از درگاه ] │ │ │ └──────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ ``` --- ### فاز ۶ — محدودیت خرید مجدد پکیج (روز ۷) **هدف:** بعد از Magic فقط IPG مجاز باشه ``` فایل‌های تغییری: ├── CheckAndProcessDayaLoansCommandHandler.cs │ └── if PurchaseCycleCount > 0 → reject │ ├── Package Purchase UI (FrontOffice) │ └── if PurchaseCycleCount > 0 → hide Daya button │ └── ActivateClubMembershipCommandHandler.cs └── if WalletMode == Magic → "ابتدا جادویی تمام شود" ``` --- ## Checklist پیاده‌سازی - [x] **فاز ۱:** WalletMode enum - [x] **فاز ۱:** UserWallet entity + 5 فیلد جدید - [x] **فاز ۱:** ClubMembershipCycle entity (جدید) - [x] **فاز ۱:** TransactionType + 2 مقدار - [x] **فاز ۱:** SystemConstants + 3 ثابت - [x] **فاز ۱:** EF Configuration (UserWallet + ClubMembershipCycle) - [x] **فاز ۱:** Migration: AddMagicWalletFields (u21 — اعمال شده ✅) - [x] **فاز ۱:** Migration: AddClubMembershipCycle + data seed (74 رکورد seed شده ✅) - [x] **فاز ۲:** Trigger ورود Magic (SubmitShopBuyOrder) - [x] **فاز ۲:** Trigger خروج Magic - [x] **فاز ۲:** ActivateClubMembership → ActivatedAt نگه‌داشته بشه + Cycle جدید - [x] **فاز ۲:** PurchaseCycleCount - [x] **فاز ۳:** InitiateMagicChargeCommand + Handler - [x] **فاز ۳:** VerifyMagicChargeCommand + Handler - [x] **فاز ۳:** MagicWalletController (HTTP callback) - [x] **فاز ۳:** gRPC Proto + Service - [x] **فاز ۳:** ۲ تراکنش + ۱ لاگ (اجباری) - [x] **فاز ۴:** فیلتر کمیسیون Magic (C# handler + SP) - [x] **فاز ۴:** تاریخ کمیسیون: ActivatedAt → Cycle.PackagePurchasedAt (C# + SP) - [x] **فاز ۵:** MagicWallet.razor - [x] **فاز ۵:** MagicPaymentCallback — نتیجه پرداخت از طریق ?payment= query param در همان MagicWallet.razor هندل میشه - [x] **فاز ۵:** WalletService gRPC client - [x] **فاز ۵:** Profile + Wallet page updates - [x] **فاز ۶:** Daya restriction (CheckAndProcessDayaLoansCommandHandler + FO Purchase UI) - [x] **فاز ۶:** Club activation restriction (ActivateClubMembershipCommandHandler + WalletMode guard) --- ## وابستگی مستقل: تکمیل ChargeDiscountWallet ✅ > ✅ **تکمیل شد** — مستقل از کیف‌پول جادویی پیاده‌سازی شد. ``` انجام شده: ├── ChargeDiscountWalletCommandHandler — CQRS handler ✅ ├── VerifyDiscountWalletChargeCommandHandler — ✅ ├── PaymentCallbackController → GET /api/wallet/verify-discount-charge ✅ ├── userwallet.proto → rpc InitiateDiscountCharge ✅ ├── UserWalletService.cs → InitiateDiscountCharge override ✅ ├── WalletService.cs (FO) → InitiateDiscountChargeAsync ✅ ├── ChargeDiscountWallet.razor + .razor.cs (FO) ✅ ├── RouteConstants → ChargeDiscountWallet ✅ └── Wallet.razor → دکمه شارژ اعتباری ✅ ```