docs: add ClubMembershipCycle table to preserve activation date
Problem: ActivateClubMembership overwrites ActivatedAt on re-purchase, losing the original club activation date. Commission check uses ActivatedAt to determine 'new member this week'. Solution: New ClubMembershipCycle table - Each package purchase creates a new cycle record - ClubMembership.ActivatedAt = first-time only (never overwritten) - Commission uses Cycle.PackagePurchasedAt instead of ActivatedAt - IsCurrentCycle flag tracks active cycle - Full history preserved for all purchase cycles Also updated: - SPEC: section 7.5 (entity), 7.6 (migration), 8.2 (SP change), 8.3 (handler) - PLAN: phase 1 (entity), phase 2 (activate handler), phase 4 (date query) - Checklist: added 4 new items
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user