docs(biz): v6 fix — carryover is per-DOWNLINE-package (not per-user-package), user's own package change has NO effect on carryover

This commit is contained in:
masoodafar-web
2026-02-27 04:01:25 +03:30
parent f8908d8e2b
commit 085583c274
2 changed files with 121 additions and 89 deletions
+119 -87
View File
@@ -42,14 +42,14 @@
| Q20 | فیچرها در خرید مجدد — چطور؟ | ✅ **DIFF/تفاضل** — مقایسه فیچرهای فعلی با پکیج جدید. فقط اختلاف اعمال می‌شود | | Q20 | فیچرها در خرید مجدد — چطور؟ | ✅ **DIFF/تفاضل** — مقایسه فیچرهای فعلی با پکیج جدید. فقط اختلاف اعمال می‌شود |
| Q21 | ردیابی فعال‌سازی ClubMembership | ✅ **FirstActivation + LastActivation** — ۴ فیلد: `FirstActivationDate` + `FirstPackageId` + `LastActivationDate` + `LastPackageId` | | Q21 | ردیابی فعال‌سازی ClubMembership | ✅ **FirstActivation + LastActivation** — ۴ فیلد: `FirstActivationDate` + `FirstPackageId` + `LastActivationDate` + `LastPackageId` |
| Q22 | تشخیص فعال‌شدگان هفته | ✅ **از `LastActivationDate`** — هر کسی که `LastActivationDate` در بازه هفته باشد | | Q22 | تشخیص فعال‌شدگان هفته | ✅ **از `LastActivationDate`** — هر کسی که `LastActivationDate` در بازه هفته باشد |
| Q23 | Carryover تعادل هفتگی | ✅ **strictly per-package**اگر کاربر پکیج عوض کرد، carryover پکیج قبلی شمرده نمی‌شود | | Q23 | Carryover تعادل هفتگی | ✅ **per-downline-package**تعادل بر اساس پکیج **زیرمجموعه‌ها** (نه پکیج خود کاربر). هر کاربر N رکورد تعادل دارد. تغییر پکیج خود کاربر تاثیری بر carryover ندارد |
| Q24 | آستانه موجودی برای ورود Magic و خرید مجدد | ✅ **کمتر از ۱۰۰,۰۰۰ تومان** — چون قیمت محصولات متفاوته، `Balance == 0` عملاً غیرممکنه. آستانه ثابت ۱,۰۰۰,۰۰۰ ریال | | Q24 | آستانه موجودی برای ورود Magic و خرید مجدد | ✅ **کمتر از ۱۰۰,۰۰۰ تومان** — چون قیمت محصولات متفاوته، `Balance == 0` عملاً غیرممکنه. آستانه ثابت ۱,۰۰۰,۰۰۰ ریال |
| Q25 | DayaLoans محدودیت پکیج | ✅ **فقط پکیج پایه**`SupportsDayaPurchase` فقط روی پکیج پایه `true` هست. تغییر نمی‌کنه | | Q25 | DayaLoans محدودیت پکیج | ✅ **فقط پکیج پایه**`SupportsDayaPurchase` فقط روی پکیج پایه `true` هست. تغییر نمی‌کنه |
| Q26 | مدیریت Stored Procedures | ✅ **SP Worker (IHostedService)** — در startup، فایل‌های `.sql` از embedded resource خوانده و با checksum مقایسه و اعمال می‌شوند | | Q26 | مدیریت Stored Procedures | ✅ **SP Worker (IHostedService)** — در startup، فایل‌های `.sql` از embedded resource خوانده و با checksum مقایسه و اعمال می‌شوند |
| Q27 | History Tables یکسان‌سازی | ✅ **نام‌گذاری مشابه master** + ثبت خودکار تغییرات در EF interceptor/domain events | | Q27 | History Tables یکسان‌سازی | ✅ **نام‌گذاری مشابه master** + ثبت خودکار تغییرات در EF interceptor/domain events |
| Q28 | UI Guidance (آموزش/هشدار) | ✅ **مودال + متن inline** — در سراسر FO/BO توضیحات آموزشی و هشداری برای سیستم پکیج‌بیس | | Q28 | UI Guidance (آموزش/هشدار) | ✅ **مودال + متن inline** — در سراسر FO/BO توضیحات آموزشی و هشداری برای سیستم پکیج‌بیس |
| Q29 | شرط EXIT Magic | ✅ **آخرین پکیج فعال** — از `ClubMembershipCycle.PackageId` (چرخه فعلی) → `Package.MagicWalletMaxDeposit` | | Q29 | شرط EXIT Magic | ✅ **آخرین پکیج فعال** — از `ClubMembershipCycle.PackageId` (چرخه فعلی) → `Package.MagicWalletMaxDeposit` |
| Q30 | Carryover توضیح | ✅ **باقی‌مانده تعادل هفتگی**پای قوی‌تر surplus نگه می‌داره. تغییر پکیج = ریست implicit (lookup پکیج جدید → ۰) | | Q30 | Carryover توضیح | ✅ **باقی‌مانده تعادل هفتگی**بر اساس پکیج **زیرمجموعه‌ها**. تغییر پکیج خود کاربر carryover را ریست **نمی‌کند**. هر Pool مجزا |
--- ---
@@ -561,10 +561,13 @@ WHERE c."UserId" = @UserId;
/ \ / \
[Sara - نقره‌ای] [Reza - پایه] [Sara - نقره‌ای] [Reza - پایه]
Commission: Commission (بالاسری Ali):
Ali → پاداش از Pool_پایه Ali → از Pool_پایه (چون Reza پکیج پایه داره)
Sara → پاداش از Pool_نقره‌ای Ali → از Pool_نقره‌ای (چون Sara پکیج نقره‌ای داره)
Reza → پاداش از Pool_پایه → مجموع هر دو Pool → کیف پول شبکه Ali
→ به تفکیک: X از پایه، Y از نقره‌ای
⚠️ پکیج خود Ali مهم نیست! مهم پکیج زیرمجموعه‌هاست
``` ```
### ۶.۳ فلوی کامل چرخه خرید مجدد و تاثیر بر پورسانت (v5) ### ۶.۳ فلوی کامل چرخه خرید مجدد و تاثیر بر پورسانت (v5)
@@ -600,9 +603,8 @@ Commission:
FirstActivationDate = حفظ, FirstPackageId = حفظ FirstActivationDate = حفظ, FirstPackageId = حفظ
LastActivationDate = now (بروزرسانی), LastPackageId = پایه LastActivationDate = now (بروزرسانی), LastPackageId = پایه
✅ ClubMembershipCycle #2 ساخته می‌شود ✅ ClubMembershipCycle #2 ساخته می‌شود
NetworkWeeklyBalance جدید (PackageId=پایه, WeekId=هفته جاری) بالاسری علی: carryover همه Poolها (بر اساس پکیج زیرمجموعه‌ها) حفظ می‌شود
carryover از هفته قبل: فقط carryover پکیج پایه (نه نقره‌ای!) پورسانت بالاسری: از هر Pool که زیرمجموعه‌ای دارد
✅ بالاسری‌ها: محاسبه مجدد از Pool_پایه
... همان چرخه Magic Wallet تکرار ... ... همان چرخه Magic Wallet تکرار ...
@@ -622,83 +624,114 @@ Commission:
FirstActivationDate = حفظ, FirstPackageId = حفظ (پایه) FirstActivationDate = حفظ, FirstPackageId = حفظ (پایه)
LastActivationDate = now (بروزرسانی), LastPackageId = نقره‌ای LastActivationDate = now (بروزرسانی), LastPackageId = نقره‌ای
✅ ClubMembershipCycle #5 ساخته می‌شود (PackageId=نقره‌ای) ✅ ClubMembershipCycle #5 ساخته می‌شود (PackageId=نقره‌ای)
NetworkWeeklyBalance جدید (PackageId=نقره‌ای, WeekId=هفته جاری) بالاسری‌های علی: carryover همه Poolها حفظ — تغییر پکیج علی تاثیری ندارد!
✅ تعادل‌ها: MaxBalancesPerLeg=30 (نه 300!) + MaxNetworkLevel=15 (از پکیج) (carryover بر اساس پکیج زیرمجموعه‌هاست نه خود علی)
carryover: فقط carryover نقره‌ای (Q23 — carryover پکیج پایه شمرده نمی‌شود!) پورسانت بالاسری از هر Pool جداگانه:
✅ بالاسری‌ها: محاسبه از Pool_نقره‌ای → ValuePerBalance کمتر Pool_پایه: بر اساس زیرمجموعههایی که پکیج پایه دارند
✅ پاداش بالاسری: ~۲,۵۲۰,۰۰۰ ÷ TotalBalances_نقره‌ای × BalancesEarned Pool_نقره‌ای: بر اساس زیرمجموعه‌هایی که پکیج نقره‌ای دارند
✅ ActivationFee علی → Pool_نقره‌ای (۲,۵۲۰,۰۰۰)
``` ```
### ۶.۴ تعادل‌ها (NetworkWeeklyBalance) — per-package (v5) ### ۶.۴ تعادل‌ها (NetworkWeeklyBalance) — per-downline-package (v6)
> ⚠️ **تغییر اساسی:** هر کاربر **به‌ازای هر پکیج فعال** یک رکورد تعادل جداگانه دارد. > ⚠️ **تغییر اساسی v6:** تعادل هر کاربر بر اساس پکیج **زیرمجموعه‌ها** گروه‌بندی می‌شود — **نه** پکیج خود کاربر.
> 🔴 **قانون carryover v5 (Q23):** اگر کاربر **پکیج عوض کرد**، carryover پکیج قبلی **شمرده نمی‌شود!** > **قانون carryover v6 (Q23 اصلاح‌شده):** تغییر پکیج خود کاربر **هیچ تاثیری** بر carryover ندارد. carryover مال زیرمجموعه‌هاست.
``` ```
قبل (تک‌پکیج): قبل (تک‌پکیج):
NetworkWeeklyBalance: [UserId, WeekId] → یک رکورد NetworkWeeklyBalance: [UserId, WeekId] → یک رکورد
بعد (چند‌پکیج): بعد (چند‌پکیج):
NetworkWeeklyBalance: [UserId, WeekId, PackageId] → N رکورد (N = تعداد پکیج) NetworkWeeklyBalance: [UserId, WeekId, PackageId] → N رکورد
(N = تعداد پکیج‌های مختلف زیرمجموعه‌ها)
``` ```
**الگوریتم محاسبه تعادل per-package:** **الگوریتم محاسبه تعادل per-downline-package:**
``` ```
برای هر پکیج فعال: برای هر بالاسری (کاربر):
1. واکشی کاربرانی که این پکیج را دارند: برای هر پکیج فعال (X):
ClubMembership.LastPackageId = X (v5 — Q21) 1. واکشی زیرمجموعه‌هایی که پکیج X دارند:
2. carryover از هفته قبل: زیرمجموعه‌ها WHERE ClubMembership.LastPackageId = X
فقط رکوردهای PackageId = X 2. شمارش: چند نفر تیم چپ + چند نفر تیم راست
✅ مهم: فقط اگر پکیج فعلی کاربر = X (Q23) 3. carryover از هفته قبل:
❌ اگر کاربر هفته قبل پکیج Y داشت و حالا X دارد → carryover Y شمرده نمی‌شود! خواندن رکورد [UserId, PrevWeekId, PackageId=X]
3. اعضای جدید این هفته: ✅ همیشه خوانده می‌شود — مستقل از پکیج خود کاربر!
ClubMembership.LastActivationDate در بازه هفته
AND ClubMembership.LastPackageId = X (v5 — Q22)
4. LeftLegTotal = NewLeft + CarryoverLeft 4. LeftLegTotal = NewLeft + CarryoverLeft
5. RightLegTotal = NewRight + CarryoverRight 5. RightLegTotal = NewRight + CarryoverRight
6. TotalBalances = MIN(Left, Right) → cap at Package.MaxBalancesPerLeg 6. TotalBalances = MIN(Left, Right) → cap at Package.MaxBalancesPerLeg
7. Remainder → carryover هفته بعد (فقط برای PackageId = X) 7. Remainder → carryover هفته بعد (برای PackageId = X)
8. SubordinateBalances: مجموع TotalBalances زیرمجموعه (تا Package.MaxNetworkLevel) 8. پورسانت: TotalBalances × ValuePerBalance_X → کیف پول شبکه
```
**مثال carryover با تغییر پکیج (v5 — Q23):**
```
هفته ۹:
علی: پکیج پایه → تعادل_پایه: Left=45, Right=30
carryover_پایه: {Left: 15, Right: 0} ← باقیمانده
هفته ۱۰:
علی: پکیج پایه → EXIT Magic → خرید پکیج نقره‌ای
❌ carryover_پایه {Left:15, Right:0} شمرده نمی‌شود! مجموع پورسانت از همه Poolها → واریز به کیف پول شبکه کاربر
(پکیج فعلی = نقره‌ای ≠ پایه) (به تفکیک مشخص: هر مبلغ از کدام Pool)
✅ carryover_نقره‌ای: {Left:0, Right:0} (تازه شروع)
✅ تعادل_نقره‌ای: Left=NewLeft+0, Right=NewRight+0
``` ```
**مثال عملی:** **مثال جامع — بالاسری "علی":**
``` ```
هفته ۱۰: علی (خودش پکیج پایه داره)
علی (پکیج پایه): ├── تیم چپ:
تعادل_پایه: Left=45, Right=52, Min=45 (cap 300) → OK │ ├── سارا (پکیج پایه)
تعادل_نقره‌ای: Left=0, Right=0 (علی پکیج نقره‌ای نداره) ├── رضا (پکیج پایه)
│ └── مریم (پکیج نقره‌ای)
سارا (پکیج نقره‌ای): └── تیم راست:
تعادل_پایه: Left=0, Right=0 ├── حسین (پکیج پایه)
تعادل_نقره‌ای: Left=12, Right=8, Min=8 (cap 30) → OK └── زهرا (پکیج نقره‌ای)
```
رضا (پکیج پایه + قبلاً نقره‌ای داشته):
تعادل_پایه: Left=30, Right=25, Min=25 (cap 300) → OK ```
تعادل_نقره‌ای: Left=2 (carryover), Right=0 (carryover) → Min=0 هفته ۱۰ — محاسبه تعادل علی:
═══════════════════════════════════════════════════════
هفته ۱۱ (Shift):
علی: carryover_پایه = {Left: surplus_left, Right: surplus_right} ═══ Pool پکیج پایه (زیرمجموعه‌هایی که پایه دارن) ═══
سارا: carryover_نقره‌ای = {Left: surplus_left, Right: surplus_right} چپ: سارا + رضا = 2
رضا: carryover_پایه = {...}, carryover_نقره‌ای = {Left:2, Right:0} راست: حسین = 1
carryover هفته ۹: {چپ: 3, راست: 0}
جمع: چپ = 2+3 = 5, راست = 1+0 = 1
تعادل = MIN(5, 1) = 1 (cap 300 → OK)
carryover → هفته ۱۱: {چپ: 4, راست: 0}
پورسانت: 1 × ValuePerBalance_پایه = A ریال
═══ Pool پکیج نقره‌ای (زیرمجموعه‌هایی که نقره‌ای دارن) ═══
چپ: مریم = 1
راست: زهرا = 1
carryover هفته ۹: {چپ: 0, راست: 0}
جمع: چپ = 1+0 = 1, راست = 1+0 = 1
تعادل = MIN(1, 1) = 1 (cap 30 → OK)
carryover → هفته ۱۱: {چپ: 0, راست: 0}
پورسانت: 1 × ValuePerBalance_نقره‌ای = B ریال
═══ مجموع پورسانت علی هفته ۱۰: ═══
کیف پول شبکه += (A + B)
ردیابی: A از Pool پایه، B از Pool نقره‌ای
```
**هفته ۱۱ — علی پکیج خودش رو عوض می‌کنه (پایه → نقره‌ای):**
```
═══ Pool پکیج پایه ═══
carryover از هفته ۱۰: {چپ: 4, راست: 0} ← هنوز هست!
✅ تغییر پکیج خود علی تاثیری نداره!
اعضای جدید: چپ = 0, راست = 1
جمع: چپ = 0+4 = 4, راست = 1+0 = 1
تعادل = MIN(4, 1) = 1
✅ علی هنوز از Pool پایه سود می‌بره (چون زیرمجموعه‌هایی با پکیج پایه داره)
═══ Pool پکیج نقره‌ای ═══
carryover از هفته ۱۰: {چپ: 0, راست: 0}
... محاسبه عادی ...
```
**نکته کلیدی:** پکیج **خود کاربر** فقط تعیین می‌کنه ActivationFee‌اش به کدوم Pool بره.
**تعادل و carryover** بر اساس پکیج **زیرمجموعه‌ها** محاسبه می‌شه.
**Cap (سقف):**
```
اگه تعادل بیشتر از MaxBalancesPerLeg بشه → بریده می‌شه (flush):
تعادل = 500، سقف = 300 → CappedBalance = 300، Flushed = 200
⚠️ Flushed از بین می‌ره — carry نمی‌شه!
``` ```
### ۶.۵ تغییرات SP (v3 — بروزرسانی) ### ۶.۵ تغییرات SP (v3 — بروزرسانی)
@@ -1713,40 +1746,39 @@ public abstract class BaseHistoryEntity : BaseEntity
--- ---
### Q30 — Carryover — توضیح ساده ### Q30 — Carryover — توضیح ساده (v6 اصلاح‌شده)
> **Carryover = باقی‌مانده تعادل از هفته قبل** > **Carryover = باقی‌مانده تعادل از هفته قبل — بر اساس پکیج زیرمجموعه‌ها**
**مثال ساده:** **مثال ساده:**
``` ```
علی بالاسری — زیرمجموعه‌هاش پکیج پایه دارن:
هفته ۱۰: هفته ۱۰:
علی: تیم چپ = ۵۰ نفر فعال، تیم راست = ۳۰ نفر فعال تیم چپ (پکیج پایه) = ۵۰ نفر، تیم راست (پکیج پایه) = ۳۰ نفر
تعادل = MIN(50, 30) = 30 تعادل_پایه = MIN(50, 30) = 30
باقی‌مانده: چپ = 50-30 = 20 ← CARRYOVER راست = 0 باقی‌مانده_پایه: {چپ: 20, راست: 0} ← CARRYOVER
هفته ۱۱: هفته ۱۱:
اعضای جدید: چپ = 5، راست = 10 اعضای جدید (پکیج پایه): چپ = 5، راست = 10
جمع با carryover: چپ = 5+20 = 25، راست = 10+0 = 10 جمع با carryover: چپ = 5+20 = 25، راست = 10+0 = 10
تعادل = MIN(25, 10) = 10 تعادل_پایه = MIN(25, 10) = 10
باقی‌مانده: چپ = 25-10 = 15 ← CARRYOVER بعدی باقی‌مانده_پایه: {چپ: 15, راست: 0} ← CARRYOVER بعدی
``` ```
**Per-package carryover (Q23):** **تغییر پکیج خود کاربر — تاثیری ندارد (Q23 اصلاحی):**
``` ```
علی پکیج پایه داره: علی پکیج خودش رو عوض می‌کنه (پایه → نقره‌ای):
carryover_پایه = {چپ: 20, راست: 0}
علی پکیج رو عوض می‌کنه → نقره‌ای: carryover_پایه = {چپ: 20, راست: 0} ← هنوز هست! حذف نمی‌شه!
carryover_پایه دیگه شمرده نمی‌شه! carryover_نقره‌ای = {چپ: 0, راست: 0}
✅ carryover_نقره‌ای = {چپ: 0, راست: 0} ← شروع از صفر
(چون سیستم هفته بعد دنبال carryover با PackageId=نقره‌ای ✅ هفته بعد، علی هنوز از Pool پایه سود می‌بره
می‌گرده و پیدا نمی‌کنه → default صفر) (چون زیرمجموعه‌هایی با پکیج پایه داره)
``` ✅ همزمان از Pool نقره‌ای هم سود می‌بره
(چون زیرمجموعه‌هایی با پکیج نقره‌ای هم داره)
**Cap (سقف):**
``` پکیج خود علی فقط تعیین می‌کنه:
اگه تعادل بیشتر از MaxBalancesPerLeg بشه → بریده می‌شه (flush): → ActivationFee‌اش به کدوم Pool بره
تعادل = 500، سقف = 300 → CappedBalance = 300، Flushed = 200 → MagicWallet سقفش چقدره
⚠️ Flushed از بین می‌ره — carry نمی‌شه!
``` ```
+2 -2
View File
@@ -85,7 +85,7 @@
| تعادل هفتگی | ۱ رکورد per user/week | **N رکورد** per user/week/package | | تعادل هفتگی | ۱ رکورد per user/week | **N رکورد** per user/week/package |
| MaxBalancesPerLeg | hardcoded `300` | per-package (پایه=۳۰۰, نقره‌ای=۳۰) | | MaxBalancesPerLeg | hardcoded `300` | per-package (پایه=۳۰۰, نقره‌ای=۳۰) |
| MaxNetworkLevel | hardcoded `15` | per-package از دیتابیس | | MaxNetworkLevel | hardcoded `15` | per-package از دیتابیس |
| Carryover | یک‌پارچه | **per-package**تغییر پکیج = ریست carryover (Q23) | | Carryover | یک‌پارچه | **per-downline-package**بر اساس پکیج زیرمجموعه‌ها (تغییر پکیج خود کاربر تاثیری ندارد) |
| Stored Procedure | پارامترهای ثابت | پارامترهای داینامیک از Package entity | | Stored Procedure | پارامترهای ثابت | پارامترهای داینامیک از Package entity |
| گزارش مشتری | بدون تفکیک | **breakdown per-package** | | گزارش مشتری | بدون تفکیک | **breakdown per-package** |
| گزارش ادمین | بدون فیلتر | **فیلتر بر اساس پکیج** | | گزارش ادمین | بدون فیلتر | **فیلتر بر اساس پکیج** |
@@ -572,7 +572,7 @@ kubectl set image deployment/backoffice bo=bo:rollback-point
|---|------|-------------------| |---|------|-------------------|
| 10 | Pool جداگانه هر پکیج | WeeklyCommissionPool با PackageId متفاوت | | 10 | Pool جداگانه هر پکیج | WeeklyCommissionPool با PackageId متفاوت |
| 11 | MaxBalancesPerLeg متفاوت | پایه=۳۰۰, نقره‌ای=۳۰ | | 11 | MaxBalancesPerLeg متفاوت | پایه=۳۰۰, نقره‌ای=۳۰ |
| 12 | Carryover per-package | تغییر پکیج → ریست carryover | | 12 | Carryover per-downline-package | تغییر پکیج خود کاربر → carryover حفظ (بر اساس زیرمجموعه‌ها) |
| 13 | SP پارامترها از Package | بدون hardcoded ۳۰۰/۱۵ | | 13 | SP پارامترها از Package | بدون hardcoded ۳۰۰/۱۵ |
### 📊 گزارشات per-package ### 📊 گزارشات per-package