docs(biz): v6 — Q24-Q30: balance threshold, SP worker, history tables, UI guidance, DayaLoans+EXIT+carryover confirmations

This commit is contained in:
masoodafar-web
2026-02-27 03:42:47 +03:30
parent ba10b6485b
commit f8908d8e2b
2 changed files with 308 additions and 5 deletions
+286 -2
View File
@@ -1,8 +1,8 @@
# 📦 سیستم مبتنی بر پکیج (Package-Based System)
> **وضعیت:** در حال پیاده‌سازی — **فاز ۰-۴ تکمیل ✅** | فاز ۵-۶ در انتظار
> **تاریخ بروزرسانی:** ۷ اسفند ۱۴۰۴
> **نسخه:** v5 (قرارداد یک‌بار + فیچر DIFF + تاریخچه فعال‌سازی)
> **تاریخ بروزرسانی:** ۸ اسفند ۱۴۰۴
> **نسخه:** v6 (آستانه موجودی + SP Worker + History Tables + UI Guidance)
> **تاثیرگذاری:** زیاد — **۹۵+** تغییر در ۶ لایه (۵۱ اصلی + ۴۴ سایدافکت)
> **کامیت‌ها:** `8b9c317` (Phase 0) → `ae92ab8` (Phase 1) → `a9cd2fd` (Phase 1.5) → `8e5c7c5` (Phase 2) → `ccb938e` (Phase 3) → `0002a5a` (Phase 4)
@@ -43,6 +43,13 @@
| Q21 | ردیابی فعال‌سازی ClubMembership | ✅ **FirstActivation + LastActivation** — ۴ فیلد: `FirstActivationDate` + `FirstPackageId` + `LastActivationDate` + `LastPackageId` |
| Q22 | تشخیص فعال‌شدگان هفته | ✅ **از `LastActivationDate`** — هر کسی که `LastActivationDate` در بازه هفته باشد |
| Q23 | Carryover تعادل هفتگی | ✅ **strictly per-package** — اگر کاربر پکیج عوض کرد، 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 پکیج جدید → ۰) |
---
@@ -1466,3 +1473,280 @@ SELECT 'Balances', COUNT(*) FROM "CMS"."NetworkWeeklyBalances" WHERE "PackageId"
> فاز ۲ و ۳ **موازی** → مسیر بحرانی: ۰→۱→۲→۴→۵ = **~۲۴ روز**
> نسبت به v4 (**۲۲ روز**): **+۲ روز** بخاطر FeatureDiffService + قرارداد یک‌بار + First/Last Activation
> نسبت به v3 (**۱۷ روز**): **+۷ روز** — بزرگ‌ترین سهم: Magic Wallet + WalletChangeLog + فیچر DIFF + First/Last
---
## ۱۴. تصمیمات جدید v6 (Q24–Q30)
> **تاریخ:** ۸ اسفند ۱۴۰۴
> **زمینه:** مرور نهایی قبل از تست و استقرار — ۷ نکته جدید شناسایی شد
### Q24 — آستانه موجودی برای ورود به Magic و خرید مجدد 🔴
> **مشکل:** شرط فعلی `wallet.Balance == 0` عملاً غیرممکنه — چون قیمت محصولات متفاوته (مثلاً ۳۵۰,۰۰۰ تومان، ۱,۲۰۰,۰۰۰ تومان، ...) کاربر هیچ‌وقت نمی‌تونه دقیقاً به صفر برسه.
**تصمیم:** آستانه ثابت **۱,۰۰۰,۰۰۰ ریال (۱۰۰,۰۰۰ تومان)** — هم برای ورود به Magic، هم برای تشخیص تکمیل چرخه.
**مکان‌های تاثیر:**
| # | فایل | شرط فعلی | شرط جدید |
|---|------|----------|----------|
| 1 | `UserOrderService.cs` L342 | `if (wallet.Balance == 0)` — ورود به Magic | `if (wallet.Balance <= MagicWalletEntryThreshold)` |
| 2 | `UserOrderService.cs` L342 | `if (wallet.Balance == 0)` — EXIT Magic check | `if (wallet.Balance <= MagicWalletEntryThreshold)` |
| 3 | خرید مجدد guard | `PackagePurchaseMethod == None` | + `Balance <= Threshold` |
**تعریف ثابت:**
```csharp
// SystemConstants.cs یا Package entity
public const long MagicWalletEntryThreshold = 1_000_000; // 100,000 تومان = 1,000,000 ریال
```
**فلوی جدید:**
```
کاربر خرید می‌کند → Balance = 850,000 ریال (کمتر از 1M)
→ سیستم: Balance <= 1,000,000 ✅ → ورود به Magic Mode
→ قبلاً: Balance == 0 ❌ → کاربر گیر می‌افتاد!
Magic تکمیل → Balance = 200,000 ریال
→ سیستم: Balance <= 1,000,000 ✅ → EXIT Magic → مجاز به خرید مجدد
→ قبلاً: Balance == 0 ❌ → کاربر تا ابد در Magic!
```
> ⚠️ **نکته:** این آستانه **ثابت (global)** تعریف می‌شه — نه per-package. چون مربوط به قیمت محصولات فروشگاهه، نه پکیج.
---
### Q25 — DayaLoans فقط پکیج پایه
> **تایید:** وام دایا فعلاً فقط برای پکیج پایه فعاله و تغییر نمی‌کنه.
**وضعیت فعلی:**
- فیلد `Package.SupportsDayaPurchase` روی پکیج پایه = `true`، نقره‌ای = `false`
- `CheckAndProcessDayaLoansCommandHandler` فقط پکیج‌هایی که `SupportsDayaPurchase = true` دارن رو بررسی می‌کنه
- این یک تصمیم بیزینسی ثابته — اگر در آینده تغییر کرد، ادمین از BackOffice فلگ رو تغییر می‌ده
---
### Q26 — SP Worker (مدیریت خودکار Stored Procedures) 🔴
> **مشکل:** الان SPها **کاملاً دستی** deploy می‌شن. اگه SP تغییر کنه، باید یادمون باشه دستی اجرا کنیم.
**تصمیم:** یک `IHostedService` به نام `StoredProcedureDeploymentService` ایجاد بشه.
**الگوریتم:**
```
1. Startup → خواندن فایل‌های .sql از Embedded Resources
2. برای هر فایل:
a. محاسبه SHA256 checksum محتوا
b. خواندن checksum قبلی از جدول "CMS"."StoredProcedureVersions"
c. اگر وجود نداشت یا checksum فرق داشت:
→ اجرای CREATE OR ALTER PROCEDURE
→ ذخیره checksum جدید
→ لاگ: "SP {name} deployed/updated"
d. اگر checksum برابر بود:
→ لاگ: "SP {name} is up-to-date, skipping"
3. اجرا بعد از EF Migration (ترتیب startup)
```
**جدول جدید:**
```sql
CREATE TABLE "CMS"."StoredProcedureVersions" (
"Id" BIGSERIAL PRIMARY KEY,
"Name" VARCHAR(256) NOT NULL UNIQUE, -- نام SP
"Checksum" VARCHAR(64) NOT NULL, -- SHA256 hash
"DeployedAt" TIMESTAMP NOT NULL, -- آخرین deploy
"Version" INT NOT NULL DEFAULT 1 -- شمارنده نسخه
);
```
**فایل‌های SP فعلی (embed شوند):**
| # | فایل | خطوط |
|---|------|------|
| 1 | `sp_CalculateWeeklyBalances.sql` | ۳۷۲ |
| 2 | `sp_CalculateWeeklyCommissionPool.sql` | ۲۶۷ |
**csproj تغییرات:**
```xml
<ItemGroup>
<EmbeddedResource Include="Persistence\StoredProcedures\*.sql" />
</ItemGroup>
```
---
### Q27 — History Tables — یکسان‌سازی و ثبت خودکار 🟡
> **مشکل:** نام‌گذاری history tableها ناسازگاره و ثبت تاریخچه دستیه.
**وضعیت فعلی:**
| Master Entity | History Entity | نام‌گذاری |
|---------------|---------------|----------|
| `UserCommissionPayout` | `CommissionPayoutStatusHistory` | ❌ نام متفاوت |
| `CommissionCashWithdrawal` | `CommissionCashWithdrawalStatusHistory` | ✅ مشابه |
| `DiscountOrder` | `DiscountOrderStatusHistory` | ✅ مشابه |
| `UserWallet` | `UserWalletChangeLog` | ❌ نام متفاوت |
| `Package` | ❌ ندارد | 🔴 |
| `ClubMembership` | ❌ ندارد | 🔴 |
| `ClubMembershipCycle` | ❌ ندارد | 🔴 |
**تصمیم — ۲ بخش:**
**بخش ۱: نام‌گذاری یکسان**
```
الگو: {MasterEntityName}History
مثال:
UserCommissionPayout → UserCommissionPayoutHistory (rename از CommissionPayoutStatusHistory)
UserWallet → UserWalletHistory (rename از UserWalletChangeLog)
Package → PackageHistory (جدید)
ClubMembership → ClubMembershipHistory (جدید)
ClubMembershipCycle → ClubMembershipCycleHistory (جدید)
```
**بخش ۲: ثبت خودکار تغییرات**
```
دو رویکرد:
A) EF Interceptor (پیشنهادی):
→ در SaveChangesInterceptor، entity‌های تغییریافته شناسایی
→ برای هر entity که IHasHistory پیاده کرده:
snapshot فعلی → رکورد History جدید
→ مزیت: یکجا و خودکار
B) Domain Events:
→ هر entity یک EntityChangedEvent publish کنه
→ Handler مربوطه History ثبت کنه
→ مزیت: async + decoupled
```
**Interface پیشنهادی:**
```csharp
public interface IHasHistory<THistory> where THistory : BaseHistoryEntity
{
THistory CreateHistorySnapshot(string action, long? changedBy);
}
public abstract class BaseHistoryEntity : BaseEntity
{
public long MasterEntityId { get; set; } // FK به entity اصلی
public string Action { get; set; } // Created, Updated, StatusChanged, ...
public long? ChangedBy { get; set; } // UserId تغییردهنده
public DateTime ChangedAt { get; set; } // زمان تغییر
public string? Snapshot { get; set; } // JSON snapshot (اختیاری)
}
```
---
### Q28 — UI Guidance (آموزش و هشدار در FO/BO) 🟡
> **تصمیم:** در سراسر FrontOffice و BackOffice، متن‌های آموزشی و هشداری اضافه بشه.
**انواع Guidance:**
| نوع | جایگاه | مثال |
|-----|--------|------|
| **مودال آموزشی** | اولین بار باز کردن صفحه | «سیستم پکیج‌بیس: شما می‌توانید بعد از تکمیل چرخه، پکیج جدید بخرید» |
| **متن inline** | کنار فیلد/آیتم | «💡 هزینه فعال‌سازی از قیمت پکیج محاسبه می‌شود» |
| **هشدار** | قبل از اقدام بحرانی | «⚠️ با تغییر پکیج، فیچرهای قبلی ممکنه غیرفعال بشن» |
| **Tooltip** | روی آیکون اطلاعات | «این مبلغ بر اساس پکیج فعال شما محاسبه شده» |
**صفحات هدف FrontOffice:**
| # | صفحه | نوع | محتوا |
|---|-------|-----|-------|
| G1 | صفحه پکیج‌ها | مودال (اولین بار) | توضیح سیستم پکیج‌بیس، تفاوت پکیج‌ها، نحوه خرید |
| G2 | Checkout | متن inline | «این پکیج {features} را شامل می‌شود» |
| G3 | MyPackages | متن inline | «بعد تکمیل چرخه جادویی، می‌توانید پکیج جدید بخرید» |
| G4 | Magic Wallet | هشدار مودال | «سقف واریز و ضریب بر اساس پکیج شماست» |
| G5 | Commission Dashboard | tooltip | «پاداش بر اساس هر پکیج جداگانه محاسبه می‌شود» |
| G6 | قرارداد باشگاه | متن inline | «این قرارداد فقط یک‌بار امضا می‌شود» |
| G7 | ActivationSection | متن inline | «هزینه تخمینی بر اساس پکیج {name} محاسبه شده» |
**صفحات هدف BackOffice:**
| # | صفحه | نوع | محتوا |
|---|-------|-----|-------|
| G8 | Package CRUD | متن inline | «تغییر قیمت روی کاربران فعلی اثر ندارد — فقط خریدهای جدید» |
| G9 | Feature Matrix | tooltip | «فیچرهای تیک‌خورده برای خریداران این پکیج فعال می‌شود» |
| G10 | Manual Payment | هشدار | «مبلغ پرداخت باید مطابق قیمت پکیج انتخابی باشد» |
| G11 | Commission Reports | متن inline | «هر پکیج Pool پورسانت مستقل دارد» |
| G12 | User Detail | متن inline | «پکیج فعلی: {name} — چرخه: {n}» |
| G13 | Club Members | tooltip | «فیلتر بر اساس آخرین پکیج خریداری‌شده» |
**پیاده‌سازی فنی:**
```razor
@* FrontOffice — کامپوننت Guidance قابل استفاده مجدد *@
<PackageGuidance
Page="Packages"
Type="Modal"
ShowOnce="true"
Title="سیستم پکیج‌بیس"
Content="@_guidanceContent" />
@* BackOffice — متن inline *@
<MudAlert Severity="Severity.Info" Dense="true" Class="mb-2">
💡 تغییر قیمت روی کاربران فعلی اثر ندارد — فقط خریدهای جدید
</MudAlert>
```
---
### Q29 — شرط EXIT Magic — تایید: آخرین پکیج فعال
> **تایید:** شرط EXIT از `ClubMembershipCycle` فعلی (`IsCurrentCycle=true`) → `PackageId` → `Package.MagicWalletMaxDeposit` خوانده می‌شه.
**فلوی دقیق:**
```
1. UserOrderService.ProcessOrderAsync → wallet.Balance کم می‌شه
2. if (Balance <= Threshold): ← Q24 آستانه جدید
a. اگر Normal + خرید کرده + باشگاه فعال → ENTER Magic
b. اگر Magic:
→ خواندن چرخه فعلی → PackageId → Package.MagicWalletMaxDeposit
→ if (MagicTotalDeposited >= MaxDeposit) → EXIT Magic
→ else: هنوز Magic (می‌تونه شارژ کنه)
```
> **Fallback:** اگه چرخه فعالی نبود → `IsBasePackage = true` (پکیج پایه)
---
### Q30 — Carryover — توضیح ساده
> **Carryover = باقی‌مانده تعادل از هفته قبل**
**مثال ساده:**
```
هفته ۱۰:
علی: تیم چپ = ۵۰ نفر فعال، تیم راست = ۳۰ نفر فعال
تعادل = MIN(50, 30) = 30
باقی‌مانده: چپ = 50-30 = 20 ← CARRYOVER راست = 0
هفته ۱۱:
اعضای جدید: چپ = 5، راست = 10
جمع با carryover: چپ = 5+20 = 25، راست = 10+0 = 10
تعادل = MIN(25, 10) = 10
باقی‌مانده: چپ = 25-10 = 15 ← CARRYOVER بعدی
```
**Per-package carryover (Q23):**
```
علی پکیج پایه داره:
carryover_پایه = {چپ: 20, راست: 0}
علی پکیج رو عوض می‌کنه → نقره‌ای:
❌ carryover_پایه دیگه شمرده نمی‌شه!
✅ carryover_نقره‌ای = {چپ: 0, راست: 0} ← شروع از صفر
(چون سیستم هفته بعد دنبال carryover با PackageId=نقره‌ای
می‌گرده و پیدا نمی‌کنه → default صفر)
```
**Cap (سقف):**
```
اگه تعادل بیشتر از MaxBalancesPerLeg بشه → بریده می‌شه (flush):
تعادل = 500، سقف = 300 → CappedBalance = 300، Flushed = 200
⚠️ Flushed از بین می‌ره — carry نمی‌شه!
```
+22 -3
View File
@@ -629,12 +629,19 @@ kubectl set image deployment/backoffice bo=bo:rollback-point
| F5 | SystemConfiguration per-package | 🟡 | تنظیمات global vs per-package |
| F6 | MagicWalletChargePage hardcoded | 🟡 | ×۲.۵ و سقف در ۶ جا FO |
| F7 | Validators async per-package | 🟢 | سقف 1B → مقدار واقعی |
| F8 | آستانه موجودی ورود به Magic (Q24) | 🔴 | `Balance == 0``Balance <= 1M ریال` |
| F9 | SP Worker — مدیریت خودکار SP (Q26) | 🔴 | IHostedService + checksum + auto-deploy |
| F10 | History Tables — یکسان‌سازی + خودکار (Q27) | 🟡 | نام‌گذاری + IHasHistory + interceptor |
| F11 | UI Guidance — آموزش و هشدار (Q28) | 🟡 | مودال + متن inline در FO/BO |
> **این موارد ریسک عملیاتی ندارند** و می‌توانند در فاز بعدی انجام شوند. سیستم فعلی با fallback مناسب کار می‌کند.
> **F8 و F9 ریسک عملیاتی دارند** و باید قبل از production پیاده شوند.
> **F10 و F11** می‌توانند در فاز بعدی انجام شوند. سایر موارد (F1-F7) با fallback فعلی مشکلی ندارند.
---
## ضمیمه: ۲۳ تصمیم بیزینسی پیاده‌شده
## ضمیمه: ۳۰ تصمیم بیزینسی (Q1Q30)
### پیاده‌شده (Q1Q23):
| # | تصمیم | وضعیت |
|---|-------|-------|
@@ -662,6 +669,18 @@ kubectl set image deployment/backoffice bo=bo:rollback-point
| Q22 | تشخیص هفته از LastActivationDate | ✅ `607f791` |
| Q23 | Carryover strictly per-package | ✅ `607f791` |
### تصمیمات جدید v6 (Q24–Q30) — در انتظار پیاده‌سازی:
| # | تصمیم | وضعیت | اولویت |
|---|-------|-------|--------|
| Q24 | آستانه موجودی ≤ ۱۰۰,۰۰۰ تومان (ورود Magic + خرید مجدد) | ⬜ | 🔴 P0 |
| Q25 | DayaLoans فقط پکیج پایه — تایید (بدون تغییر کد) | ✅ تایید | — |
| Q26 | SP Worker — auto-deploy با checksum | ⬜ | 🔴 P0 |
| Q27 | History Tables — نام‌گذاری یکسان + IHasHistory + interceptor | ⬜ | 🟡 P1 |
| Q28 | UI Guidance — مودال + متن آموزشی/هشداری در FO/BO | ⬜ | 🟡 P1 |
| Q29 | شرط EXIT Magic — تایید: آخرین پکیج فعال (بدون تغییر کد) | ✅ تایید | — |
| Q30 | Carryover — تایید: توضیح مستند شد (بدون تغییر کد) | ✅ تایید | — |
---
*آخرین بروزرسانی: ۸ اسفند ۱۴۰۴ — آماده تست و استقرار*
*آخرین بروزرسانی: ۸ اسفند ۱۴۰۴ — v6: Q24-Q30 اضافه شد (آستانه موجودی + SP Worker + History + UI Guidance)*