diff --git a/roadmap/MAGIC-WALLET-PLAN.md b/roadmap/MAGIC-WALLET-PLAN.md index 57b457c..40940df 100644 --- a/roadmap/MAGIC-WALLET-PLAN.md +++ b/roadmap/MAGIC-WALLET-PLAN.md @@ -19,10 +19,13 @@ ├── TransactionType.cs → + MagicWalletDeposit=14, MagicWalletBonus=15 ├── SystemConstants.cs → + MagicWalletMultiplier, MagicWalletMaxDeposit, MagicWalletMaxCredit ├── UserWalletConfiguration.cs → EF config برای فیلدهای جدید -└── Migration: AddMagicWalletFields → dotnet ef migrations add +├── ClubMembershipCycle.cs → 🆕 entity جدید (حل مشکل تاریخ کمیسیون) +├── ClubMembershipCycleConfiguration.cs → EF config +├── Migration: AddMagicWalletFields → dotnet ef migrations add +└── Migration: AddClubMembershipCycle → dotnet ef migrations add + data seed ``` -**تست:** Migration اجرا بشه، فیلدها در DB ایجاد بشن، default‌ها درست باشن. +**تست:** Migration اجرا بشه، فیلدها در DB ایجاد بشن، default‌ها درست باشن. هر ClubMembership موجود یه رکورد Cycle=1 داشته باشه. --- @@ -36,6 +39,10 @@ │ ├── بعد از کسر Balance: check ورود به Magic │ └── بعد از کسر Balance: check خروج از Magic │ +├── ActivateClubMembershipCommandHandler.cs +│ ├── ActivatedAt فقط بار اول ست بشه (دیگه overwrite نشه) +│ └── هر بار یک ClubMembershipCycle جدید اضافه بشه +│ ├── (Optional) Domain Event: WalletModeChangedEvent │ └── برای لاگ و نوتیفیکیشن │ @@ -85,20 +92,28 @@ --- -### فاز ۴ — غیرفعال‌سازی کمیسیون (روز ۴.۵) +### فاز ۴ — غیرفعال‌سازی کمیسیون + تاریخ Cycle (روز ۴.۵) -**هدف:** کاربرهای Magic از کمیسیون خارج بشن +**هدف:** کاربرهای Magic از کمیسیون خارج بشن + تاریخ کمیسیون از Cycle بخونه ``` فایل‌های تغییری: ├── CalculateWeeklyBalancesCommandHandler.cs -│ └── فیلتر: WHERE wallet.WalletMode != Magic +│ ├── فیلتر: WHERE wallet.WalletMode != Magic +│ └── تاریخ: ActivatedAt → ClubMembershipCycle.PackagePurchasedAt │ -└── (یا SP اگه از Stored Procedure استفاده میکنه) - └── sp_CalculateWeeklyBalances → + JOIN UserWallets WHERE WalletMode = 0 +├── sp_CalculateWeeklyBalances.sql +│ ├── + JOIN UserWallets WHERE WalletMode = 0 +│ └── WHERE cm.ActivatedAt → cc.PackagePurchasedAt (AND cc.IsCurrentCycle = 1) +│ +└── WeekRepository (اگه date range query داره) + └── آپدیت query ``` -**تست:** کاربر Magic در محاسبات هفتگی شرکت نکنه ✅ +**تست:** +- کاربر Magic در محاسبات هفتگی شرکت نکنه ✅ +- کاربر دور ۲ (پکیج مجدد): با تاریخ PackagePurchasedAt جدید امتیاز بگیره ✅ +- تاریخ اصلی ActivatedAt تغییر نکرده باشه ✅ --- @@ -165,19 +180,23 @@ - [ ] **فاز ۱:** WalletMode enum - [ ] **فاز ۱:** UserWallet entity + 5 فیلد جدید +- [ ] **فاز ۱:** ClubMembershipCycle entity (جدید) - [ ] **فاز ۱:** TransactionType + 2 مقدار - [ ] **فاز ۱:** SystemConstants + 3 ثابت -- [ ] **فاز ۱:** EF Configuration -- [ ] **فاز ۱:** Migration +- [ ] **فاز ۱:** EF Configuration (UserWallet + ClubMembershipCycle) +- [ ] **فاز ۱:** Migration: AddMagicWalletFields +- [ ] **فاز ۱:** Migration: AddClubMembershipCycle + data seed - [ ] **فاز ۲:** Trigger ورود Magic (SubmitShopBuyOrder) - [ ] **فاز ۲:** Trigger خروج Magic +- [ ] **فاز ۲:** ActivateClubMembership → ActivatedAt نگه‌داشته بشه + Cycle جدید - [ ] **فاز ۲:** PurchaseCycleCount - [ ] **فاز ۳:** InitiateMagicChargeCommand + Handler - [ ] **فاز ۳:** VerifyMagicChargeCommand + Handler - [ ] **فاز ۳:** MagicWalletController (HTTP callback) - [ ] **فاز ۳:** gRPC Proto + Service - [ ] **فاز ۳:** ۲ تراکنش + ۱ لاگ (اجباری) -- [ ] **فاز ۴:** فیلتر کمیسیون (C# یا SP) +- [ ] **فاز ۴:** فیلتر کمیسیون Magic (C# handler + SP) +- [ ] **فاز ۴:** تاریخ کمیسیون: ActivatedAt → Cycle.PackagePurchasedAt (C# + SP) - [ ] **فاز ۵:** MagicWallet.razor - [ ] **فاز ۵:** MagicPaymentCallback.razor - [ ] **فاز ۵:** WalletService gRPC client diff --git a/roadmap/MAGIC-WALLET-SPEC.md b/roadmap/MAGIC-WALLET-SPEC.md index b428e35..9280795 100644 --- a/roadmap/MAGIC-WALLET-SPEC.md +++ b/roadmap/MAGIC-WALLET-SPEC.md @@ -337,7 +337,87 @@ public const long MagicWalletMaxDeposit = 1_000_000_000; // 100M تومان = public const long MagicWalletMaxCredit = 2_500_000_000; // 250M تومان = 2.5B ریال ``` -### ۷.۵ EF Migration +### ۷.۵ ClubMembershipCycle — جدول جدید (حل مشکل تاریخ کمیسیون) + +#### مشکل فعلی + +``` +⚠️ الان محاسبه کمیسیون هفتگی از ClubMembership.ActivatedAt استفاده میکنه: + + WHERE cm.ActivatedAt >= @StartDate AND cm.ActivatedAt <= @EndDate + +وقتی کاربر دور دوم پکیج بخره، ActivateClubMembership این تاریخ رو overwrite میکنه: + entity.ActivatedAt = DateTime.Now; // ← تاریخ اصلی از بین میره! + +مشکل: تاریخ اولین فعال‌سازی باشگاه از دست میره. +``` + +#### راه‌حل: جدول `ClubMembershipCycle` + +به‌جای آپدیت کردن `ActivatedAt`، هر بار که پکیج خریده میشه یک رکورد جدید در جدول `ClubMembershipCycle` ایجاد میشه. محاسبه کمیسیون از این جدول استفاده میکنه. + +```csharp +// Entity جدید: +public class ClubMembershipCycle : BaseAuditableEntity +{ + public long UserId { get; set; } + public User User { get; set; } + public long ClubMembershipId { get; set; } + public ClubMembership ClubMembership { get; set; } + public int CycleNumber { get; set; } // شماره دور (1, 2, 3...) + public DateTime PackagePurchasedAt { get; set; } // تاریخ خرید پکیج + public DateTime? MagicStartedAt { get; set; } // شروع Magic (Balance=0) + public DateTime? MagicCompletedAt { get; set; } // پایان Magic + public PackagePurchaseMethod PurchaseMethod { get; set; } // IPG یا Daya + public long PackageAmount { get; set; } // 56M + public bool IsCurrentCycle { get; set; } // فقط یکی true +} +``` + +#### تغییرات در منطق کمیسیون + +```sql +-- قبل (غلط — ActivatedAt از بین میره): +WHERE cm.ActivatedAt >= @StartDate AND cm.ActivatedAt <= @EndDate + +-- بعد (درست — از جدول Cycle): +WHERE cc.PackagePurchasedAt >= @StartDate + AND cc.PackagePurchasedAt <= @EndDate + AND cc.IsCurrentCycle = 1 +``` + +#### ClubMembership — بدون تغییر ساختاری + +``` +ClubMembership: + ├── ActivatedAt → تاریخ اولین فعال‌سازی (هرگز overwrite نمیشه ✅) + ├── IsActive → وضعیت فعلی باشگاه + └── + Cycles (nav prop) → لیست دورها +``` + +#### مثال عملی + +``` +ClubMembership #42: + UserId = 100 + ActivatedAt = 1403/10/15 ← اولین بار (حفظ میشه ✅) + IsActive = true + +ClubMembershipCycles: + ┌────┬──────┬───────────────────┬──────────────┬─────────────┐ + │ Id │ Cycle│ PackagePurchasedAt│ PurchaseMethod│IsCurrentCycle│ + ├────┼──────┼───────────────────┼──────────────┼─────────────┤ + │ 1 │ 1 │ 1403/10/15 │ DayaLoan │ false │ + │ 2 │ 2 │ 1404/01/20 │ DirectIPG │ false │ + │ 3 │ 3 │ 1404/04/05 │ DirectIPG │ true ✅ │ + └────┴──────┴───────────────────┴──────────────┴─────────────┘ + +→ کمیسیون هفته 1404/04/05 تا 1404/04/11: + PackagePurchasedAt (دور ۳) = 1404/04/05 → ✅ در بازه هست → امتیاز میگیره +→ تاریخ اولین فعال‌سازی: 1403/10/15 → حفظ شده ✅ +``` + +### ۷.۶ EF Migration ``` Migration: AddMagicWalletFields @@ -346,6 +426,23 @@ Migration: AddMagicWalletFields ├── ALTER TABLE UserWallets ADD MagicTotalCredited bigint NOT NULL DEFAULT 0 ├── ALTER TABLE UserWallets ADD MagicActivatedAt datetime2 NULL └── ALTER TABLE UserWallets ADD MagicCompletedAt datetime2 NULL + +Migration: AddClubMembershipCycle + ├── CREATE TABLE ClubMembershipCycles ( + │ Id bigint IDENTITY PRIMARY KEY, + │ UserId bigint NOT NULL FK → Users, + │ ClubMembershipId bigint NOT NULL FK → ClubMemberships, + │ CycleNumber int NOT NULL, + │ PackagePurchasedAt datetime2 NOT NULL, + │ MagicStartedAt datetime2 NULL, + │ MagicCompletedAt datetime2 NULL, + │ PurchaseMethod int NOT NULL, + │ PackageAmount bigint NOT NULL, + │ IsCurrentCycle bit NOT NULL DEFAULT 0, + │ + BaseAuditableEntity fields + │ ) + └── Data Migration: INSERT یک رکورد Cycle=1 برای هر ClubMembership موجود + (PackagePurchasedAt = ClubMembership.ActivatedAt) ``` --- @@ -378,12 +475,49 @@ Migration: AddMagicWalletFields ### ۸.۲ CalculateWeeklyBalancesCommandHandler (تغییر) ``` -فیلتر اضافه: +تغییر ۱ — فیلتر Magic: فقط کاربرهایی که wallet.WalletMode == Normal (کاربرهای Magic از محاسبه کمیسیون خارج میشن) + +تغییر ۲ — تاریخ از Cycle (به‌جای ActivatedAt): + قبل: + WHERE cm.ActivatedAt >= @StartDate AND cm.ActivatedAt <= @EndDate + + بعد: + WHERE cc.PackagePurchasedAt >= @StartDate + AND cc.PackagePurchasedAt <= @EndDate + AND cc.IsCurrentCycle = 1 + + (هم در C# handler و هم در SP باید تغییر کنه) ``` -### ۸.۳ CheckAndProcessDayaLoansCommandHandler (تغییر) +### ۸.۳ ActivateClubMembershipCommandHandler (تغییر مهم) + +``` +قبل (غلط — تاریخ overwrite میشه): + entity.ActivatedAt = DateTime.Now; + +بعد (درست): + // ActivatedAt فقط بار اول ست میشه: + if (entity.ActivatedAt == default) + entity.ActivatedAt = DateTime.Now; + + // هر بار یه Cycle جدید: + var previousCycle = entity.Cycles.FirstOrDefault(c => c.IsCurrentCycle); + if (previousCycle != null) + previousCycle.IsCurrentCycle = false; + + entity.Cycles.Add(new ClubMembershipCycle + { + CycleNumber = (previousCycle?.CycleNumber ?? 0) + 1, + PackagePurchasedAt = DateTime.Now, + PurchaseMethod = user.PackagePurchaseMethod, + PackageAmount = SystemConstants.BasePackageAmount, + IsCurrentCycle = true + }); +``` + +### ۸.۴ CheckAndProcessDayaLoansCommandHandler (تغییر) ``` Validation اضافه: @@ -391,7 +525,7 @@ Validation اضافه: → reject: "وام دایا فقط برای خرید اولین پکیج" ``` -### ۸.۴ InitiateMagicChargeCommandHandler (جدید) +### ۸.۵ InitiateMagicChargeCommandHandler (جدید) ``` Input: UserId, Amount @@ -405,7 +539,7 @@ Action: Return: PaymentUrl ``` -### ۸.۵ VerifyMagicChargeCommandHandler (جدید) +### ۸.۶ VerifyMagicChargeCommandHandler (جدید) ``` Input: Authority, Status