docs(biz): v5 — قرارداد یکبار (Q19) + فیچر DIFF (Q20) + First/Last Activation (Q21-Q22) + carryover تغییر پکیج (Q23)
تغییرات بنیادی v5: - Q19: قرارداد باشگاه فقط یک بار امضا — حذف re-contract از G5, A10, T2.7 - Q20: فیچرها DIFF/تفاضل — FeatureDiffService جدید (مقایسه + اعمال اختلاف) - Q21: ClubMembership: ActivatedAt → FirstActivationDate + LastActivationDate + FirstPackageId + LastPackageId - Q22: تشخیص فعالشدگان هفته از LastActivationDate - Q23: carryover strictly per-package — تغییر پکیج = carryover قبلی شمرده نمیشود بخشهای جدید: - 5.4: قرارداد یکبار + فلوچارت خرید مجدد بدون قرارداد - 5.5: الگوریتم DIFF فیچرها + مثال عملی + کد پیشنهادی - 5.6: تشخیص فعالشدگان هفته (SQL) - ClubMembership entity v5 با ۴ فیلد جدید اصلاحات: - EXIT Magic Mode: حذف membership.IsActive=false - State diagram: re-purchase بدون قرارداد - Migration: ActivatedAt → First/LastActivationDate - Impact Analysis: 95+ تغییر (51 اصلی + 44 سایدافکت) - Timeline: ~28 روز مجموع، ~24 روز critical path
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
# 📦 سیستم مبتنی بر پکیج (Package-Based System)
|
||||
|
||||
> **وضعیت:** تاییدشده — آماده پیادهسازی
|
||||
> **تاریخ بروزرسانی:** ۶ اسفند ۱۴۰۴
|
||||
> **نسخه:** v4 (تحلیل جامع سایدافکتها — ۴۴ مورد جدید)
|
||||
> **تاثیرگذاری:** زیاد — **۹۰+** تغییر در ۶ لایه (۴۸ اصلی + ۴۴ سایدافکت)
|
||||
> **تاریخ بروزرسانی:** ۷ اسفند ۱۴۰۴
|
||||
> **نسخه:** v5 (قرارداد یکبار + فیچر DIFF + تاریخچه فعالسازی)
|
||||
> **تاثیرگذاری:** زیاد — **۹۵+** تغییر در ۶ لایه (۵۱ اصلی + ۴۴ سایدافکت)
|
||||
|
||||
---
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
| Q16 | NetworkWeeklyBalance + PackageId | ✅ **هر رکورد تعادل = per-package** — carryover هم جداگانه |
|
||||
| Q17 | گزارش پورسانت FO | ✅ **breakdown per-package** — مشتری ببیند از هر پکیج چقدر |
|
||||
| Q18 | گزارش پورسانت BO | ✅ **فیلتر بر اساس پکیج** — ادمین بر اساس پکیج فیلتر کند |
|
||||
| Q19 | قرارداد باشگاه — چند بار؟ | ✅ **فقط یک بار در طول عمر** — قرارداد فقط اولین خرید امضا میشود. خرید مجدد بدون قرارداد |
|
||||
| Q20 | فیچرها در خرید مجدد — چطور؟ | ✅ **DIFF/تفاضل** — مقایسه فیچرهای فعلی با پکیج جدید. فقط اختلاف اعمال میشود |
|
||||
| Q21 | ردیابی فعالسازی ClubMembership | ✅ **FirstActivation + LastActivation** — ۴ فیلد: `FirstActivationDate` + `FirstPackageId` + `LastActivationDate` + `LastPackageId` |
|
||||
| Q22 | تشخیص فعالشدگان هفته | ✅ **از `LastActivationDate`** — هر کسی که `LastActivationDate` در بازه هفته باشد |
|
||||
| Q23 | Carryover تعادل هفتگی | ✅ **strictly per-package** — اگر کاربر پکیج عوض کرد، carryover پکیج قبلی شمرده نمیشود |
|
||||
|
||||
---
|
||||
|
||||
@@ -93,8 +98,8 @@
|
||||
| G2 | `InitiateBasePackagePaymentCommandHandler` | `PackagePurchaseMethod != None` → fail | مسدود | ✅ اجازه بعد تکمیل چرخه |
|
||||
| G3 | `PurchasePackageCommandHandler` | `PackagePurchaseMethod != None` → throw | مسدود | ✅ اجازه بعد تکمیل چرخه |
|
||||
| G4 | `CheckAndProcessDayaLoansCommandHandler` | `hasPreviousCycle` → skip | عمدی ✅ | ❌ حفظ (دایا فقط دور اول) |
|
||||
| G5 | `AcceptClubMembershipContractCommandHandler` | `IsActive == true` → fail | مسدود | ✅ اجازه re-contract |
|
||||
| G6 | `ActivateClubMembershipCommandHandler` | `IsActive == true` → return true | short-circuit | ✅ باید چرخه جدید بسازه |
|
||||
| G5 | `AcceptClubMembershipContractCommandHandler` | `IsActive == true` → fail | مسدود | ✅ حفظ — قرارداد فقط یک بار (Q19). خرید مجدد → Skip قرارداد |
|
||||
| G6 | `ActivateClubMembershipCommandHandler` | `IsActive == true` → return true | short-circuit | ✅ باید چرخه جدید بسازه + فیچر DIFF (Q20) |
|
||||
| G7 | JWT Claim `HasPurchasedPackage` | permanent true | UI مسدود | ✅ اضافه `CanRepurchase` |
|
||||
|
||||
### ۳.۵ Root Cause — خرید مجدد کار نمیکند
|
||||
@@ -106,8 +111,11 @@ EXIT Magic Mode (UserOrderService.cs):
|
||||
✅ cycle.MagicCompletedAt = now
|
||||
|
||||
❌ MISSING: user.PackagePurchaseMethod = None ← Guards G1-G3 مسدود میمانند
|
||||
❌ MISSING: membership.IsActive = false ← Guards G5-G6 مسدود میمانند
|
||||
❌ MISSING: cycle.IsCurrentCycle = false ← آماده چرخه جدید
|
||||
❌ MISSING: JWT CanRepurchase = true ← UI دکمه خرید نشان نمیدهد
|
||||
|
||||
ℹ️ membership.IsActive حفظ میشود (قرارداد یکبار — Q19)
|
||||
ℹ️ فیچرها حفظ میشوند — در خرید مجدد DIFF اعمال میشود (Q20)
|
||||
```
|
||||
|
||||
**راهحل:** در EXIT Magic Mode، وضعیت کاربر ریست شود تا بتواند پکیج جدید بخرد.
|
||||
@@ -178,12 +186,68 @@ public class PackageFeature : BaseAuditableEntity
|
||||
|
||||
| Entity | فیلد جدید | توضیح |
|
||||
|--------|-----------|-------|
|
||||
| `ClubMembership` | `long? PackageId` + FK | آخرین پکیج خریداریشده |
|
||||
| `ClubMembership` | `DateTime FirstActivationDate` | اولین فعالسازی — فقط یک بار ست میشود (v5 — Q21) |
|
||||
| `ClubMembership` | `long FirstPackageId` + FK | اولین پکیج خریداریشده (v5 — Q21) |
|
||||
| `ClubMembership` | `DateTime LastActivationDate` | آخرین/جاری فعالسازی — هر خرید بروزرسانی میشود (v5 — Q21) |
|
||||
| `ClubMembership` | `long LastPackageId` + FK | آخرین/جاری پکیج (v5 — Q21) |
|
||||
| `ClubMembership` | حذف `ActivatedAt` | ← جایگزین با First/LastActivationDate |
|
||||
| `ClubMembershipCycle` | `long PackageId` + FK | پکیج این چرخه |
|
||||
| `WeeklyCommissionPool` | `long PackageId` + FK | Pool جداگانه هر پکیج |
|
||||
| `UserCommissionPayout` | `long PackageId` + FK | از کدام Pool |
|
||||
| **`NetworkWeeklyBalance`** | **`long PackageId` + FK** ← v3 | **هر رکورد تعادل = per-package** |
|
||||
|
||||
> **تغییر v5:** `ClubMembership.ActivatedAt` حذف شد و به دو فیلد `FirstActivationDate` و `LastActivationDate` تبدیل شد.
|
||||
> `FirstActivationDate` فقط در اولین فعالسازی ست میشود و هیچوقت تغییر نمیکند.
|
||||
> `LastActivationDate` در هر خرید مجدد بروزرسانی میشود و مبنای تشخیص "فعالشدگان این هفته" است (Q22).
|
||||
|
||||
**ClubMembership entity (v5):**
|
||||
|
||||
```csharp
|
||||
public class ClubMembership : BaseAuditableEntity
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public virtual User User { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
// === v5: First/Last Activation Tracking (Q21) ===
|
||||
/// <summary>
|
||||
/// اولین فعالسازی — فقط یک بار ست میشود، هیچوقت overwrite نمیشود
|
||||
/// </summary>
|
||||
public DateTime FirstActivationDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// اولین پکیج خریداریشده — فقط یک بار ست میشود
|
||||
/// </summary>
|
||||
public long FirstPackageId { get; set; }
|
||||
public virtual Package FirstPackage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// آخرین/جاری فعالسازی — هر خرید مجدد بروزرسانی میشود
|
||||
/// مبنای تشخیص "فعالشدگان این هفته" (Q22)
|
||||
/// </summary>
|
||||
public DateTime LastActivationDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// آخرین/جاری پکیج — هر خرید مجدد بروزرسانی میشود
|
||||
/// مبنای carryover per-package (Q23)
|
||||
/// </summary>
|
||||
public long LastPackageId { get; set; }
|
||||
public virtual Package LastPackage { get; set; }
|
||||
|
||||
// === فیلدهای فعلی (حفظ) ===
|
||||
public long InitialContribution { get; set; }
|
||||
public long GiftValue { get; set; }
|
||||
public long TotalEarned { get; set; }
|
||||
public PackagePurchaseMethod PurchaseMethod { get; set; }
|
||||
|
||||
// === Navigation ===
|
||||
public virtual ICollection<UserClubFeature>? UserClubFeatures { get; set; }
|
||||
public virtual ICollection<ClubMembershipHistory>? ClubMembershipHistories { get; set; }
|
||||
public virtual ICollection<ClubMembershipCycle>? Cycles { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Constraintهای جدید:**
|
||||
- `WeeklyCommissionPool` → Unique(`WeekDefinitionId`, `PackageId`)
|
||||
- `NetworkWeeklyBalance` → Unique(`UserId`, `WeekDefinitionId`, `PackageId`) ← v3
|
||||
@@ -242,19 +306,21 @@ stateDiagram-v2
|
||||
|
||||
NoPurchase --> PackagePurchased: خرید پکیج\n(هر پکیجی)
|
||||
|
||||
PackagePurchased --> ClubActivated: فعالسازی باشگاه\n(OTP + قرارداد)
|
||||
PackagePurchased --> ClubActivated: فعالسازی باشگاه\n(OTP + قرارداد — فقط بار اول)
|
||||
|
||||
ClubActivated --> Shopping: خرج Balance\nدر فروشگاه
|
||||
|
||||
Shopping --> MagicMode: Balance == 0
|
||||
|
||||
MagicMode --> MagicCharging: شارژ + خرج\n(تا سقف 1B)
|
||||
MagicMode --> MagicCharging: شارژ + خرج\n(تا سقف per-package)
|
||||
|
||||
MagicCharging --> MagicMode: ادامه
|
||||
|
||||
MagicMode --> CycleComplete: Balance == 0\nAND Deposit ≥ 1B
|
||||
MagicMode --> CycleComplete: Balance == 0\nAND Deposit ≥ MaxDeposit
|
||||
|
||||
CycleComplete --> NoPurchase: ریست وضعیت\nآماده خرید مجدد
|
||||
CycleComplete --> RePurchase: ریست PackagePurchaseMethod\n(membership فعال باقی — Q19)
|
||||
|
||||
RePurchase --> ClubActivated: خرید مجدد\n(بدون قرارداد + فیچر DIFF)
|
||||
```
|
||||
|
||||
### ۵.۲ ریست وضعیت بعد تکمیل چرخه (EXIT Magic Mode)
|
||||
@@ -266,17 +332,203 @@ wallet.MagicCompletedAt = DateTime.UtcNow;
|
||||
cycle.MagicCompletedAt = DateTime.UtcNow;
|
||||
|
||||
// ✅ اضافه شود:
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.None; // اجازه خرید مجدد
|
||||
membership.IsActive = false; // اجازه re-contract
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.None; // اجازه خرید مجدد (G1-G3)
|
||||
cycle.IsCurrentCycle = false; // آماده چرخه جدید
|
||||
|
||||
// ❌ membership.IsActive حفظ میشود! (قرارداد یکبار — Q19)
|
||||
// ❌ فیچرها حفظ میشوند! (DIFF در خرید بعدی اعمال میشود — Q20)
|
||||
```
|
||||
|
||||
> **تفاوت v5 با v4:** `membership.IsActive = false` حذف شد. قرارداد فقط یک بار امضا میشود و عضویت فعال باقی میماند. در خرید مجدد، فقط فیچر DIFF + شارژ wallet + Commission انجام میشود.
|
||||
|
||||
### ۵.۳ نکات مهم
|
||||
|
||||
1. **دایا فقط دور اول** — بعد از دور اول، فقط IPG مجاز
|
||||
2. **هر خرید = Commission contribution** — ActivationFee به Pool آن پکیج
|
||||
3. **PackagePurchaseMethod ریست** بعد تکمیل چرخه
|
||||
4. **فیچرها بر اساس پکیج جدید** — ممکنه متفاوت باشه
|
||||
4. **فیچرها بر اساس پکیج جدید — DIFF/تفاضل اعمال میشود (Q20)**
|
||||
|
||||
### ۵.۴ قرارداد باشگاه — فقط یک بار (v5 — Q19)
|
||||
|
||||
> ⚠️ **تغییر بنیادی v5:** قرارداد باشگاه مشتریان **فقط یک بار** در طول عمر کاربر امضا میشود.
|
||||
|
||||
```
|
||||
سناریو:
|
||||
خرید اول (پکیج پایه):
|
||||
✅ OTP + امضای قرارداد + فعالسازی باشگاه
|
||||
✅ فیچرهای پکیج پایه فعال
|
||||
✅ قرارداد ثبت شد → UserContract record
|
||||
|
||||
خرید دوم (پکیج پایه مجدد):
|
||||
✅ پرداخت + شارژ wallet + Commission
|
||||
❌ قرارداد مجدد نمیشود (Q19)
|
||||
✅ فیچرها بدون تغییر (همان پکیج)
|
||||
|
||||
خرید سوم (پکیج نقرهای):
|
||||
✅ پرداخت + شارژ wallet + Commission
|
||||
❌ قرارداد مجدد نمیشود (Q19)
|
||||
✅ فیچر DIFF: حذف فیچرهای پایهای که نقرهای ندارد + اضافه فیچرهای جدید نقرهای
|
||||
```
|
||||
|
||||
**فلوی خرید مجدد (بدون قرارداد):**
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["EXIT Magic Mode\n(چرخه قبلی تکمیل شد)"] --> B["ریست PackagePurchaseMethod=None\n+ JWT: CanRepurchase=true"]
|
||||
B --> C["کاربر پکیج جدید انتخاب میکند"]
|
||||
C --> D["پرداخت\n(IPG / دایا دور اول)"]
|
||||
D --> E["Verify Payment"]
|
||||
E --> F["شارژ Balance + DiscountBalance"]
|
||||
F --> G{"قرارداد دارد؟\n(UserContract exists?)"}
|
||||
G -->|بله — Skip| H["فیچر DIFF (Q20)"]
|
||||
G -->|خیر — اولین خرید| I["OTP + امضای قرارداد"]
|
||||
I --> H
|
||||
H --> J["بروزرسانی ClubMembership\nLastActivationDate + LastPackageId"]
|
||||
J --> K["ساخت ClubMembershipCycle جدید"]
|
||||
K --> L["ActivationFee → Pool پکیج"]
|
||||
```
|
||||
|
||||
### ۵.۵ الگوریتم DIFF فیچرها (v5 — Q20)
|
||||
|
||||
> ⚠️ **فیچرها بر اساس تفاضل (DIFF) اعمال میشوند — نه تخصیص کامل مجدد.**
|
||||
|
||||
```
|
||||
ورودی:
|
||||
currentFeatures = UserClubFeatures WHERE UserId = X AND IsActive = true
|
||||
newFeatures = PackageFeatures WHERE PackageId = newPackage.Id AND IsIncluded = true
|
||||
|
||||
الگوریتم:
|
||||
toAdd = newFeatures - currentFeatures (فیچرهای جدید که قبلاً نداشت)
|
||||
toRemove = currentFeatures - newFeatures (فیچرهای قبلی که پکیج جدید ندارد)
|
||||
toKeep = currentFeatures ∩ newFeatures (مشترک — بدون تغییر)
|
||||
|
||||
اقدامات:
|
||||
foreach feature in toAdd:
|
||||
INSERT UserClubFeature (UserId, ClubFeatureId, IsActive=true)
|
||||
Notes = "اضافه شده بابت خرید پکیج [نام پکیج]"
|
||||
|
||||
foreach feature in toRemove:
|
||||
UPDATE UserClubFeature SET IsActive = false
|
||||
Notes = "حذف شده بابت تغییر از پکیج [قبلی] به [جدید]"
|
||||
|
||||
foreach feature in toKeep:
|
||||
بدون تغییر — فیچر فعال باقی میماند
|
||||
```
|
||||
|
||||
**مثال عملی:**
|
||||
|
||||
```
|
||||
پکیج پایه فیچرها: [چاتیکا, بیمه, سفر, آموزش]
|
||||
پکیج نقرهای فیچرها: [چاتیکا, بیمه]
|
||||
|
||||
کاربر "علی" (فعلاً پکیج پایه) → خرید پکیج نقرهای:
|
||||
currentFeatures = [چاتیکا, بیمه, سفر, آموزش]
|
||||
newFeatures = [چاتیکا, بیمه]
|
||||
|
||||
toKeep = [چاتیکا, بیمه] → بدون تغییر
|
||||
toRemove = [سفر, آموزش] → IsActive = false
|
||||
toAdd = [] → هیچ فیچر جدیدی نیست
|
||||
|
||||
کاربر "سارا" (فعلاً پکیج نقرهای) → خرید پکیج پایه:
|
||||
currentFeatures = [چاتیکا, بیمه]
|
||||
newFeatures = [چاتیکا, بیمه, سفر, آموزش]
|
||||
|
||||
toKeep = [چاتیکا, بیمه] → بدون تغییر
|
||||
toRemove = [] → هیچی حذف نمیشه
|
||||
toAdd = [سفر, آموزش] → UserClubFeature جدید ساخته میشه
|
||||
```
|
||||
|
||||
**کد پیشنهادی:**
|
||||
|
||||
```csharp
|
||||
// FeatureDiffService.cs — لاجیک DIFF فیچرها
|
||||
public async Task ApplyFeatureDiffAsync(
|
||||
long userId, long clubMembershipId,
|
||||
long oldPackageId, long newPackageId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// 1. فیچرهای فعال فعلی کاربر
|
||||
var currentFeatureIds = await _context.UserClubFeatures
|
||||
.Where(ucf => ucf.UserId == userId && ucf.IsActive)
|
||||
.Select(ucf => ucf.ClubFeatureId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
// 2. فیچرهای پکیج جدید
|
||||
var newFeatureIds = await _context.PackageFeatures
|
||||
.Where(pf => pf.PackageId == newPackageId && pf.IsIncluded)
|
||||
.Select(pf => pf.ClubFeatureId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
// 3. DIFF
|
||||
var toAdd = newFeatureIds.Except(currentFeatureIds).ToList();
|
||||
var toRemove = currentFeatureIds.Except(newFeatureIds).ToList();
|
||||
|
||||
// 4. حذف فیچرهای قدیمی
|
||||
if (toRemove.Any())
|
||||
{
|
||||
var featuresToDeactivate = await _context.UserClubFeatures
|
||||
.Where(ucf => ucf.UserId == userId
|
||||
&& ucf.IsActive
|
||||
&& toRemove.Contains(ucf.ClubFeatureId))
|
||||
.ToListAsync(ct);
|
||||
|
||||
foreach (var f in featuresToDeactivate)
|
||||
{
|
||||
f.IsActive = false;
|
||||
f.Notes = $"حذف شده بابت تغییر پکیج (PackageId: {oldPackageId} → {newPackageId})";
|
||||
}
|
||||
}
|
||||
|
||||
// 5. اضافه فیچرهای جدید
|
||||
if (toAdd.Any())
|
||||
{
|
||||
var newFeatures = toAdd.Select(featureId => new UserClubFeature
|
||||
{
|
||||
UserId = userId,
|
||||
ClubMembershipId = clubMembershipId,
|
||||
ClubFeatureId = featureId,
|
||||
GrantedAt = DateTime.Now,
|
||||
IsActive = true,
|
||||
Notes = $"اضافه شده بابت خرید پکیج (PackageId: {newPackageId})"
|
||||
}).ToList();
|
||||
|
||||
_context.UserClubFeatures.AddRange(newFeatures);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Feature DIFF applied for UserId={UserId}: Added={Added}, Removed={Removed}, Kept={Kept}",
|
||||
userId, toAdd.Count, toRemove.Count,
|
||||
currentFeatureIds.Count - toRemove.Count);
|
||||
}
|
||||
```
|
||||
|
||||
> **نکته مهم:** در اولین خرید (`currentFeatures` خالی)، تمام فیچرهای پکیج اضافه میشوند.
|
||||
> در خرید مجدد **همان پکیج**، DIFF خالی است و هیچ تغییری در فیچرها نمیشود.
|
||||
|
||||
### ۵.۶ تشخیص فعالشدگان هفته (v5 — Q22)
|
||||
|
||||
```sql
|
||||
-- کاربرانی که در هفته جاری فعال/خرید مجدد کردهاند:
|
||||
SELECT cm."UserId", cm."LastPackageId", cm."LastActivationDate",
|
||||
p."Title" AS PackageTitle
|
||||
FROM "CMS"."ClubMemberships" cm
|
||||
JOIN "CMS"."Packages" p ON p."Id" = cm."LastPackageId"
|
||||
WHERE cm."IsActive" = true
|
||||
AND cm."LastActivationDate" >= @WeekStartDate
|
||||
AND cm."LastActivationDate" < @WeekEndDate;
|
||||
|
||||
-- اولین فعالسازی (عمر کاربر):
|
||||
SELECT cm."FirstActivationDate", cm."FirstPackageId"
|
||||
FROM "CMS"."ClubMemberships" cm
|
||||
WHERE cm."UserId" = @UserId;
|
||||
|
||||
-- تعداد خرید (از تاریخچه):
|
||||
SELECT COUNT(*) AS TotalPurchases
|
||||
FROM "CMS"."ClubMembershipCycles" c
|
||||
WHERE c."UserId" = @UserId;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -307,16 +559,21 @@ Commission:
|
||||
Reza → پاداش از Pool_پایه
|
||||
```
|
||||
|
||||
### ۶.۳ فلوی کامل چرخه خرید مجدد و تاثیر بر پورسانت (v3)
|
||||
### ۶.۳ فلوی کامل چرخه خرید مجدد و تاثیر بر پورسانت (v5)
|
||||
|
||||
```
|
||||
چرخه ۱ — کاربر "علی" پکیج پایه میخرد (۵۶M):
|
||||
────────────────────────────────────────────────
|
||||
چرخه ۱ — کاربر "علی" پکیج پایه میخرد (۵۶M) — اولین خرید:
|
||||
────────────────────────────────────────────────────────────────
|
||||
✅ Balance += 56M
|
||||
✅ DiscountBalance += 112M (×2)
|
||||
✅ ActivationFee → Pool_پایه هفته جاری
|
||||
✅ فیچرهای پکیج پایه فعال
|
||||
✅ NetworkWeeklyBalance ساخته میشود (PackageId=پایه)
|
||||
✅ OTP + امضای قرارداد (فقط این بار — Q19)
|
||||
✅ فیچرهای پکیج پایه فعال (DIFF = همه اضافه — لیست خالی بود)
|
||||
✅ ClubMembership:
|
||||
FirstActivationDate = now, FirstPackageId = پایه
|
||||
LastActivationDate = now, LastPackageId = پایه
|
||||
✅ ClubMembershipCycle #1 ساخته میشود
|
||||
✅ NetworkWeeklyBalance (PackageId=پایه, WeekId=هفته جاری)
|
||||
✅ بالاسریها: تعادلها بر اساس MaxBalancesPerLeg=300 + MaxNetworkLevel=15
|
||||
✅ پورسانت بالاسریها از Pool_پایه
|
||||
|
||||
@@ -325,11 +582,16 @@ Commission:
|
||||
|
||||
چرخه ۲ — کاربر "علی" دوباره پکیج پایه میخرد:
|
||||
──────────────────────────────────────────────
|
||||
✅ ریست وضعیت: PackagePurchaseMethod=None, membership.IsActive=false
|
||||
✅ ریست وضعیت: PackagePurchaseMethod=None (membership فعال باقی — Q19)
|
||||
✅ Balance += 56M (مجدد شارژ)
|
||||
✅ DiscountBalance += 112M (مجدد شارژ)
|
||||
✅ ActivationFee → Pool_پایه هفته جاری
|
||||
✅ فیچرهای پکیج پایه فعال (مجدد)
|
||||
❌ بدون قرارداد مجدد (Q19 — قبلاً امضا شده)
|
||||
✅ فیچر DIFF = خالی (همان پکیج → بدون تغییر فیچر — Q20)
|
||||
✅ ClubMembership:
|
||||
FirstActivationDate = حفظ, FirstPackageId = حفظ
|
||||
LastActivationDate = now (بروزرسانی), LastPackageId = پایه
|
||||
✅ ClubMembershipCycle #2 ساخته میشود
|
||||
✅ NetworkWeeklyBalance جدید (PackageId=پایه, WeekId=هفته جاری)
|
||||
✅ carryover از هفته قبل: فقط carryover پکیج پایه (نه نقرهای!)
|
||||
✅ بالاسریها: محاسبه مجدد از Pool_پایه
|
||||
@@ -341,18 +603,30 @@ Commission:
|
||||
✅ Balance += 5.6M
|
||||
✅ DiscountBalance += 11.2M (×2)
|
||||
✅ ActivationFee → Pool_نقرهای هفته جاری (۲,۵۲۰,۰۰۰)
|
||||
✅ فیچرهای پکیج نقرهای فعال (ممکنه کمتر از پایه باشه!)
|
||||
❌ بدون قرارداد مجدد (Q19)
|
||||
✅ فیچر DIFF (Q20):
|
||||
فعلی: [چاتیکا, بیمه, سفر, آموزش] (از پکیج پایه)
|
||||
جدید: [چاتیکا, بیمه] (از پکیج نقرهای)
|
||||
→ حذف: [سفر, آموزش] → IsActive=false
|
||||
→ اضافه: [] → هیچی
|
||||
→ حفظ: [چاتیکا, بیمه]
|
||||
✅ ClubMembership:
|
||||
FirstActivationDate = حفظ, FirstPackageId = حفظ (پایه)
|
||||
LastActivationDate = now (بروزرسانی), LastPackageId = نقرهای
|
||||
✅ ClubMembershipCycle #5 ساخته میشود (PackageId=نقرهای)
|
||||
✅ NetworkWeeklyBalance جدید (PackageId=نقرهای, WeekId=هفته جاری)
|
||||
✅ تعادلها: MaxBalancesPerLeg=30 (نه 300!) + MaxNetworkLevel=15 (از پکیج)
|
||||
✅ carryover: فقط carryover نقرهای (جداگانه از پایه)
|
||||
✅ carryover: فقط carryover نقرهای (Q23 — carryover پکیج پایه شمرده نمیشود!)
|
||||
✅ بالاسریها: محاسبه از Pool_نقرهای → ValuePerBalance کمتر
|
||||
✅ پاداش بالاسری: ~۲,۵۲۰,۰۰۰ ÷ TotalBalances_نقرهای × BalancesEarned
|
||||
```
|
||||
|
||||
### ۶.۴ تعادلها (NetworkWeeklyBalance) — per-package (v3)
|
||||
### ۶.۴ تعادلها (NetworkWeeklyBalance) — per-package (v5)
|
||||
|
||||
> ⚠️ **تغییر اساسی:** هر کاربر **بهازای هر پکیج فعال** یک رکورد تعادل جداگانه دارد.
|
||||
|
||||
> 🔴 **قانون carryover v5 (Q23):** اگر کاربر **پکیج عوض کرد**، carryover پکیج قبلی **شمرده نمیشود!**
|
||||
|
||||
```
|
||||
قبل (تکپکیج):
|
||||
NetworkWeeklyBalance: [UserId, WeekId] → یک رکورد
|
||||
@@ -365,9 +639,15 @@ Commission:
|
||||
|
||||
```
|
||||
برای هر پکیج فعال:
|
||||
1. واکشی کاربرانی که این پکیج را دارند (PackageId = X)
|
||||
2. carryover از هفته قبل: فقط رکوردهای PackageId = X
|
||||
3. اعضای جدید: فقط کسانی که PackageId = X خریدند + JoinedAt در بازه هفته
|
||||
1. واکشی کاربرانی که این پکیج را دارند:
|
||||
ClubMembership.LastPackageId = X (v5 — Q21)
|
||||
2. carryover از هفته قبل:
|
||||
فقط رکوردهای PackageId = X
|
||||
✅ مهم: فقط اگر پکیج فعلی کاربر = X (Q23)
|
||||
❌ اگر کاربر هفته قبل پکیج Y داشت و حالا X دارد → carryover Y شمرده نمیشود!
|
||||
3. اعضای جدید این هفته:
|
||||
ClubMembership.LastActivationDate در بازه هفته
|
||||
AND ClubMembership.LastPackageId = X (v5 — Q22)
|
||||
4. LeftLegTotal = NewLeft + CarryoverLeft
|
||||
5. RightLegTotal = NewRight + CarryoverRight
|
||||
6. TotalBalances = MIN(Left, Right) → cap at Package.MaxBalancesPerLeg
|
||||
@@ -375,6 +655,22 @@ Commission:
|
||||
8. SubordinateBalances: مجموع TotalBalances زیرمجموعه (تا Package.MaxNetworkLevel)
|
||||
```
|
||||
|
||||
**مثال carryover با تغییر پکیج (v5 — Q23):**
|
||||
|
||||
```
|
||||
هفته ۹:
|
||||
علی: پکیج پایه → تعادل_پایه: Left=45, Right=30
|
||||
carryover_پایه: {Left: 15, Right: 0} ← باقیمانده
|
||||
|
||||
هفته ۱۰:
|
||||
علی: پکیج پایه → EXIT Magic → خرید پکیج نقرهای
|
||||
|
||||
❌ carryover_پایه {Left:15, Right:0} شمرده نمیشود!
|
||||
(پکیج فعلی = نقرهای ≠ پایه)
|
||||
✅ carryover_نقرهای: {Left:0, Right:0} (تازه شروع)
|
||||
✅ تعادل_نقرهای: Left=NewLeft+0, Right=NewRight+0
|
||||
```
|
||||
|
||||
**مثال عملی:**
|
||||
|
||||
```
|
||||
@@ -584,7 +880,7 @@ flowchart LR
|
||||
| `PackageCreated` | ساخت WeeklyCommissionPool خالی هفته جاری |
|
||||
| `PackageDeactivated` | هشدار ادمین — Pool موجود تکمیل شود |
|
||||
| `PackagePurchased` | ActivationFee → Pool پکیج + شارژ wallets |
|
||||
| `MagicCycleCompleted` | ریست PackagePurchaseMethod + Deactivate membership |
|
||||
| `MagicCycleCompleted` | ریست PackagePurchaseMethod + حفظ membership فعال (Q19) |
|
||||
|
||||
---
|
||||
|
||||
@@ -689,25 +985,32 @@ claims.Add("CanRepurchase", HasCompletedMagicCycle(membership).ToString());
|
||||
|
||||
---
|
||||
|
||||
### ۷٫۵٫۵ Club Features — تخصیص global
|
||||
### ۷٫۵٫۵ Club Features — تخصیص global → DIFF (v5)
|
||||
|
||||
> 🔴 **بحرانی — همه فیچرها به همه کاربران**
|
||||
> 🔴 **بحرانی — فیچرها باید DIFF/تفاضل باشند (Q20)**
|
||||
|
||||
```csharp
|
||||
// ActivateClubMembershipCommandHandler.cs + AcceptClubMembershipContractCommandHandler.cs:
|
||||
// فعلی (اشتباه) — ActivateClubMembershipCommandHandler.cs + AcceptClubMembershipContractCommandHandler.cs:
|
||||
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
|
||||
// ↑ همیشه همه فیچرها (Chatika, Bime, Trip, Learn) — صرفنظر از پکیج!
|
||||
```
|
||||
|
||||
**مشکل:** پکیج نقرهای ممکنه فقط ۲ فیچر داشته باشه ولی سیستم فعلی **همه ۴ فیچر** رو فعال میکنه.
|
||||
**مشکل ۱:** پکیج نقرهای ممکنه فقط ۲ فیچر داشته باشه ولی سیستم فعلی **همه ۴ فیچر** رو فعال میکنه.
|
||||
**مشکل ۲:** در خرید مجدد، باید فقط **تفاضل** اعمال بشه (Q20) — نه تخصیص مجدد همه.
|
||||
|
||||
**اقدام:**
|
||||
**اقدام (v5):**
|
||||
```csharp
|
||||
// باید بشه:
|
||||
var featureIds = await _context.PackageFeatures
|
||||
.Where(pf => pf.PackageId == package.Id && pf.IsIncluded)
|
||||
.Select(pf => pf.ClubFeatureId)
|
||||
.ToListAsync();
|
||||
// خرید اول (currentFeatures خالی → همه فیچرها اضافه):
|
||||
await _featureDiffService.ApplyFeatureDiffAsync(
|
||||
userId, membershipId,
|
||||
oldPackageId: 0, // بدون پکیج قبلی
|
||||
newPackageId: package.Id);
|
||||
|
||||
// خرید مجدد (مقایسه + تفاضل):
|
||||
await _featureDiffService.ApplyFeatureDiffAsync(
|
||||
userId, membershipId,
|
||||
oldPackageId: membership.LastPackageId,
|
||||
newPackageId: newPackage.Id);
|
||||
```
|
||||
|
||||
---
|
||||
@@ -873,9 +1176,14 @@ BEGIN
|
||||
SELECT "Id" INTO base_pkg_id
|
||||
FROM "CMS"."Packages" WHERE "IsBasePackage" = true LIMIT 1;
|
||||
|
||||
-- STEP 2: ClubMembership
|
||||
-- STEP 2: ClubMembership — v5 migration (Q21)
|
||||
-- ActivatedAt → FirstActivationDate + LastActivationDate
|
||||
UPDATE "CMS"."ClubMemberships"
|
||||
SET "PackageId" = base_pkg_id WHERE "PackageId" IS NULL;
|
||||
SET "FirstActivationDate" = "ActivatedAt",
|
||||
"LastActivationDate" = "ActivatedAt",
|
||||
"FirstPackageId" = base_pkg_id,
|
||||
"LastPackageId" = base_pkg_id
|
||||
WHERE "FirstActivationDate" IS NULL;
|
||||
|
||||
-- STEP 3: ClubMembershipCycle
|
||||
UPDATE "CMS"."ClubMembershipCycles"
|
||||
@@ -897,7 +1205,7 @@ BEGIN
|
||||
END $$;
|
||||
|
||||
-- STEP 7: Verify — همه باید 0 باشند
|
||||
SELECT 'ClubMemberships' AS tbl, COUNT(*) FROM "CMS"."ClubMemberships" WHERE "PackageId" IS NULL
|
||||
SELECT 'ClubMemberships' AS tbl, COUNT(*) FROM "CMS"."ClubMemberships" WHERE "LastPackageId" IS NULL
|
||||
UNION ALL
|
||||
SELECT 'Cycles', COUNT(*) FROM "CMS"."ClubMembershipCycles" WHERE "PackageId" IS NULL
|
||||
UNION ALL
|
||||
@@ -910,9 +1218,9 @@ SELECT 'Balances', COUNT(*) FROM "CMS"."NetworkWeeklyBalances" WHERE "PackageId"
|
||||
|
||||
---
|
||||
|
||||
## ۱۱. Impact Analysis — ۹۰+ تغییر در ۶ لایه (v4)
|
||||
## ۱۱. Impact Analysis — ۹۵+ تغییر در ۶ لایه (v5)
|
||||
|
||||
> ۴۸ تغییر اصلی (بخش ۱۰) + ۴۴ سایدافکت (بخش ۷.۵) = **۹۲ تغییر کل**
|
||||
> ۵۱ تغییر اصلی (بخش ۱۰) + ۴۴ سایدافکت (بخش ۷.۵) = **۹۵ تغییر کل**
|
||||
|
||||
### ۱۰.۱ لایه Domain (۱۰ تغییر)
|
||||
|
||||
@@ -920,7 +1228,7 @@ SELECT 'Balances', COUNT(*) FROM "CMS"."NetworkWeeklyBalances" WHERE "PackageId"
|
||||
|---|------|-----|------|-----|
|
||||
| D1 | `Package.cs` | اضافه **۱۱ فیلد** جدید (v2: ۷ + v3: ۴) | 🟡 | 🔄 |
|
||||
| D2 | `PackageFeature.cs` | Entity جدید + EF Config | 🔴 | |
|
||||
| D3 | `ClubMembership.cs` | اضافه `PackageId` | 🟡 | |
|
||||
| D3 | `ClubMembership.cs` | حذف `ActivatedAt` → اضافه **۴ فیلد**: `FirstActivationDate`, `FirstPackageId`, `LastActivationDate`, `LastPackageId` (v5 — Q21) | 🔴 | 🔄 |
|
||||
| D4 | `ClubMembershipCycle.cs` | اضافه `PackageId` | 🟡 | |
|
||||
| D5 | `WeeklyCommissionPool.cs` | اضافه `PackageId` + Unique | 🔴 | |
|
||||
| D6 | `UserCommissionPayout.cs` | اضافه `PackageId` + Unique(UserId,WeekId,PackageId) | 🟡 | 🔄 |
|
||||
@@ -943,17 +1251,19 @@ SELECT 'Balances', COUNT(*) FROM "CMS"."NetworkWeeklyBalances" WHERE "PackageId"
|
||||
| A7 | `PurchasePackageCommandHandler` | اجازه re-purchase | 🟡 |
|
||||
| A8 | `CreateManualPaymentCommandHandler` | DiscountMultiplier از Package | 🟡 |
|
||||
| A9 | `CheckAndProcessDayaLoansCommandHandler` | حذف ID=4 + DiscountMultiplier | 🟡 |
|
||||
| A10 | `AcceptClubMembershipContractCommandHandler` | اجازه re-contract بعد چرخه | 🟡 |
|
||||
| A10 | `AcceptClubMembershipContractCommandHandler` | حفظ guard (قرارداد یکبار — Q19). خرید مجدد Skip قرارداد | 🟡 |
|
||||
| A11 | `UserOrderService` (EXIT Magic) | ریست PackagePurchaseMethod + Deactivate | 🔴 |
|
||||
| A12 | Package CRUD handlers | فیلدهای جدید + PackageFeature CRUD | 🟡 |
|
||||
| **A13** | **`ChargeMagicWalletCommandHandler`** | **`MagicWalletMaxDeposit` از Package بخوند (نه global)** | **🔴 v4** |
|
||||
| **A14** | **`VerifyMagicWalletChargeCommandHandler`** | **`MagicWalletMultiplier` از Package بخوند (نه ×2.5 global)** | **🔴 v4** |
|
||||
| **A15** | **`UserOrderService.cs` EXIT/ENTRY** | **EXIT: سقف از Package + ENTRY: چک کدام پکیج** | **🔴 v4** |
|
||||
| **A16** | **Validators (۳ فایل)** | **`1_000_000_000` hardcoded → داینامیک per-package** | **🟡 v4** |
|
||||
| **A17** | **`GetAllFeatureIds()` (۲ handler)** | **همه فیچرها global → per-package از PackageFeature** | **🔴 v4** |
|
||||
| **A17** | **`GetAllFeatureIds()` (۲ handler)** | **فیچر DIFF/تفاضل (Q20): مقایسه فعلی vs جدید، فقط اختلاف اعمال** | **🔴 v5** |
|
||||
| **A18** | **JWT Token Generation** | **اضافه PackageId + PackageTitle + CanRepurchase** | **🟡 v4** |
|
||||
| **A19** | **`WalletGrpcService.GetMagicWalletStatus`** | **سقف و باقیمانده per-package (نه global)** | **🟡 v4** |
|
||||
| **A20** | **Notifications (۴ مورد)** | **اضافه PackageId + PackageTitle به interface** | **🟡 v4** |
|
||||
| **A21** | **`FeatureDiffService` — جدید** | **سرویس DIFF فیچرها (Q20): مقایسه + اعمال تفاضل** | **🔴 v5** |
|
||||
| **A22** | **`ActivateClubMembership` — FirstLast** | **بروزرسانی First/LastActivationDate + First/LastPackageId (Q21)** | **🟡 v5** |
|
||||
|
||||
### ۱۰.۳ لایه Infrastructure (۶ تغییر)
|
||||
|
||||
@@ -1029,7 +1339,7 @@ SELECT 'Balances', COUNT(*) FROM "CMS"."NetworkWeeklyBalances" WHERE "PackageId"
|
||||
|-----|----- |-----|
|
||||
| **T1.1** | بروزرسانی `Package` entity (**۱۱ فیلد** جدید: v2 ۷ + v3 ۴ شامل MaxBalancesPerLeg, MaxNetworkLevel, MagicWalletMaxDeposit, MagicWalletMaxCredit) | 🔄 |
|
||||
| **T1.2** | ایجاد `PackageFeature` entity + EF Config | |
|
||||
| **T1.3** | اضافه `PackageId` به `ClubMembership` | |
|
||||
| **T1.3** | حذف `ActivatedAt` → اضافه **۴ فیلد**: `FirstActivationDate`, `FirstPackageId`, `LastActivationDate`, `LastPackageId` (Q21) | 🔄 v5 |
|
||||
| **T1.4** | اضافه `PackageId` به `ClubMembershipCycle` | |
|
||||
| **T1.5** | اضافه `PackageId` به `WeeklyCommissionPool` + Unique(WeekId,PackageId) | |
|
||||
| **T1.6** | اضافه `PackageId` به `UserCommissionPayout` + Unique(UserId,WeekId,PackageId) | 🔄 |
|
||||
@@ -1047,11 +1357,11 @@ SELECT 'Balances', COUNT(*) FROM "CMS"."NetworkWeeklyBalances" WHERE "PackageId"
|
||||
|-----|----- |-----|
|
||||
| **T2.1** | ادغام Verify handlers → Generic (DiscountMultiplier + UserPackagePurchase) | |
|
||||
| **T2.2** | ادغام Purchase handlers → Generic (حذف "طلایی"، حذف ID=4) | |
|
||||
| **T2.3** | بروزرسانی `ActivateClubMembership` — فیچر از PackageFeature | |
|
||||
| **T2.3** | بروزرسانی `ActivateClubMembership` — فیچر DIFF از PackageFeature (Q20) + First/LastActivation (Q21) | 🔄 v5 |
|
||||
| **T2.4** | بروزرسانی `ActivateClubMembership` — ActivationFee از Package | |
|
||||
| **T2.5** | اجازه re-purchase در Guards (G1–G3) | |
|
||||
| **T2.6** | ریست وضعیت در EXIT Magic Mode | |
|
||||
| **T2.7** | اجازه re-contract (G5) + بروزرسانی JWT (G7) | |
|
||||
| **T2.6** | ریست وضعیت در EXIT Magic Mode (بدون `IsActive=false` — Q19) | |
|
||||
| **T2.7** | حفظ guard قرارداد (Q19) + ایجاد `FeatureDiffService` (Q20) + بروزرسانی JWT (G7) | 🔄 v5 |
|
||||
| **T2.8** | بروزرسانی `CreateManualPayment` + `DayaLoan` | |
|
||||
| **T2.9** | PackageFeature CRUD | |
|
||||
| **T2.10** | Event: PackageCreated → ساخت Pool خالی | |
|
||||
@@ -1061,6 +1371,8 @@ SELECT 'Balances', COUNT(*) FROM "CMS"."NetworkWeeklyBalances" WHERE "PackageId"
|
||||
| **T2.14** | **`WalletGrpcService.GetMagicWalletStatus`: سقف per-package** | **🆕 v4** |
|
||||
| **T2.15** | **Notifications (۴ مورد): اضافه PackageId به interface + پیام** | **🆕 v4** |
|
||||
| **T2.16** | **بروزرسانی ۲۱ محل ساخت WalletChangeLog با PackageId** | **🆕 v4** |
|
||||
| **T2.17** | **ایجاد `FeatureDiffService` — سرویس DIFF فیچرها (Q20) + استفاده در Activate + AcceptContract** | **🆕 v5** |
|
||||
| **T2.18** | **بروزرسانی فلوی خرید مجدد — Skip قرارداد (Q19) + فقط FeatureDiff + شارژ wallet** | **🆕 v5** |
|
||||
|
||||
### فاز ۳ — محاسبه پورسانت ≈ ۴ روز (موازی با فاز ۲)
|
||||
|
||||
@@ -1112,11 +1424,15 @@ SELECT 'Balances', COUNT(*) FROM "CMS"."NetworkWeeklyBalances" WHERE "PackageId"
|
||||
| **T5.9** | **تست ضریب جادویی: پکیج A ×2.5 vs پکیج B ×2.0 — اعتبار صحیح** | **🆕 v4** |
|
||||
| **T5.10** | **تست قرارداد حقوقی: مبلغ و نام پکیج صحیح در متن** | **🆕 v4** |
|
||||
| **T5.11** | **تست WalletChangeLog: رکوردها PackageId دارند** | **🆕 v4** |
|
||||
| **T5.12** | Deploy staging → production |
|
||||
| **T5.12** | **تست قرارداد یکبار: خرید مجدد بدون OTP/قرارداد (Q19)** | **🆕 v5** |
|
||||
| **T5.13** | **تست فیچر DIFF: تغییر پکیج → فیچرهای صحیح فعال/غیرفعال (Q20)** | **🆕 v5** |
|
||||
| **T5.14** | **تست First/Last: FirstActivationDate حفظ + LastActivationDate بروزرسانی (Q21)** | **🆕 v5** |
|
||||
| **T5.15** | **تست carryover تغییر پکیج: carryover قبلی شمرده نمیشود (Q23)** | **🆕 v5** |
|
||||
| **T5.16** | Deploy staging → production |
|
||||
|
||||
---
|
||||
|
||||
## ۱۲. ریسکها (v4)
|
||||
## ۱۲. ریسکها (v5)
|
||||
|
||||
| ریسک | احتمال | شدت | راهحل |
|
||||
|------|--------|-----|--------|
|
||||
@@ -1128,21 +1444,24 @@ SELECT 'Balances', COUNT(*) FROM "CMS"."NetworkWeeklyBalances" WHERE "PackageId"
|
||||
| **Magic Wallet EXIT اشتباه — کاربر گیر میافته** | **زیاد** | **بحرانی** | **اولویت P0 — سقف از Package خوانده شود** |
|
||||
| **قرارداد حقوقی با مبلغ اشتباه** | **زیاد** | **بحرانی** | **متن قرارداد داینامیک از Package** |
|
||||
| **۲۱ WalletChangeLog بدون ردیابی** | **قطعی** | **متوسط** | **اضافه PackageId به entity** |
|
||||
| **فیچر DIFF — حذف فیچر فعال (v5)** | **متوسط** | **بالا** | **کاربر اگر فیچر فعالی حذف بشه، باید اطلاعرسانی بشه** |
|
||||
| **Migration ActivatedAt → First/Last (v5)** | **کم** | **بحرانی** | **هر دو فیلد = ActivatedAt فعلی، بعداً Last بروزرسانی** |
|
||||
| **ناسازگاری AcceptContract vs Activate (v5)** | **زیاد** | **بالا** | **AcceptContract: overwrite ActivatedAt / Activate: حفظ — باید یکسان شوند** |
|
||||
|
||||
---
|
||||
|
||||
## ۱۳. تخمین زمانی (v4)
|
||||
## ۱۳. تخمین زمانی (v5)
|
||||
|
||||
| فاز | مدت | وابستگی | v4 تغییر |
|
||||
| فاز | مدت | وابستگی | v5 تغییر |
|
||||
|-----|------|---------|----------|
|
||||
| فاز ۰ — فیکس باگها | ۱ روز | — | |
|
||||
| فاز ۱ — زیرساخت | **۵ روز** | فاز ۰ | +۱ (WalletChangeLog PackageId + ۲۱ محل) |
|
||||
| فاز ۲ — منطق | **۵ روز** | فاز ۱ | +۱ (Magic Wallet + Validators + JWT + Notifications) |
|
||||
| فاز ۳ — پورسانت | ۴ روز | فاز ۱ | |
|
||||
| فاز ۴ — UI | **۷ روز** | فاز ۲ | +۲ (Magic Wallet UI + قرارداد + ManualActivation + SystemConfig) |
|
||||
| فاز ۵ — تست | **۴ روز** | فاز ۳, ۴ | +۱ (تست Magic per-package + قرارداد + WalletLog) |
|
||||
| **مجموع** | **~۲۶ روز** | | |
|
||||
| فاز ۱ — زیرساخت | **۵ روز** | فاز ۰ | ClubMembership: ۴ فیلد First/Last (Q21) + Migration ActivatedAt |
|
||||
| فاز ۲ — منطق | **۶ روز** | فاز ۱ | +۱ (FeatureDiffService + Skip قرارداد + First/Last Activation) |
|
||||
| فاز ۳ — پورسانت | ۴ روز | فاز ۱ | LastActivationDate برای تشخیص هفتگی (Q22) |
|
||||
| فاز ۴ — UI | **۷ روز** | فاز ۲ | |
|
||||
| فاز ۵ — تست | **۵ روز** | فاز ۳, ۴ | +۱ (تست قرارداد یکبار + فیچر DIFF + First/Last + carryover تغییر پکیج) |
|
||||
| **مجموع** | **~۲۸ روز** | | |
|
||||
|
||||
> فاز ۲ و ۳ **موازی** → مسیر بحرانی: ۰→۱→۲→۴→۵ = **~۲۲ روز**
|
||||
> نسبت به v3 (**۱۷ روز**): **+۵ روز** بخاطر ۴۴ سایدافکت کشفشده
|
||||
> نسبت به v2 (**۱۴ روز**): **+۸ روز** — بزرگترین سهم: Magic Wallet + WalletChangeLog + قرارداد حقوقی
|
||||
> فاز ۲ و ۳ **موازی** → مسیر بحرانی: ۰→۱→۲→۴→۵ = **~۲۴ روز**
|
||||
> نسبت به v4 (**۲۲ روز**): **+۲ روز** بخاطر FeatureDiffService + قرارداد یکبار + First/Last Activation
|
||||
> نسبت به v3 (**۱۷ روز**): **+۷ روز** — بزرگترین سهم: Magic Wallet + WalletChangeLog + فیچر DIFF + First/Last
|
||||
|
||||
Reference in New Issue
Block a user