docs(biz): v6 — Q24-Q30: balance threshold, SP worker, history tables, UI guidance, DayaLoans+EXIT+carryover confirmations
This commit is contained in:
@@ -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 نمیشه!
|
||||
```
|
||||
|
||||
@@ -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 فعلی مشکلی ندارند.
|
||||
|
||||
---
|
||||
|
||||
## ضمیمه: ۲۳ تصمیم بیزینسی پیادهشده
|
||||
## ضمیمه: ۳۰ تصمیم بیزینسی (Q1–Q30)
|
||||
|
||||
### پیادهشده (Q1–Q23):
|
||||
|
||||
| # | تصمیم | وضعیت |
|
||||
|---|-------|-------|
|
||||
@@ -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)*
|
||||
|
||||
Reference in New Issue
Block a user