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