Compare commits

..

10 Commits

Author SHA1 Message Date
masoodafar-web 4218d08597 docs: add session log for 2026-05-13 (guest browsing + top-seller landing sections)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 19:50:15 +03:30
masoodafar-web 1db77b1a1b Add comprehensive database integrity audit report and fix critical bugs in commission pool charging flow
- Introduced a detailed audit report for the CMS database integrity, highlighting issues related to data entry, code bugs, and stored procedures.
- Fixed double-charge issue in the commission pool during club membership activation.
- Updated stored procedures to ensure correct pool calculations across different weeks.
- Enhanced the network tree feature to include activation type and package details.
- Improved UI for the network tree display and resolved pagination issues in the discount store.
2026-05-01 00:07:25 +03:30
masoodafar-web e3850f9dd8 docs: فاز ۱۱ — فیکس‌های پرداخت ZarinPal + تصحیح تومان/ریال + امنیت Callback URL
- CHANGELOG: Phase 11 (11a-11f) — ZarinPal verify fix, تومان/ریال مدل, صفحه موفقیت, حذف ×۱۰ دوبار, callback URL امنیت
- BUSINESS-02: تصحیح مدل ارزی (DB=تومان نه ریال), ZarinPal verify fix, جدول callback URL امنیت
- TECH-01: اضافه CmsBaseUrl/FrontOfficeBaseUrl به appsettings, توضیح امنیت Open Redirect
- TECH-02: اضافه PaymentCallback.razor, وضعیت‌های جدید
- ROADMAP: بروزرسانی Payment 97→99%, اضافه فاز ۱۱ به DONE list
2026-02-27 22:33:35 +03:30
masoodafar-web 39590d2cbe docs: Phase 10 — DataMigration + EF Staging + PackagePurchaseDialog + UI Fixes
Updated 8 docs:
- CHANGELOG: Phase 10a-d (DataMigration tool, EF staging migrations, PackagePurchaseDialog, 4 UI fixes)
- PAYMENT-FINANCE: Rial→Toman conversion chain documented, PackagePurchaseDialog status
- TECH-02: Added PackagePurchaseDialog to folder structure + status table
- TECH-04: DataMigration tool features (smart retry, FK handling, fallback tables), EF staging
- PACKAGE-TASKS: Phase 10 graph, NuGet v0.0.189, T4.1+T4.2 marked 
- BIZ-PACKAGE: v6→v7, commits updated, T4.1+T4.2 marked 
- INDEX: Updated last-update + R3 description
- ROADMAP: Progress bars updated, DONE section + NOW section refreshed
2026-02-27 20:56:00 +03:30
masoodafar-web de69bf862c docs: F1-F7 همه تکمیل — آپدیت PACKAGE-MIGRATION-GUIDE
- F1-F7 از 🟡 به  تغییر کردند
- NuGet: v0.0.188 → v0.0.189
- R8 (validator hardcoded 1B): فیکس شد
- کامیت هش‌های جدید اضافه شد
- آمار کامیت‌ها و تاریخ بروز شد
2026-02-27 09:05:50 +03:30
masoodafar-web 6fc15b474e docs: update PACKAGE-MIGRATION-GUIDE — Q24-Q30 all done, F8-F11 completed
- Mark Q24, Q26, Q27, Q28 as  with commit refs
- Mark F8-F11 as completed (were marked as future/pending)
- Add 3 new CMS commits (a1024a3, fdbb91d, 10d2ca2)
- Add FO commit (474d364) and BO commit (6939780)
- Add Migration step 2.5b: Q27_HistoryTables_And_RenameWalletHistory
- Update stats: 48 commits, 227+ files, 30 business decisions
- Update CMS commit table header to 20 commits
2026-02-27 06:50:07 +03:30
masoodafar-web e9f1fb9911 docs: Phase 9 — Q24-Q30 + History Interceptor + Rename + Migration
- CHANGELOG: add Phase 9 (9a-9d) with all new commits (CMS:a1024a3→fdbb91d→10d2ca2, FO:474d364, BO:6939780)
- CHANGELOG: update summary table (279 items, 99%)
- TASKS: add Phase 9 section with detailed descriptions + update commit list (30 total)
- BIZ: update header to 'code complete — Phase 0-9 ' + add Phase 9 commits
- TECH-01: add SP Worker (Q26), History Tracking System (Q27), IHasHistory, Interceptor docs
- TECH-01: add UserWalletHistoryService (renamed from ChangeLog)
- INDEX: update R3/R6 descriptions, update timestamp
2026-02-27 06:35:09 +03:30
masoodafar-web 085583c274 docs(biz): v6 fix — carryover is per-DOWNLINE-package (not per-user-package), user's own package change has NO effect on carryover 2026-02-27 04:01:25 +03:30
masoodafar-web f8908d8e2b docs(biz): v6 — Q24-Q30: balance threshold, SP worker, history tables, UI guidance, DayaLoans+EXIT+carryover confirmations 2026-02-27 03:42:47 +03:30
masoodafar-web ba10b6485b docs: add comprehensive Package Migration Guide — business impact + step-by-step deployment plan 2026-02-27 03:00:23 +03:30
14 changed files with 2716 additions and 117 deletions
+380 -64
View File
@@ -1,10 +1,10 @@
# 📦 سیستم مبتنی بر پکیج (Package-Based System)
> **وضعیت:** در حال پیاده‌سازی — **فاز ۰-۴ تکمیل ✅** | فاز ۵-۶ در انتظار
> **تاریخ بروزرسانی:** ۷ اسفند ۱۴۰۴
> **نسخه:** v5 (قرارداد یک‌بار + فیچر DIFF + تاریخچه فعال‌سازی)
> **تاثیرگذاری:** زیاد — **۹۵+** تغییر در ۶ لایه (۵۱ اصلی + ۴۴ سایدافکت)
> **کامیت‌ها:** `8b9c317` (Phase 0) → `ae92ab8` (Phase 1) → `a9cd2fd` (Phase 1.5) → `8e5c7c5` (Phase 2) → `ccb938e` (Phase 3) → `0002a5a` (Phase 4)
> **وضعیت:** پیاده‌سازی کد تکمیل — **فاز ۰-۱۰ ✅** | تست نهایی باقی‌مانده
> **تاریخ بروزرسانی:** ۸ اسفند ۱۴۰۴
> **نسخه:** v7 (DataMigration + EF Staging + PackagePurchaseDialog + UI Fixes)
> **تاثیرگذاری:** زیاد — **۱۰۰+** تغییر در ۶ لایه (۵۱ اصلی + ۴۴ سایدافکت + ۵ استقرار)
> **کامیت‌ها:** `8b9c317` (Phase 0) → `ae92ab8` (Phase 1) → `a9cd2fd` (Phase 1.5) → `8e5c7c5` (Phase 2) → `ccb938e` (Phase 3) → `0002a5a` (Phase 4) → `a1024a3` (Q24+Q26) → `fdbb91d` (Q27) → `10d2ca2` (Rename+Interceptor+Migration) → `a3681a8` (PackagePurchaseDialog) → `3c1a8ff` (UI Fixes)
---
@@ -42,7 +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 توضیح | ✅ **باقی‌مانده تعادل هفتگی** — بر اساس پکیج **زیرمجموعه‌ها**. تغییر پکیج خود کاربر carryover را ریست **نمی‌کند**. هر Pool مجزا |
---
@@ -554,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)
@@ -593,9 +603,8 @@ Commission:
FirstActivationDate = حفظ, FirstPackageId = حفظ
LastActivationDate = now (بروزرسانی), LastPackageId = پایه
✅ ClubMembershipCycle #2 ساخته می‌شود
NetworkWeeklyBalance جدید (PackageId=پایه, WeekId=هفته جاری)
carryover از هفته قبل: فقط carryover پکیج پایه (نه نقره‌ای!)
✅ بالاسری‌ها: محاسبه مجدد از Pool_پایه
بالاسری علی: carryover همه Poolها (بر اساس پکیج زیرمجموعه‌ها) حفظ می‌شود
پورسانت بالاسری: از هر Pool که زیرمجموعه‌ای دارد
... همان چرخه Magic Wallet تکرار ...
@@ -615,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)
7. Remainder → carryover هفته بعد (برای PackageId = X)
8. پورسانت: TotalBalances × ValuePerBalance_X → کیف پول شبکه
مجموع پورسانت از همه Poolها → واریز به کیف پول شبکه کاربر
(به تفکیک مشخص: هر مبلغ از کدام Pool)
```
**مثال 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
علی (خودش پکیج پایه داره)
├── تیم چپ:
│ ├── سارا (پکیج پایه)
│ ├── رضا (پکیج پایه)
│ └── مریم (پکیج نقره‌ای)
└── تیم راست:
├── حسین (پکیج پایه)
└── زهرا (پکیج نقره‌ای)
```
**مثال عملی:**
```
هفته ۱۰ — محاسبه تعادل علی:
═══════════════════════════════════════════════════════
═══ 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 نقره‌ای
```
**هفته ۱۱ — علی پکیج خودش رو عوض می‌کنه (پایه → نقره‌ای):**
```
هفته ۱۰:
علی (پکیج پایه):
تعادل_پایه: Left=45, Right=52, Min=45 (cap 300) → OK
تعادل_نقره‌ای: Left=0, Right=0 (علی پکیج نقره‌ای نداره)
═══ Pool پکیج پایه ═══
carryover از هفته ۱۰: {چپ: 4, راست: 0} ← هنوز هست!
✅ تغییر پکیج خود علی تاثیری نداره!
اعضای جدید: چپ = 0, راست = 1
جمع: چپ = 0+4 = 4, راست = 1+0 = 1
تعادل = MIN(4, 1) = 1
✅ علی هنوز از Pool پایه سود می‌بره (چون زیرمجموعه‌هایی با پکیج پایه داره)
سارا (پکیج نقره‌ای):
تعادل_پایه: Left=0, Right=0
تعادل_نقره‌ای: Left=12, Right=8, Min=8 (cap 30) → OK
═══ Pool پکیج نقره‌ای ═══
carryover از هفته ۱۰: {چپ: 0, راست: 0}
... محاسبه عادی ...
```
رضا (پکیج پایه + قبلاً نقره‌ای داشته):
تعادل_پایه: Left=30, Right=25, Min=25 (cap 300) → OK
تعادل_نقره‌ای: Left=2 (carryover), Right=0 (carryover) → Min=0
**نکته کلیدی:** پکیج **خود کاربر** فقط تعیین می‌کنه ActivationFee‌اش به کدوم Pool بره.
**تعادل و carryover** بر اساس پکیج **زیرمجموعه‌ها** محاسبه می‌شه.
هفته ۱۱ (Shift):
علی: carryover_پایه = {Left: surplus_left, Right: surplus_right}
سارا: carryover_نقره‌ای = {Left: surplus_left, Right: surplus_right}
رضا: carryover_پایه = {...}, carryover_نقره‌ای = {Left:2, Right:0}
**Cap (سقف):**
```
اگه تعادل بیشتر از MaxBalancesPerLeg بشه → بریده می‌شه (flush):
تعادل = 500، سقف = 300 → CappedBalance = 300، Flushed = 200
⚠️ Flushed از بین می‌ره — carry نمی‌شه!
```
### ۶.۵ تغییرات SP (v3 — بروزرسانی)
@@ -1391,8 +1431,8 @@ SELECT 'Balances', COUNT(*) FROM "CMS"."NetworkWeeklyBalances" WHERE "PackageId"
| تسک | شرح | v3? |
|-----|----- |-----|
| **T4.1** | FrontOffice: کاشی‌های پکیج (داینامیک) | |
| **T4.2** | FrontOffice: مدال پرداخت (دایا+مستقیم / فقط مستقیم) | |
| **T4.1** | FrontOffice: کاشی‌های پکیج (داینامیک) — ✅ `PackagePurchaseDialog.razor` | |
| **T4.2** | FrontOffice: مدال پرداخت (دایا+مستقیم / فقط مستقیم) — ✅ Step 2 in dialog | |
| **T4.3** | FrontOffice: MyPackages — re-purchase | |
| **T4.4** | FrontOffice: ActivationSection + Contract — داینامیک | |
| **T4.5** | BackOffice: CRUD پکیج — فیلدهای جدید (۱۱ فیلد) | 🔄 |
@@ -1466,3 +1506,279 @@ 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 — توضیح ساده (v6 اصلاح‌شده)
> **Carryover = باقی‌مانده تعادل از هفته قبل — بر اساس پکیج زیرمجموعه‌ها**
**مثال ساده:**
```
علی بالاسری — زیرمجموعه‌هاش پکیج پایه دارن:
هفته ۱۰:
تیم چپ (پکیج پایه) = ۵۰ نفر، تیم راست (پکیج پایه) = ۳۰ نفر
تعادل_پایه = MIN(50, 30) = 30
باقی‌مانده_پایه: {چپ: 20, راست: 0} ← CARRYOVER
هفته ۱۱:
اعضای جدید (پکیج پایه): چپ = 5، راست = 10
جمع با carryover: چپ = 5+20 = 25، راست = 10+0 = 10
تعادل_پایه = MIN(25, 10) = 10
باقی‌مانده_پایه: {چپ: 15, راست: 0} ← CARRYOVER بعدی
```
**تغییر پکیج خود کاربر — تاثیری ندارد (Q23 اصلاحی):**
```
علی پکیج خودش رو عوض می‌کنه (پایه → نقره‌ای):
carryover_پایه = {چپ: 20, راست: 0} ← هنوز هست! حذف نمی‌شه!
carryover_نقره‌ای = {چپ: 0, راست: 0}
✅ هفته بعد، علی هنوز از Pool پایه سود می‌بره
(چون زیرمجموعه‌هایی با پکیج پایه داره)
✅ همزمان از Pool نقره‌ای هم سود می‌بره
(چون زیرمجموعه‌هایی با پکیج نقره‌ای هم داره)
پکیج خود علی فقط تعیین می‌کنه:
→ ActivationFee‌اش به کدوم Pool بره
→ MagicWallet سقفش چقدره
```
+30 -5
View File
@@ -1,7 +1,7 @@
# 💰 سیستم مالی، پرداخت و درگاه‌ها
> **منابع ادغام‌شده:** `payment-gateway.md`, `payment-architecture-pyms.md`, `daya-loan-integration.md`, `manual-payment-system.md`, `discount-shop-business.md`
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: فیکس WalletChangeLog + validation کیف‌پول اعتباری + نام‌گذاری جدید کیف‌پول‌ها)
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: تصحیح مدل تومان/ریال + فیکس ZarinPal Verify + امنیت Callback URL)
---
@@ -47,21 +47,41 @@ flowchart TD
```mermaid
flowchart TD
A["کاربر → انتخاب محصول\nدرخواست پرداخت"] --> B["CMS → CreatePaymentRequest\ngRPC to PYMS"]
B --> C["PYMS → ZarinPal API\nدریافت Authority"]
B --> C["PYMS → ZarinPal API\nمبلغ ×۱۰ (تومان→ریال)\nدریافت Authority"]
C --> D["Redirect کاربر\nصفحه پرداخت ZarinPal"]
D --> E["بازگشت با Authority\nCMS VerifyPayment"]
D --> E["بازگشت با Authority\nCMS VerifyPayment (مبلغ ×۱۰)"]
E -->|موفق| F["✅ ثبت سفارش\n+ شارژ کیف‌پول"]
E -->|ناموفق| G["❌ نمایش پیام خطا"]
```
> **✅ فیکس ZarinPal Verify (اسفند ۱۴۰۴ — `721661a`):**
> - **باگ:** `VerifyPaymentAsync(authority)` با ۲ آرگومان → amount=0 → ZarinPal Code=-1
> - **فیکس:** lookup `PaymentTransaction.Amount` از DB + استفاده از overload ۳ آرگومانه `VerifyPaymentAsync(authority, orderId, amount)`
> - `IPaymentGatewayService` — default impl ۳ آرگومانه اضافه شد
> - ۷ فایل تغییر: PackageService, TransactionsService, VerifyDiscountWalletChargeCommandHandler, VerifyPackagePurchaseCommandHandler, IPaymentGatewayService, MockPaymentGatewayService, DayaPaymentService
### ۲.۲ تنظیمات ZarinPal
| پارامتر | مقدار |
|----------|-------|
| `MerchantId` | `4225d555-5fa9-4df0-9b61-1ce152cbbba8` |
| `CallbackUrl` | `/payment/callback` |
| `CallbackUrl` | از `appsettings.json` خوانده می‌شود (نه از ورودی کاربر) |
| `Sandbox` | `true` (staging) / `false` (production) |
| `Currency` | IRR (ریال → تبدیل به تومان در UI) |
| `Currency` | DB: تومان — ZarinPal: ریال (×۱۰ هنگام ارسال) |
> **✅ مدل ارزی (تصحیح اسفند ۱۴۰۴):**
> - **DB:** `Package.Price` و همه مبالغ مالی به **تومان** ذخیره می‌شوند
> - **CMS → ZarinPal:** `ZarinPalPaymentService` مبلغ را ×۱۰ می‌کند (`amountInRials = amount * 10`)
> - **FrontOffice UI:** مبالغ مستقیم به تومان نمایش داده می‌شوند (بدون تبدیل)
> - **FrontOffice → CMS:** مبالغ به تومان ارسال می‌شوند (FO هیچ تبدیلی انجام نمی‌دهد)
> - **باگ قبلی ۱:** FO مبلغ تومان را ×۱۰ تبدیل می‌کرد + CMS/ZarinPal دوباره ×۱۰ → مبلغ ۱۰۰ برابر (فیکس: `2f9ef15`)
> - **باگ قبلی ۲:** `FormattedPrice = Price / 10` اشتباه بود — Price از قبل تومان است (فیکس: `3c1a8ff` اصلاح شد)
> **✅ امنیت Callback URL (اسفند ۱۴۰۴):**
> - هیچ callback URL از ورودی کاربر خوانده نمی‌شود — همه از `appsettings.json` خوانده می‌شوند
> - `PackageService` و `TransactionsService`: از `FrontOfficeBaseUrl` config
> - `MagicWallet` و `DiscountWallet`: از `CmsBaseUrl` config
> - جلوگیری از حمله Open Redirect
> **تنظیمات محیطی:**
> - `appsettings.json` + `appsettings.Staging.json`: `UseSandbox: true` (تست)
@@ -239,11 +259,16 @@ service PaymentService {
| ماژول | وضعیت | یادداشت |
|-------|--------|---------|
| ZarinPal IPG | ✅ کامل | **Production فعال** — MerchantId: `4225d555...` |
| ZarinPal Verify | ✅ فیکس شده | رفع amount=0 با overload ۳ آرگومانه (`721661a`) |
| Callback URL امنیت | ✅ فیکس شده | همه از config خوانده می‌شوند — جلوگیری از Open Redirect |
| وام دایا | ✅ کامل | Mock mode فعال در staging |
| پرداخت ترکیبی | ✅ کامل | Discount + IPG |
| Pool هفتگی | ✅ کامل | SP + Hangfire |
| WalletChangeLog | ✅ فیکس شده | لاگ تغییرات کیف‌پول در ۳ هندلر اضافه شد |
| Validation کیف‌پول اعتباری | ✅ فیکس شده | ارور اگر موجودی کافی نباشد |
| Toman/Rial مدل | ✅ تصحیح شده | DB=تومان، فقط ZarinPal ریال (×۱۰) — FO بدون تبدیل |
| صفحه موفقیت پرداخت | ✅ بهبود | TransactionId + موجودی واقعی + دکمه بازگشت |
| PackagePurchaseDialog | ✅ کامل | دیالوگ داینامیک کاشی‌ای با انتخاب روش پرداخت |
| پرداخت دستی | ⬜ طراحی | نیاز به تصمیم مدیریت |
| Refund | ⬜ طراحی | فقط در PYMS تعریف‌شده |
| کیف‌پول جادویی (Magic) | ✅ کامل | فاز 1-6 پیاده‌سازی شده — Production فعال |
+700
View File
@@ -0,0 +1,700 @@
# 📦 راهنمای مهاجرت سیستم پکیج‌بیس — خلاصه تغییرات و پلن استقرار
> **وضعیت:** آماده تست و استقرار — **Q1-Q30 تکمیل‌شده ✅ | F1-F11 تکمیل‌شده ✅**
> **تاریخ:** ۸ اسفند ۱۴۰۴ (27 Feb 2026) — آپدیت ۱۰ اسفند
> **نسخه NuGet:** v0.0.189
> **تعداد کامیت‌ها:** ۵۱+ کامیت در ۴ ریپازیتوری (۲۱ CMS + ۹ FO + ۷ BO + ۱۴+ docs)
> **مدت پیاده‌سازی:** ۶ روز (۲۴ فوریه – ۱ مارس ۲۰۲۶)
> **ریپوها:** CMS (`gitea`/`kub-stage`) · FrontOffice (`kub-stage`) · BackOffice (`kub-stage`) · totalDoc (`foursatDocs`/`main`)
---
## فهرست مطالب
1. [خلاصه اجرایی](#1-خلاصه-اجرایی)
2. [چه چیزی تغییر کرده؟ — نمای بیزینسی](#2-چه-چیزی-تغییر-کرده--نمای-بیزینسی)
3. [بخش‌های تحت تاثیر سیستم](#3-بخشهای-تحت-تاثیر-سیستم)
4. [جزئیات تغییرات هر ریپو](#4-جزئیات-تغییرات-هر-ریپو)
5. [پلن مهاجرت مرحله‌به‌مرحله](#5-پلن-مهاجرت-مرحلهبهمرحله)
6. [Rollback Plan](#6-rollback-plan)
7. [چک‌لیست تست قبل از Production](#7-چکلیست-تست-قبل-از-production)
8. [ریسک‌ها و نکات بحرانی](#8-ریسکها-و-نکات-بحرانی)
---
## 1. خلاصه اجرایی
### قبل (سیستم تک‌پکیج):
- فقط **یک پکیج پایه** (۵۶ میلیون تومان) وجود داشت
- تمام مقادیر مالی (قیمت، هزینه فعال‌سازی، ضرایب، سقف‌ها) **hardcoded** در کد بودند
- خرید مجدد پکیج **غیرممکن** بود (حتی بعد تکمیل چرخه)
- پورسانت فقط از **یک Pool واحد** محاسبه می‌شد
- همه کاربران **همه فیچرها** را دریافت می‌کردند
### بعد (سیستم چندپکیجی):
- سیستم **N پکیج** با قیمت و ویژگی‌های متفاوت پشتیبانی می‌کند
- تمام مقادیر مالی از **دیتابیس (Package entity)** خوانده می‌شوند
- خرید مجدد بعد تکمیل چرخه Magic Wallet **فعال** شده
- هر پکیج **Commission Pool مستقل** خود را دارد
- فیچرها **per-package** هستند و با الگوریتم **DIFF** مدیریت می‌شوند
- قرارداد باشگاه **فقط یک بار** (اولین خرید) امضا می‌شود
### آمار تغییرات:
| شاخص | مقدار |
|-------|-------|
| فایل‌های تغییریافته | **۲۲۷+ فایل** |
| خطوط اضافه‌شده | **+۱۷,۰۰۰+** |
| خطوط حذف‌شده | **−۲,۶۶۰+** |
| تصمیمات بیزینسی پیاده‌شده | **۳۰ تصمیم** (Q1Q30) |
| باگ‌های فیکس‌شده | **۶ باگ بحرانی** |
| مقادیر hardcoded حذف‌شده | **۱۵+ مورد** |
| Handlerهای deprecated حذف‌شده | **۴ handler** (۱۲ فایل) |
| RPCهای deprecated حذف‌شده | **۴ RPC** + ۸ message type |
| فایل‌های rename شده | **۳۴ فایل** + ۱۱ دایرکتوری (UserWalletChangeLog → UserWalletHistory) |
| History Tables جدید | **۳ جدول** (PackageHistories, ClubMembershipCycleHistories, UserWalletHistories) |
---
## 2. چه چیزی تغییر کرده؟ — نمای بیزینسی
### 2.1 🏪 مدل فروش پکیج
| قابلیت | قبل | بعد |
|--------|-----|------|
| تعداد پکیج | ۱ (پایه ۵۶M) | **N پکیج** (پایه ۵۶M + نقره‌ای ۵.۶M + ...) |
| قیمت‌گذاری | hardcoded `56_000_000` | از `Package.Price` در دیتابیس |
| هزینه فعال‌سازی | hardcoded `25_200_000` | از `Package.ActivationFee` |
| ضریب تخفیف | hardcoded `× 2` | از `Package.DiscountMultiplier` |
| پشتیبانی دایا | فقط پکیج پایه | بر اساس `Package.SupportsDayaPurchase` |
| پرداخت مستقیم | همه | بر اساس `Package.SupportsDirectPurchase` |
### 2.2 🔄 چرخه خرید مجدد (Re-Purchase)
| مرحله | قبل | بعد |
|-------|-----|------|
| تکمیل چرخه Magic | کاربر در بن‌بست | `PackagePurchaseMethod = None` ریست می‌شود |
| خرید مجدد | **مسدود** (guard G1-G3) | **مجاز** — بعد تکمیل چرخه Magic |
| قرارداد باشگاه | هر بار | **فقط یک بار** — خرید مجدد Skip (Q19) |
| فیچرها | همه فیچرها بدون توجه به پکیج | **DIFF/تفاضل** — فقط اختلاف اعمال می‌شود (Q20) |
| تاریخچه | فقط `ActivatedAt` | `FirstActivationDate` + `LastActivationDate` (Q21) |
### 2.3 💰 پورسانت و تعادل‌ها
| ویژگی | قبل | بعد |
|-------|-----|------|
| Commission Pool | ۱ Pool واحد | **Pool جداگانه هر پکیج** |
| تعادل هفتگی | ۱ رکورد per user/week | **N رکورد** per user/week/package |
| MaxBalancesPerLeg | hardcoded `300` | per-package (پایه=۳۰۰, نقره‌ای=۳۰) |
| MaxNetworkLevel | hardcoded `15` | per-package از دیتابیس |
| Carryover | یک‌پارچه | **per-downline-package** — بر اساس پکیج زیرمجموعه‌ها (تغییر پکیج خود کاربر تاثیری ندارد) |
| Stored Procedure | پارامترهای ثابت | پارامترهای داینامیک از Package entity |
| گزارش مشتری | بدون تفکیک | **breakdown per-package** |
| گزارش ادمین | بدون فیلتر | **فیلتر بر اساس پکیج** |
### 2.4 🪄 کیف پول جادویی (Magic Wallet)
| ویژگی | قبل | بعد |
|-------|-----|------|
| ضریب جادویی | hardcoded `× 2.5` | از `Package.MagicWalletMultiplier` |
| سقف واریز | hardcoded `1,000,000,000` | از `Package.MagicWalletMaxDeposit` |
| سقف اعتبار | hardcoded `2,500,000,000` | از `Package.MagicWalletMaxCredit` |
| شرط EXIT | بررسی سقف global | بررسی سقف **per-package** |
### 2.5 📋 فیچرهای باشگاه
| ویژگی | قبل | بعد |
|-------|-----|------|
| تخصیص فیچر | `GetAllFeatureIds()` — همه فیچرها | از `Package.PackageFeatures` — per-package |
| خرید مجدد | — | الگوریتم **DIFF**: مقایسه فیچرهای فعلی با پکیج جدید |
| مدیریت ادمین | — | ماتریس checkbox پکیج × فیچر در BackOffice |
---
## 3. بخش‌های تحت تاثیر سیستم
### 3.1 نقشه تاثیرگذاری
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 🏗️ سیستم پکیج‌بیس — Impact Map │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─── CMS (Backend) ──────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 📦 Domain Layer (Entity تغییرات) │ │
│ │ ├── Package.cs ← +۱۱ فیلد جدید │ │
│ │ ├── PackageFeature.cs ← Entity کاملاً جدید │ │
│ │ ├── ClubMembership.cs ← ActivatedAt → ۴ فیلد First/Last │ │
│ │ ├── ClubMembershipCycle.cs ← +PackageId │ │
│ │ ├── WeeklyCommissionPool.cs ← +PackageId │ │
│ │ ├── UserCommissionPayout.cs ← +PackageId │ │
│ │ ├── NetworkWeeklyBalance.cs ← +PackageId │ │
│ │ └── SystemConstants.cs ← حذف ۹ ثابت منسوخ │ │
│ │ │ │
│ │ ⚙️ Application Layer (Handler تغییرات) │ │
│ │ ├── ActivateClubMembershipCommandHandler ← فیچر DIFF + re-activate│ │
│ │ ├── AcceptClubMembershipContractCommandHandler ← فیچر DIFF │ │
│ │ ├── VerifyPackagePurchaseCommandHandler ← حذف fallback 2.0m │ │
│ │ ├── CustomerPurchasePackage/Verify ← Generic purchase flow │ │
│ │ ├── ChargeMagicWalletCommandHandler ← سقف per-package │ │
│ │ ├── VerifyMagicWalletChargeCommandHandler ← ضریب per-package │ │
│ │ ├── UserOrderService (EXIT Magic) ← ریست + سقف per-package │ │
│ │ ├── CreateManualPaymentCommandHandler ← ضریب از Package │ │
│ │ └── CheckAndProcessDayaLoansCommandHandler ← حذف ID=4 │ │
│ │ │ │
│ │ 🔌 Infrastructure Layer │ │
│ │ ├── sp_CalculateWeeklyBalances ← @PackageId + @Max params │ │
│ │ ├── sp_CalculateWeeklyCommissionPool ← @PackageId │ │
│ │ ├── WeeklyCommissionCalculationService ← Loop per-package │ │
│ │ ├── OrmCommissionCalculationStrategy ← فیلتر PackageId │ │
│ │ └── SpCommissionCalculationStrategy ← پارامترهای داینامیک │ │
│ │ │ │
│ │ 📡 Proto/gRPC Layer │ │
│ │ ├── package.proto ← ۱۱ فیلد + PackageFeature CRUD │ │
│ │ ├── commission.proto ← package_id/title در ۴ model + فیلتر │ │
│ │ ├── حذف ۴ RPC deprecated (Golden/Base) │ │
│ │ └── حذف ۸ message type deprecated │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── FrontOffice (مشتری) ─────────────────────────────────────────────┐│
│ │ ├── Packages.razor ← کاشی‌های داینامیک (نه hardcoded) ││
│ │ ├── PackageDetail.razor ← فیچرها از API (نه ثابت) ││
│ │ ├── Checkout.razor ← پرداخت شرطی (دایا/مستقیم) ││
│ │ ├── MyPackages.razor ← خرید مجدد + پیشرفت Magic ││
│ │ ├── ActivationSection.razor ← قیمت داینامیک (نه ۵۶M hardcoded) ││
│ │ ├── ClubMembershipContractDialog ← متن قرارداد داینامیک ││
│ │ ├── CommissionDashboard ← فیلتر + ستون پکیج ││
│ │ ├── WeeklyBalancePage ← فیلتر per-package ││
│ │ ├── PaymentCallback ← مهاجرت به Customer* RPCs ││
│ │ └── حذف "پکیج طلایی" hardcoded (۵+ جا) ││
│ └─────────────────────────────────────────────────────────────────────┘│
│ │
│ ┌─── BackOffice (ادمین) ──────────────────────────────────────────────┐│
│ │ ├── Package CRUD ← +۱۲ فیلد جدید در Create/Update ││
│ │ ├── PackageFeature Matrix ← checkbox فیچرها ││
│ │ ├── ManualPaymentDialog ← حذف ۵۶M hardcoded + Amount editable ││
│ │ ├── ChangeParentDialog ← جابجایی در شبکه (جدید) ││
│ │ ├── UserPayouts ← فیلتر + ستون پکیج ││
│ │ ├── BalancesReport ← فیلتر + ستون پکیج ││
│ │ ├── PackageSelect Component ← dropdown قابل استفاده مجدد ││
│ │ └── حذف "پکیج طلایی" → "خرید پکیج" ││
│ └─────────────────────────────────────────────────────────────────────┘│
│ │
│ ┌─── Database ────────────────────────────────────────────────────────┐│
│ │ ├── Packages ← ۱۱ ستون جدید + Seed نقره‌ای ││
│ │ ├── PackageFeatures ← جدول جدید ││
│ │ ├── ClubMemberships ← ۴ ستون First/Last + حذف ActivatedAt ││
│ │ ├── ClubMembershipCycles ← +PackageId ││
│ │ ├── WeeklyCommissionPools ← +PackageId + Unique ││
│ │ ├── UserCommissionPayouts ← +PackageId + Unique ││
│ │ ├── NetworkWeeklyBalances ← +PackageId + Unique ││
│ │ └── EF Migration + Data Backfill ││
│ └─────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────┘
```
### 3.2 خلاصه آماری per-repo
| ریپو | کامیت | فایل | اضافه | حذف | شرح اصلی |
|-------|-------|------|-------|-----|-----------|
| **CMS** | ۲۰ | ۱۷۹+ | +۱۳,۵۸۶ | −۲,۳۲۷ | Domain + Business + Commission + Proto + History + Rename + Interceptor |
| **FrontOffice** | ۸ | ۲۹ | +۵۵۰ | −۱۳۶ | Dynamic UI + Customer RPCs + Per-package Reports + UI Guidance |
| **BackOffice** | ۶ | ۲۴ | +۵۸۰ | −۳۱ | Package CRUD + Feature Matrix + Per-package Reports + UI Guidance |
| **totalDoc** | ۱۴ | ۱۳ | +۲,۷۰۰ | −۱۹۴ | مستندات بیزینسی + تکنیکال + Phase 9 |
---
## 4. جزئیات تغییرات هر ریپو
### 4.1 CMS — ۲۰ کامیت
| فاز | کامیت | شرح |
|-----|--------|------|
| **Phase 0** | `8b9c317` | فیکس ۴ باگ بحرانی: DiscountBalance + UserPackagePurchase |
| **Phase 0** | `fe3edd1` | فیکس EXIT Magic Mode — ریست PackagePurchaseMethod + بستن چرخه |
| **Phase 1** | `ae92ab8` | زیرساخت Domain: Package +۱۱ فیلد، PackageFeature entity، FKهای جدید |
| **Phase 1.5** | `a9cd2fd` | EF Migration + Seed Data + Data Backfill |
| **Phase 2** | `8e5c7c5` | جایگزینی همه SystemConstants با Package entity reads |
| **Phase 3** | `ccb938e` | بازسازی لایه Package + Proto enhancement + باگ‌فیکس |
| **Phase 4** | `0002a5a` | CRUD DTOs + Legacy fixes |
| **Phase 5** | `607f791` | پورسانت per-package + حذف ref طلایی |
| **SP Fix** | `7176fe4` | فیکس SP: `cm.PackageId``cm.LastPackageId` |
| **Phase 6** | `d19c569` | Deprecation cleanup + ConfigurationService MagicWallet |
| **Phase 7a** | `469d97b` | Cosmetic cleanup + حذف orphan handler |
| **Phase 7b** | `161f796` | Embed orderId در callback URL |
| **Phase 7c** | `8446e0e` | حذف ۴ handler deprecated (۱۴ فایل، −۱,۱۲۵ خط) |
| **Phase 8b** | `ce8e248` | NuGet bump → 0.0.185 |
| **Phase 8d** | `7554d70` | حذف ۴ RPC + ۸ message deprecated از Proto |
| **Phase 8e** | `aaaf7fc` | Per-package filtering در Commission queries |
| **Phase 8f** | `dcd1135` | PackageFeature CRUD support |
| **Audit** | `1ac2366` | Compliance audit — Feature DIFF + حذف fallbackهای hardcoded |
| **Phase 9a** | `a1024a3` | Q24: آستانه موجودی `≤1M` ریال + Q26: SP Worker auto-deploy (IHostedService + checksum) |
| **Phase 9b** | `fdbb91d` | Q27: PackageHistory + ClubMembershipCycleHistory entities + enums + EF configs |
| **Phase 9d** | `10d2ca2` | Rename UserWalletChangeLog→UserWalletHistory (86 فایل) + IHasHistory + Interceptor + Migration |
### 4.2 FrontOffice — ۸ کامیت
| فاز | کامیت | شرح |
|-----|--------|------|
| **Phase 7a** | `b82cac4` | حذف "پکیج طلایی" + PackageTitle در DTO |
| **Phase 7b** | `71f391a` | مهاجرت به Customer* RPCs |
| **Phase 8a** | `0bbc11e` | Checkout wire-up به Customer RPCs |
| **Phase 8c** | `d71d463` | صفحات پکیج — فیچرهای داینامیک |
| **Phase 8d** | `40882c8` | NuGet bump Proto cleanup |
| **Phase 8e** | `a956cb9` | Per-package filtering در Commission pages |
| **Phase 8f** | `3bffc13` | T4.2+T4.3+F3: پرداخت شرطی + خرید مجدد + PV |
| **Audit** | `816dcb7` | حذف ۵۶M hardcoded — قیمت‌گذاری داینامیک |
| **Phase 9c** | `474d364` | Q28: UI Guidance alerts (G1-G7) — ۷ صفحه MudAlert آموزشی |
### 4.3 BackOffice — ۶ کامیت
| فاز | کامیت | شرح |
|-----|--------|------|
| **Phase 7a** | `f1b0085` | تغییر label "پکیج طلایی" → "خرید پکیج" |
| **Phase 8b** | `89f5241` | Package CRUD expansion — ۱۲ فیلد جدید |
| **Phase 8d** | `c96377a` | NuGet bump Proto cleanup |
| **Phase 8e** | `8be98ae` | Per-package commission filtering + PackageSelect component |
| **Phase 8f** | `e020354` | ChangeParentDialog + PackageFeature checkbox matrix |
| **Audit** | `e6cf90e` | ManualPaymentDialog — حذف ۵۶M + Amount editable |
| **Phase 9c** | `6939780` | Q28: UI Guidance alerts (G8-G13) — ۶ صفحه MudAlert |
---
## 5. پلن مهاجرت مرحله‌به‌مرحله
### 📋 پیش‌نیازها
- [ ] بکاپ کامل از دیتابیس Production
- [ ] بکاپ از stateهای Kubernetes (Deployments, ConfigMaps)
- [ ] اطمینان از دسترسی به Container Registry (تصاویر فعلی)
- [ ] زمان‌بندی Maintenance Window (ترجیحاً شب یا آخر هفته)
- [ ] اطلاع‌رسانی به کاربران (در صورت نیاز به downtime)
---
### مرحله ۱ از ۶: بکاپ و آماده‌سازی محیط 🛡️
> ⏱️ تخمین: ۳۰ دقیقه
```
1.1 بکاپ کامل دیتابیس
└── pg_dump -Fc cms_db > cms_backup_pre_package_migration.dump
1.2 بکاپ دیتابیس BO (اگر جداست)
└── pg_dump -Fc bo_db > bo_backup_pre_package_migration.dump
1.3 ثبت وضعیت فعلی
└── تعداد رکوردها:
• ClubMemberships: SELECT COUNT(*) ...
• ClubMembershipCycles: SELECT COUNT(*) ...
• WeeklyCommissionPools: SELECT COUNT(*) ...
• UserCommissionPayouts: SELECT COUNT(*) ...
• NetworkWeeklyBalances: SELECT COUNT(*) ...
• Packages: SELECT COUNT(*) ...
1.4 ذخیره نسخه فعلی Docker images
└── docker tag <current-cms> cms:rollback-point
└── docker tag <current-fo> fo:rollback-point
└── docker tag <current-bo> bo:rollback-point
```
**✅ Checkpoint:** بکاپ‌ها ذخیره شده‌اند و قابل restore هستند.
---
### مرحله ۲ از ۶: استقرار CMS (Backend) 🏗️
> ⏱️ تخمین: ۴۵ دقیقه
> ⚠️ **ترتیب بحرانی:** CMS باید **اول** deploy شود چون FO و BO به آن وابسته‌اند.
```
2.1 Build CMS Docker image
└── cd CMS/src
└── docker build -t cms:package-based .
2.2 اجرای EF Migration
└── این migration شامل:
• ۱۱ ستون جدید به جدول Packages
• جدول جدید PackageFeatures
• ستون PackageId به ۵ جدول (ClubMemberships, Cycles, Pools, Payouts, Balances)
• ۴ ستون First/Last به ClubMemberships
• Unique Indexها
⚠️ Migration خودکار اجرا می‌شود در startup اگر EF auto-migration فعال باشد.
✅ اگر دستی: dotnet ef database update
2.3 Data Backfill — مقداردهی پکیج پایه
└── اسکریپت SQL:
┌──────────────────────────────────────────────────────────┐
│ -- مشخص کردن ID پکیج پایه │
│ DO $$ │
│ DECLARE base_pkg_id BIGINT; │
│ BEGIN │
│ SELECT "Id" INTO base_pkg_id │
│ FROM "CMS"."Packages" │
│ WHERE "IsBasePackage" = true LIMIT 1; │
│ │
│ -- ClubMemberships │
│ UPDATE "CMS"."ClubMemberships" │
│ SET "FirstActivationDate" = "ActivatedAt", │
│ "LastActivationDate" = "ActivatedAt", │
│ "FirstPackageId" = base_pkg_id, │
│ "LastPackageId" = base_pkg_id │
│ WHERE "FirstActivationDate" IS NULL; │
│ │
│ -- ClubMembershipCycles │
│ UPDATE "CMS"."ClubMembershipCycles" │
│ SET "PackageId" = base_pkg_id │
│ WHERE "PackageId" IS NULL; │
│ │
│ -- WeeklyCommissionPools │
│ UPDATE "CMS"."WeeklyCommissionPools" │
│ SET "PackageId" = base_pkg_id │
│ WHERE "PackageId" IS NULL; │
│ │
│ -- UserCommissionPayouts │
│ UPDATE "CMS"."UserCommissionPayouts" │
│ SET "PackageId" = base_pkg_id │
│ WHERE "PackageId" IS NULL; │
│ │
│ -- NetworkWeeklyBalances │
│ UPDATE "CMS"."NetworkWeeklyBalances" │
│ SET "PackageId" = base_pkg_id │
│ WHERE "PackageId" IS NULL; │
│ │
│ RAISE NOTICE 'Migration done: PackageId=%', │
│ base_pkg_id; │
│ END $$; │
└──────────────────────────────────────────────────────────┘
2.4 Verification — بررسی migration
┌──────────────────────────────────────────────────────────┐
│ 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 │
│ SELECT 'Pools', COUNT(*) │
│ FROM "CMS"."WeeklyCommissionPools" │
│ WHERE "PackageId" IS NULL │
│ UNION ALL │
│ SELECT 'Payouts', COUNT(*) │
│ FROM "CMS"."UserCommissionPayouts" │
│ WHERE "PackageId" IS NULL │
│ UNION ALL │
│ SELECT 'Balances', COUNT(*) │
│ FROM "CMS"."NetworkWeeklyBalances" │
│ WHERE "PackageId" IS NULL; │
│ │
│ -- ✅ همه باید 0 باشند! │
└──────────────────────────────────────────────────────────┘
2.5 Seed پکیج نقره‌ای (اگر توسط EF Seed انجام نشده)
└── INSERT پکیج نقره‌ای + PackageFeatures
2.5b اجرای Migration دوم: Q27_HistoryTables_And_RenameWalletHistory
└── این migration شامل:
• RenameTable: UserWalletChangeLogs → UserWalletHistories (حفظ داده‌ها!)
• RenameIndex × 2 + sp_rename PK + FK × 2
• CreateTable: PackageHistories (فیلدهای Old*/New*)
• CreateTable: ClubMembershipCycleHistories (فیلدهای Old*/New*)
⚠️ داده‌های قبلی UserWalletChangeLogs حفظ می‌شوند (RenameTable نه DropTable)
2.6 Deploy CMS به Kubernetes
└── kubectl set image deployment/cms cms=cms:package-based
└── kubectl rollout status deployment/cms
2.7 Health Check
└── curl http://cms-service/health
└── بررسی لاگ‌ها: kubectl logs deployment/cms --tail=100
```
**✅ Checkpoint:** CMS جدید بالا آمده، migration اجرا شده، همه رکوردها PackageId دارند.
---
### مرحله ۳ از ۶: استقرار FrontOffice 🖥️
> ⏱️ تخمین: ۲۰ دقیقه
> پیش‌نیاز: CMS باید بالا و سالم باشد
```
3.1 Build FrontOffice Docker image
└── cd FrontOffice/src
└── docker build -t fo:package-based .
3.2 Deploy به Kubernetes
└── kubectl set image deployment/frontoffice fo=fo:package-based
└── kubectl rollout status deployment/frontoffice
3.3 Smoke Test
└── ✅ صفحه پکیج‌ها باز می‌شود (کاشی‌های داینامیک)
└── ✅ جزئیات پکیج — فیچرها نمایش داده می‌شود
└── ✅ صفحه پاداش‌ها — فیلتر پکیج کار می‌کند
└── ✅ صفحه تعادل‌ها — per-package نمایش داده می‌شود
└── ✅ متن قرارداد — مبلغ داینامیک (نه ۵۶M hardcoded)
```
**✅ Checkpoint:** FrontOffice جدید بالا آمده و صفحات اصلی کار می‌کنند.
---
### مرحله ۴ از ۶: استقرار BackOffice 🛠️
> ⏱️ تخمین: ۲۰ دقیقه
> پیش‌نیاز: CMS باید بالا و سالم باشد
```
4.1 Build BackOffice Docker image
└── cd BackOffice/src
└── docker build -t bo:package-based .
4.2 Deploy به Kubernetes
└── kubectl set image deployment/backoffice bo=bo:package-based
└── kubectl rollout status deployment/backoffice
4.3 Smoke Test
└── ✅ CRUD پکیج — ۱۲ فیلد جدید نمایش داده می‌شود
└── ✅ ماتریس فیچر — checkboxها load می‌شوند
└── ✅ گزارش تعادل‌ها — فیلتر پکیج کار می‌کند
└── ✅ گزارش پرداخت‌ها — ستون پکیج نمایش داده می‌شود
└── ✅ ManualPayment — مبلغ editable (نه ۵۶M disabled)
```
**✅ Checkpoint:** BackOffice جدید بالا آمده و CRUD + گزارشات کار می‌کنند.
---
### مرحله ۵ از ۶: بررسی پورسانت (بحرانی!) 💰
> ⏱️ تخمین: ۳۰ دقیقه
> ⚠️ پورسانت = پول واقعی — دقت مضاعف لازم است
```
5.1 بررسی SP پارامترها
└── محاسبه پورسانت هفته تستی (staging)
└── بررسی: هر پکیج Pool جداگانه دارد
└── بررسی: MaxBalancesPerLeg صحیح (پایه=۳۰۰, نقره‌ای=۳۰)
└── بررسی: MaxNetworkLevel صحیح
5.2 مقایسه نتایج
└── اجرای محاسبه در staging
└── مقایسه Pool مبلغ با محاسبه دستی
└── ✅ تفاوت < ۱% قابل قبول
5.3 بررسی carryover
└── ✅ carryover فقط per-package
└── ✅ تغییر پکیج → ریست carryover
```
**✅ Checkpoint:** محاسبات پورسانت per-package صحیح هستند.
---
### مرحله ۶ از ۶: تنظیمات نهایی و بررسی سلامت ✅
> ⏱️ تخمین: ۱۵ دقیقه
```
6.1 بررسی PackageFeatures seed شده‌اند
└── SELECT * FROM "CMS"."PackageFeatures";
└── پکیج پایه: همه فیچرها ✅
└── پکیج نقره‌ای: فیچرهای تعیین‌شده ✅
6.2 بررسی JWT Claims (اختیاری)
└── لاگین یک کاربر تست → decode JWT
└── ✅ PackageId وجود دارد
└── ✅ CanRepurchase صحیح
6.3 غیرفعال کردن Maintenance Mode (اگر فعال بود)
6.4 مانیتورینگ ۲۴ ساعته
└── بررسی لاگ خطاها
└── بررسی response timeها
└── بررسی پرداخت‌های جدید
```
**✅ مهاجرت تکمیل شد!**
---
## 6. Rollback Plan
### سناریو ۱: مشکل در Migration دیتابیس
```bash
# Restore از بکاپ
pg_restore -d cms_db cms_backup_pre_package_migration.dump
# Rollback CMS image
kubectl set image deployment/cms cms=cms:rollback-point
```
### سناریو ۲: مشکل در CMS (بعد Migration موفق)
```bash
# ⚠️ نکته: migration undo ممکن نیست (ستون‌های جدید اضافه شده‌اند)
# اما کد قدیمی با ستون‌های nullable مشکلی ندارد
# Rollback فقط CMS image
kubectl set image deployment/cms cms=cms:rollback-point
```
### سناریو ۳: مشکل در FO/BO
```bash
# FO و BO مستقل از هم هستند — هرکدام جداگانه rollback
kubectl set image deployment/frontoffice fo=fo:rollback-point
kubectl set image deployment/backoffice bo=bo:rollback-point
```
### نکته مهم Rollback:
- ستون‌های جدید **nullable** هستند → کد قدیمی بدون مشکل کار می‌کند
- جدول `PackageFeatures` جدید است → کد قدیمی آن را ignore می‌کند
- **فقط Data Backfill** غیرقابل‌برگشت است (ولی ضرری ندارد — فقط NULL → مقدار)
---
## 7. چک‌لیست تست قبل از Production
### 🛒 خرید و فعال‌سازی
| # | تست | روش | نتیجه مورد انتظار |
|---|------|------|-------------------|
| 1 | خرید پکیج نقره‌ای (ZarinPal) | از FO → پکیج‌ها → نقره‌ای → پرداخت | Balance = ۵.۶M, Discount = ۱۱.۲M |
| 2 | خرید پکیج پایه (ZarinPal) | از FO → پکیج‌ها → پایه → پرداخت | Balance = ۵۶M, Discount = ۱۱۲M |
| 3 | خرید پکیج پایه (Daya Loan) | از FO → پکیج‌ها → پایه → دایا | Balance = ۵۶M + loan created |
| 4 | پرداخت دستی (BO) | از BO → ManualPayment → مبلغ دلخواه | Amount editable, not hardcoded |
| 5 | فعال‌سازی با نقره‌ای | فعال‌سازی باشگاه بعد خرید نقره‌ای | فقط فیچرهای نقره‌ای فعال (نه همه) |
| 6 | فعال‌سازی با پایه | فعال‌سازی باشگاه بعد خرید پایه | همه فیچرها فعال |
### 🔄 چرخه Magic + خرید مجدد
| # | تست | نتیجه مورد انتظار |
|---|------|-------------------|
| 7 | تکمیل چرخه Magic → ریست | PackagePurchaseMethod = None |
| 8 | خرید مجدد همان پکیج | بدون قرارداد مجدد، فقط شارژ wallet |
| 9 | خرید مجدد پکیج متفاوت (پایه → نقره‌ای) | DIFF اجرا: فیچرهای اضافی غیرفعال |
### 💰 پورسانت per-package
| # | تست | نتیجه مورد انتظار |
|---|------|-------------------|
| 10 | Pool جداگانه هر پکیج | WeeklyCommissionPool با PackageId متفاوت |
| 11 | MaxBalancesPerLeg متفاوت | پایه=۳۰۰, نقره‌ای=۳۰ |
| 12 | Carryover per-downline-package | تغییر پکیج خود کاربر → carryover حفظ (بر اساس زیرمجموعه‌ها) |
| 13 | SP پارامترها از Package | بدون hardcoded ۳۰۰/۱۵ |
### 📊 گزارشات per-package
| # | تست | نتیجه مورد انتظار |
|---|------|-------------------|
| 14 | FO — فیلتر dropdown پکیج | فیلتر عملکرد صحیح |
| 15 | FO — breakdown پاداش per-package | مبالغ صحیح به تفکیک |
| 16 | BO — فیلتر پکیج در تعادل‌ها | فیلتر عملکرد صحیح |
| 17 | BO — ستون پکیج در پرداخت‌ها | نام پکیج نمایش داده می‌شود |
### 📋 UI / قرارداد
| # | تست | نتیجه مورد انتظار |
|---|------|-------------------|
| 18 | متن قرارداد — مبلغ داینامیک | مبلغ و نام پکیج صحیح (نه ۵۶M hardcoded) |
| 19 | ActivationSection — قیمت | از API خوانده می‌شود |
| 20 | BO — ManualPayment editable | مبلغ قابل ویرایش با validation |
| 21 | BO — Package CRUD ۱۲ فیلد | همه فیلدهای جدید ذخیره/بارگذاری |
| 22 | BO — Feature Matrix | checkboxها sync با DB |
---
## 8. ریسک‌ها و نکات بحرانی
### 🔴 ریسک‌های بحرانی
| # | ریسک | احتمال | تاثیر | کاهش‌دهنده |
|---|-------|--------|-------|------------|
| R1 | Migration دیتابیس — PackageId اشتباه | کم | **فاجعه** | Verification query (مرحله 2.4) + بکاپ |
| R2 | SP تغییریافته → محاسبات مالی اشتباه | متوسط | **فاجعه** | تست staging + مقایسه دستی |
| R3 | Magic Wallet EXIT — سقف global به‌جای per-package | متوسط | **بالا** | بررسی MW1-MW3 در CMS handlers |
| R4 | قرارداد حقوقی — مبلغ اشتباه | کم | **حقوقی** | متن قرارداد داینامیک ✅ فیکس شده |
### 🟡 ریسک‌های متوسط
| # | ریسک | کاهش‌دهنده |
|---|-------|------------|
| R5 | Proto breaking change | Field numberها backward compatible (فقط اضافه) |
| R6 | NuGet version mismatch بین repos | همه روی v0.0.189 ✅ |
| R7 | JWT claims — cache invalidation | کاربران باید re-login کنند |
| R8 | ~~Validator hardcoded 1B~~ | ✅ فیکس شد — `SystemConstants.WalletMaxSafeAmount` (10B) حصار ایمنی |
### ⚠️ تغییرات آینده (هنوز پیاده‌نشده — Phase بعدی)
این موارد در BIZ spec شناسایی شده‌اند ولی **هنوز پیاده نشده‌اند**:
| # | مورد | شدت | شرح |
|---|------|------|------|
| ~~F1~~ | ~~WalletChangeLog + PackageId~~ | ✅ انجام‌شده | CMS:`e5bc3a9` — PackageId در UserWalletHistory |
| ~~F2~~ | ~~Notification + PackageId~~ | ✅ انجام‌شده | CMS:`61b7e4f` — SmsTemplates+IUserNotificationService+UserNotificationService با packageName |
| ~~F3~~ | ~~Background Services + PackageId~~ | ✅ بررسی‌شده | بدون تغییر — هر ۳ worker از قبل per-package صحیح کار می‌کنند |
| ~~F4~~ | ~~CSV exports + ستون پکیج~~ | ✅ انجام‌شده | CMS:`61b7e4f` BO:`92c9922` — proto+handler+CSV برای ManualPayments/WithdrawalRequests |
| ~~F5~~ | ~~SystemConfiguration per-package~~ | ✅ بررسی‌شده | بدون تغییر — مقادیر per-package قبلاً به Package entity منتقل شده‌اند |
| ~~F6~~ | ~~MagicWalletChargePage hardcoded~~ | ✅ انجام‌شده | CMS:`61b7e4f` FO:`ecc4f44` — magic_multiplier+magic_max_credit از API، داشبورد "شارژ چند‌برابری" |
| ~~F7~~ | ~~Validators async per-package~~ | ✅ انجام‌شده | CMS:`61b7e4f` FO:`ecc4f44` — SystemConstants.WalletMaxSafeAmount (10B) حصار ایمنی، سقف واقعی per-package در هندلر |
| ~~F8~~ | ~~آستانه موجودی ورود به Magic (Q24)~~ | ✅ انجام‌شده | CMS:`a1024a3``Balance <= 1_000_000` |
| ~~F9~~ | ~~SP Worker — مدیریت خودکار SP (Q26)~~ | ✅ انجام‌شده | CMS:`a1024a3``StoredProcedureDeploymentService` |
| ~~F10~~ | ~~History Tables — یکسان‌سازی + خودکار (Q27)~~ | ✅ انجام‌شده | CMS:`fdbb91d`+`10d2ca2` — IHasHistory + Interceptor + RenameTable migration |
| ~~F11~~ | ~~UI Guidance — آموزش و هشدار (Q28)~~ | ✅ انجام‌شده | FO:`474d364` BO:`6939780` — ۱۳ صفحه MudAlert |
> ✅ **F1-F11 همه پیاده‌سازی شدند.**
---
## ضمیمه: ۳۰ تصمیم بیزینسی (Q1–Q30)
### پیاده‌شده (Q1Q23):
| # | تصمیم | وضعیت |
|---|-------|-------|
| Q1 | باگ DiscountBalance → فیکس | ✅ `8b9c317` |
| Q2 | ادغام ۳ مسیر پرداخت → Generic | ✅ `ccb938e` + `8446e0e` |
| Q3 | پکیج نقره‌ای + پایه — داینامیک | ✅ `ae92ab8` + `a9cd2fd` |
| Q4 | ActivationFee یک فیلد (حذف GiftValue) | ✅ `ae92ab8` |
| Q5 | DiscountMultiplier داینامیک | ✅ `8e5c7c5` |
| Q6 | Migration کاربران فعلی → پکیج پایه | ✅ `a9cd2fd` |
| Q7 | خرید N بار بعد تکمیل چرخه | ✅ `fe3edd1` + `8e5c7c5` |
| Q8 | Commission Pool جدا per-package | ✅ `607f791` |
| Q9 | MagicWallet Multiplier داینامیک | ✅ `8e5c7c5` |
| Q10 | دایا = پکیج پایه (نه طلایی) | ✅ `ccb938e` |
| Q11 | فیچرها داینامیک per-package | ✅ `dcd1135` |
| Q12 | MaxBalancesPerLeg per-package | ✅ `607f791` |
| Q13 | MaxNetworkLevel per-package | ✅ `607f791` |
| Q14 | MagicWalletMaxDeposit per-package | ✅ `ae92ab8` |
| Q15 | MagicWalletMaxCredit per-package | ✅ `ae92ab8` |
| Q16 | NetworkWeeklyBalance + PackageId | ✅ `ae92ab8` |
| Q17 | گزارش FO breakdown per-package | ✅ `a956cb9` |
| Q18 | گزارش BO فیلتر per-package | ✅ `8be98ae` |
| Q19 | قرارداد فقط یک بار | ✅ `1ac2366` |
| Q20 | فیچر DIFF/تفاضل | ✅ `1ac2366` |
| Q21 | First/Last ActivationDate | ✅ `ae92ab8` |
| Q22 | تشخیص هفته از LastActivationDate | ✅ `607f791` |
| Q23 | Carryover strictly per-package | ✅ `607f791` |
### تصمیمات v6 (Q24–Q30) — ✅ تکمیل‌شده:
| # | تصمیم | وضعیت | کامیت |
|---|-------|-------|-------|
| Q24 | آستانه موجودی ≤ ۱,۰۰۰,۰۰۰ ریال (ورود Magic + خرید مجدد) | ✅ | CMS:`a1024a3` |
| Q25 | DayaLoans فقط پکیج پایه — تایید (بدون تغییر کد) | ✅ تایید | — |
| Q26 | SP Worker — auto-deploy با checksum (IHostedService) | ✅ | CMS:`a1024a3` |
| Q27 | History Tables — PackageHistory + CycleHistory + IHasHistory + Interceptor + Rename UserWalletChangeLog→UserWalletHistory | ✅ | CMS:`fdbb91d`+`10d2ca2` |
| Q28 | UI Guidance — ۱۳ صفحه MudAlert آموزشی/هشداری در FO/BO | ✅ | FO:`474d364` BO:`6939780` |
| Q29 | شرط EXIT Magic — تایید: آخرین پکیج فعال (بدون تغییر کد) | ✅ تایید | — |
| Q30 | Carryover — تایید: توضیح مستند شد (بدون تغییر کد) | ✅ تایید | — |
---
*آخرین بروزرسانی: ۱۰ اسفند ۱۴۰۴ — v7: F1-F7 همه تکمیل‌شده ✅ | ۵۱+ کامیت (۲۱ CMS + ۹ FO + ۷ BO + ۱۴+ docs) | NuGet v0.0.189 | Notifications+PackageName, CSV ستون پکیج, Dynamic MagicWallet, SystemConstants validators*
+356
View File
@@ -0,0 +1,356 @@
# گزارش ممیزی صحت داده‌های دیتابیس CMS
**تاریخ بررسی:** 1405/01/28 (2026-04-17)
**فایل بکاپ:** `dbbkup/CMS-20260417.sql` (4.9MB, 21,397 خط)
**تعداد جداول:** ~47 جدول | **تعداد کاربران:** 115 | **تعداد سفارشات:** 72
---
## فهرست مطالب
1. [خلاصه اجرایی](#خلاصه-اجرایی)
2. [اصلاحیه مهم — PaymentStatus](#اصلاحیه-مهم)
3. [دسته ۱ — ورود دستی / مهاجرت دیتا](#دسته-۱--ورود-دستی--مهاجرت-دیتا)
4. [دسته ۲ — باگ‌های کد](#دسته-۲--باگهای-کد)
5. [دسته ۳ — وضعیت Stored Procedure‌ها](#دسته-۳--وضعیت-stored-procedureها)
6. [آمار کلی جداول](#آمار-کلی-جداول)
7. [خلاصه مالی](#خلاصه-مالی)
8. [اقدامات پیشنهادی](#اقدامات-پیشنهادی)
---
## خلاصه اجرایی
بکاپ دیتابیس CMS در تاریخ 17 آوریل 2026 تحلیل شد. تحلیل شامل بررسی صحت داده‌ها، ارجاعات خارجی (FK)، زنجیره مالی، و تطبیق با کد سورس C# و Stored Procedure‌ها بود.
**وضعیت کلی:** سیستم در حال مهاجرت از ساختار استاتیک به پکیج‌محور بوده. بخش عمده مشکلات ناشی از ورود دستی داده و مهاجرت سیستم دایا است. چند باگ کد نیز در SP کمیسیون و Worker دایا شناسایی شد.
---
## اصلاحیه مهم
> **`PaymentStatus=0` در enum کد یعنی `Success` نه `Pending`!**
>
> ```csharp
> // PaymentStatus.cs
> Success = 0,
> Reject = 1,
> Pending = 2
> ```
>
> بنابراین تمام 72 سفارش واقعاً **موفق** هستند. این مشکل نیست.
---
## دسته ۱ — ورود دستی / مهاجرت دیتا
### 1A) موجودی 56M بدون فلگ `HasReceivedDayaCredit`
**شدت:** 🟠 متوسط
**علت:** ورود دستی / مهاجرت
- **23 کاربر** دقیقاً 56,000,000 ریال در `Balance` دارند ولی `HasReceivedDayaCredit = 0`
- فقط **16 کاربر** از طریق Daya Worker صحیح اعتبار گرفتند (`HasReceivedDayaCredit = 1`)
- بقیه احتمالاً دستی شارژ شدند بدون ثبت تاریخچه
**کاربران آسیب‌پذیر:**
| UserId | نام | Balance | HasReceivedDayaCredit |
|--------|-----|---------|----------------------|
| 51 | مرتضی اینالو | 56,000,000 | 0 |
| 58 | وحید حق‌گو | 56,000,000 | 0 |
| 87 | امیررضا محمدی | 56,000,000 | 0 |
| 43 | کریم خادمی | 56,000,000 | 0 |
| 52 | حمیدرضا اسمعیلی | 56,000,000 | 0 |
| 88 | کریم رعیت‌پیشه | 56,000,000 | 0 |
| 91 | رحیم رعیت‌پیشه | 56,000,000 | 0 |
| 93 | ریحانه سادات هاشمی‌نصر | 56,000,000 | 0 |
| 99 | هستی خادمی | 56,000,000 | 0 |
| 110 | علی وفائی | 56,000,000 | 0 |
| 113 | کاوس بیگ‌اینالو | 56,000,000 | 0 |
| 119 | علیرضا کریمی‌پیروز | 56,000,000 | 0 |
| 122 | ابوالقاسم عابدی | 56,000,000 | 0 |
| 123 | ناصر کریمی‌پیروز | 56,000,000 | 0 |
| 124 | محمدرضا باغجری | 56,000,000 | 0 |
| 126 | امیرعباس میرزایی | 56,000,000 | 0 |
| 138 | سیما اکبرزاده | 56,000,000 | 0 |
| 139 | مسعود توسلیان | 56,000,000 | 0 |
| 142 | صغری شبانکاره | 56,000,000 | 0 |
| 170 | لیلا خدارحمی | 56,000,000 | 0 |
| 172 | مهرافشان زاهدنیا | 56,000,000 | 0 |
| 175 | ناهید حسن‌زاده | 56,000,000 | 0 |
| 176 | مهریدخت میکانیکی | 56,000,000 | 0 |
---
### 1B) کد ملی تکراری — اکانت‌های تستی
**شدت:** 🟡 پایین
**علت:** ورود دستی / تست
| کد ملی | تعداد اکانت | UserIdها | نام |
|--------|------------|----------|-----|
| مشترک #1 | 6 | 9, 11, 12, 13, 40, 41 | مهدی مرجانی |
| مشترک #2 | 3 | 7, 8, 10 | مهدی صیفی |
| مشترک #3 | 2 | 42, 43 | کریم خادمی |
| مشترک #4 | 2 | 50, 51 | مرتضی اینالو |
| مشترک #5 | 2 | 120, 190 | عسلی |
- شماره موبایل تکراری: `09038888074` بین کاربران 120 و 190
---
### 1C) `NetworkInfos` خالی — مهاجرت صحیح انجام شده
**شدت:** ✅ مشکل نیست
جدول `NetworkInfos` خالی است چون داده‌های شبکه به فیلدهای مستقیم `Users` مهاجرت شدند:
- `Users.NetworkParentId` ← FK به والد شبکه
- `Users.LegPosition` ← Left(0) / Right(1)
مهاجرت در `20250601_MigrateParentIdToNetworkParentId.sql` انجام شده. entity `NetworkInfo` در C# وجود ندارد. SP‌ها هم از `Users.NetworkParentId` استفاده می‌کنند.
---
### 1D) `PasswordHash = NULL` برای تمام 115 کاربر
**شدت:** 🟡 نیاز به بررسی
**علت:** احتمالاً بکاپ شامل فیلد پسورد نشده، یا سیستم OTP/موبایل استفاده می‌کند
---
### 1E) دوره‌های عضویت باشگاه — `PaidAmount = 0`
**شدت:** 🟡 نیاز به بررسی
- 87 دوره `ClubMembershipCycles` همه `PaidAmount = 0`
- ممکن است عضویت باشگاه خودکار با خرید پکیج فعال شود (نه پرداخت جداگانه)
---
## دسته ۲ — باگ‌های کد
### 2A) تراکنش‌های تکراری دایا — Race Condition در `CheckAndProcessDayaLoansCommandHandler`
**شدت:** 🔴 بحرانی
**فایل:** `CMSMicroservice.Application/DayaLoanCQ/Commands/CheckAndProcessDayaLoans/CheckAndProcessDayaLoansCommandHandler.cs`
**یافته‌ها:**
- **109 رکورد `DayaLoanContracts`** ولی فقط **16 کاربر** `HasReceivedDayaCredit=1`
- **64 تراکنش** با «دریافت اعتبار دایا» ساخته شده ولی فقط **11 رکورد `UserPackagePurchases`**
- تراکنش‌ها با `RefId` منحصربه‌فرد ساخته شدند (مثل `C4_T8579002`) — همه در `2025-11-19 01:24:24` ایجاد شدند
**تحلیل ریشه‌ای:**
- Daya Worker (Hangfire هر 15 دقیقه) احتمالاً برای بعضی کاربران **چند بار** اجرا شده
- `CreateTransaction` و `DayaLoanContract` ساخته شده ولی `HasReceivedDayaCredit=true` ست نشده (exception بعد از SaveChanges اول ولی قبل از SaveChanges دوم)
- یا: چون همه در یک لحظه ساخته شدند (`2025-11-19 01:24:24`)، ممکن است **یک بار bulk import دستی** بوده
**ریسک:** کاربرانی که `HasReceivedDayaCredit=0` دارند ممکن است **دوباره** از Worker اعتبار بگیرند.
---
### 2B) SP `sp_CalculateWeeklyCommissionPool` — `DistributedAmount` آپدیت نمی‌شود
**شدت:** 🔴 بحرانی
**فایل:** `dbbkup/CMS-20260417.sql` خط ~18830
در Step 10 (آپدیت نهایی Pool):
```sql
-- کد فعلی (باگ‌دار):
UPDATE CMS.WeeklyCommissionPools
SET
IsCalculated = 1,
CalculatedAt = @CalculatedAt,
TotalBalances = @TotalBalances,
ValuePerBalance = @ValuePerBalance,
LastModified = @CalculatedAt,
LastModifiedBy = 'SP'
WHERE Id = @PoolId;
```
**مشکل:** فیلد `DistributedAmount` **هرگز مقداردهی نمی‌شود** و 0 باقی می‌ماند.
**نتیجه در دیتا:**
- 15 استخر، مجموع `TotalPoolAmount = 2,016,000,000` ریال
- همه `DistributedAmount = 0`
- ولی 46 پرداخت واقعاً ثبت و به `NetworkBalance` اضافه شدند
---
### 2C) `UserWalletChangeLogs` خالی
**شدت:** 🟠 متوسط
**فایل:** SP Step 9 + `CalculateWeeklyCommissionPoolCommandHandler.cs`
- SP باید در Step 9 لاگ تغییرات کیف‌پول را در `UserWalletChangeLogs` ذخیره کند
- جدول **صفر رکورد** دارد
- **احتمال 1:** SP هرگز Step 9 را درست اجرا نکرده
- **احتمال 2:** ORM Strategy (نه SP) استفاده شده و آن `UserWalletChangeLogs` نمی‌نویسد
- **احتمال 3:** لاگ‌ها در حین ForceRecalculate حذف شدند
---
### 2D) `WalletHistory.ChangeType = NULL` در تمام 239 رکورد
**شدت:** 🟠 متوسط
**فایل:** `UserOrderService.cs` و `PackageService.cs` — هرجا `UserWalletHistory` ساخته می‌شود
- فیلد `ChangeType` هرگز ست نمی‌شود
- کد از `IsIncrease` (bool) برای تفکیک واریز/برداشت استفاده می‌کند
- `ChangeType` احتمالاً فیلد قدیمی deprecated شده
---
### 2E) `NetworkWeeklyBalances.WeeklyCommissionPoolId = NULL` (805 رکورد)
**شدت:** 🟡 پایین
- SP مقدار `WeeklyPoolContribution = 0` ثبت می‌کند و PoolId ست نمی‌شود
- ارتباط بین `NetworkWeeklyBalances` و `WeeklyCommissionPools` از طریق `WeekDefinitionId` برقرار است، نه FK مستقیم
- **عملاً مشکل عملکردی ایجاد نمی‌کند** ولی tracking سخت‌تر می‌شود
---
### 2F) 30 سفارش کیف‌پولی — بررسی WalletHistory
**شدت:** 🟠 نیاز به تأیید
- 30 سفارش با `PaymentMethod=1` (Wallet) ثبت شدند
- اولین بررسی نشان داد «هیچ برداشتی ثبت نشده» — **اما** این بررسی بر اساس `ChangeType` بود که همه NULL هستند
- **باید بر اساس `IsIncrease=0` (false = decrease)** دوباره بررسی شود
- `SubmitShopBuyOrder()` در کد `UserWalletHistory` می‌سازد — احتمالاً رکوردها وجود دارند ولی `ChangeType` NULL است
---
## دسته ۳ — وضعیت Stored Procedure‌ها
### `GetNetworkTree`
| آیتم | وضعیت |
|------|--------|
| از `Users.NetworkParentId` استفاده می‌کند | ✅ صحیح (بعد از مهاجرت) |
| JOIN با `ClubMemberships` | ✅ صحیح |
| `MAXRECURSION 0` | ✅ صحیح |
| فیلتر `IsDeleted = 0` | ✅ صحیح |
### `sp_CalculateWeeklyBalances`
| آیتم | وضعیت |
|------|--------|
| پارامتر `@PackageId` از `Packages.IsBasePackage` | ✅ صحیح |
| `MaxBalancesPerLeg` و `MaxNetworkLevel` از Package | ✅ صحیح |
| Carryover از هفته قبل | ✅ صحیح |
| CTE recursive برای چپ/راست | ✅ صحیح |
| `TotalBalances = MIN(left, right)` | ✅ صحیح |
| `SubordinateBalances` محاسبه | ✅ صحیح |
| 805 رکورد تولید شده | ✅ کار می‌کند |
### `sp_CalculateWeeklyCommissionPool`
| آیتم | وضعیت |
|------|--------|
| `ValuePerBalance = TotalPoolAmount / TotalBalances` | ✅ صحیح |
| ایجاد `UserCommissionPayouts` | ✅ صحیح (46 رکورد) |
| ثبت `CommissionPayoutHistories` | ✅ صحیح |
| شارژ `NetworkBalance` کیف‌پول | ✅ صحیح |
| آپدیت `DistributedAmount` در Pool | ❌ **انجام نمی‌شود** |
| ثبت `UserWalletChangeLogs` | ⚠️ نامشخص |
| ForceRecalculate — Revert | ✅ منطق صحیح |
---
## آمار کلی جداول
| جدول | تعداد | وضعیت |
|------|--------|--------|
| Users | 115 | |
| UserWallets | 115 | |
| UserWalletHistories | 239 | ChangeType همه NULL |
| UserWalletChangeLogs | 0 | ⚠️ خالی |
| UserOrders | 72 | همه PaymentStatus=0 (Success) |
| FactorDetails | 177 | |
| Transactions | 175 | 64 تراکنش دایا |
| PaymentTransactions | 22 | فقط DiscountOrders + شارژ |
| Products | 163 | |
| Categories | 14 | |
| InventoryItems | 175 | |
| StockMovements | 242 | 11 chain issue |
| ClubMemberships | 88 | همه IsActive=1 |
| ClubMembershipCycles | 87 | همه PaidAmount=0 |
| UserClubFeatures | 249 | |
| NetworkInfos | 0 | ✅ deprecated — مهاجرت شده |
| NetworkWeeklyBalances | 805 | PoolId همه NULL |
| WeeklyCommissionPools | 15 | DistributedAmount همه 0 |
| UserCommissionPayouts | 46 | Status=3, مبالغ کلان |
| WeekDefinitions | 59 | |
| Packages | 2 | Base=56M, Secondary=5.6M |
| DayaLoanContracts | 109 | |
| UserPackagePurchases | 11 | |
| DiscountOrders | 13 | |
| DiscountOrderDetails | 14 | |
| DiscountCategories | 8 | |
| OrderVATs | 44 | ✅ محاسبات صحیح |
| UserAddresses | 127 | |
| Roles | 3 | user, admin, Administrator |
| UserRoles | 119 | 115 user + 2 admin + 2 Administrator |
| ProductImages | 4 | |
| ShippingMethods | 0 | ⚠️ خالی |
| SitePages | 0 | ⚠️ خالی |
| SystemConfigurations | 0 | ⚠️ خالی |
| Coupons | 0 | ⚠️ خالی |
| ProductProperties | 0 | ⚠️ خالی |
---
## خلاصه مالی
### موجودی‌های کل سیستم
| فیلد | مبلغ (ریال) |
|------|-------------|
| مجموع `Balance` کل کیف‌پول‌ها | 2,548,684,394 |
| مجموع `NetworkBalance` (کمیسیون) | 453,599,993 |
| مجموع `DiscountBalance` (تخفیف) | 7,326,400,000 |
| مجموع سفارشات (UserOrders) | 1,039,410,812 |
| مجموع استخر کمیسیون (WeeklyPools) | 2,016,000,000 |
### پکیج‌ها
| Package | قیمت | IsBase | MaxBalancesPerLeg | MaxNetworkLevel | DiscountMultiplier |
|---------|-------|--------|-------------------|-----------------|-------------------|
| Package 1 | 56,000,000 | ✅ | 300 | 1,000,000 | 2.0 |
| Package 4 | 5,600,000 | ❌ | 30 | 1,000,000 | 2.0 |
### ارجاعات شکسته (FK)
| ارجاع | تعداد |
|-------|--------|
| FactorDetails → OrderId ناموجود | 2 (DetailId=22,23 → OrderId=21) |
| InventoryItems ≠ آخرین StockMovement | 3 |
| StockMovement chain breaks | 11 |
---
## اقدامات پیشنهادی
### اولویت بالا (انجام ندهید تا بررسی بیشتر)
1. **فیکس SP `sp_CalculateWeeklyCommissionPool`:** اضافه کردن `DistributedAmount` به UPDATE نهایی
2. **بررسی Daya Worker:** race condition در `CheckAndProcessDayaLoansCommandHandler` — ممکن است تراکنش تکراری بسازد
3. **23 کاربر با 56M بدون فلگ دایا:** تعیین اینکه آیا دستی شارژ شدند یا از Worker — سپس اصلاح `HasReceivedDayaCredit`
### اولویت متوسط
4. **WalletHistory ChangeType:** تأیید اینکه deprecated شده و `IsIncrease` جایگزین است
5. **UserWalletChangeLogs خالی:** بررسی اینکه ORM Strategy استفاده شده یا SP Strategy
6. **اکانت‌های تکراری:** تصمیم‌گیری درباره 13 اکانت تکراری (حذف/ادغام)
### اولویت پایین
7. **جداول خالی:** SystemConfigurations, ShippingMethods, SitePages — آیا باید از seed پر شوند؟
8. **StockMovement chain issues:** 11 ناسازگاری — آیا از ورود دستی موجودی بوده؟
---
*این گزارش فقط مستندات یافته‌ها است. هیچ تغییری در کد یا دیتابیس اعمال نشده است.*
+3 -3
View File
@@ -4,7 +4,7 @@
> **تاریخ تجمیع:** اسفند ۱۴۰۴
> **تعداد فایل‌های مبدأ:** ۵۳ فایل (~۳۲,۰۰۰ خط)
> **تعداد فایل‌های نهایی:** ۲۲ فایل (۱۵ اصلی + ۷ roadmap/business)
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (تحول پکیج‌بیس فاز ۰-۴ + فعال‌سازی درگاه + مرج Production)
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (فاز ۱۰: DataMigration + EF Staging + PackagePurchaseDialog + UI Fixes | NuGet v0.0.189)
---
@@ -81,10 +81,10 @@ totalDoc/
|---|------|--------|--------|
| R1 | [MAGIC-WALLET-SPEC](../roadmap/MAGIC-WALLET-SPEC.md) | کیف‌پول جادویی — مشخصات | State Machine، ضریب ×2.5، سقف 100M، قوانین، API، مدل داده |
| R2 | [MAGIC-WALLET-PLAN](../roadmap/MAGIC-WALLET-PLAN.md) | کیف‌پول جادویی — پلن | ۶ فاز، **فاز 1-6 تکمیل ✅** |
| R3 | [PACKAGE-TRANSFORMATION-TASKS](../roadmap/PACKAGE-TRANSFORMATION-TASKS.md) | تحول پکیج‌بیس — تسک‌ها | ۵ فاز، **فاز 0-4 تکمیل ✅**، فاز 5-6 در انتظار |
| R3 | [PACKAGE-TRANSFORMATION-TASKS](../roadmap/PACKAGE-TRANSFORMATION-TASKS.md) | تحول پکیج‌بیس — تسک‌ها | ۱۰ فاز، **فاز 0-10 تکمیل ✅**، تست + deploy در انتظار |
| R4 | [PACKAGE-TRANSFORMATION-UX](../roadmap/PACKAGE-TRANSFORMATION-UX.md) | تاثیر بر UX | تحلیل تاثیر بر FrontOffice + BackOffice |
| R5 | [FEATURE-BACKLOG](../roadmap/FEATURE-BACKLOG.md) | بکلاگ فیچرها | ۱۲ RPC آماده بدون UI، اولویت‌بندی‌شده |
| R6 | [BIZ-PACKAGE-BASED-SYSTEM](../business/BIZ-PACKAGE-BASED-SYSTEM.md) | طراحی سیستم پکیج‌بیس | v5، ۲۳ تصمیم + ۵۱ تغییر + ۴۴ سایدافکت |
| R6 | [BIZ-PACKAGE-BASED-SYSTEM](../business/BIZ-PACKAGE-BASED-SYSTEM.md) | طراحی سیستم پکیج‌بیس | v6، ۳۰ تصمیم (Q1-Q30) + ۵۱ تغییر + ۴۴ سایدافکت |
---
+167 -7
View File
@@ -1,7 +1,7 @@
# 📜 تاریخچه کارهای انجام‌شده
> **همه فعالیت‌های پروژه به صورت بولت با توضیح یک‌خطی و درصد تکمیل**
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: فاز ۸ePer-Package Commission Reports | NuGet v0.0.187)
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: فاز ۱۱فیکس‌های پرداخت ZarinPal + امنیت Callback URL + اصلاح تومان/ریال)
---
@@ -9,13 +9,13 @@
| حوزه | تعداد آیتم | تکمیل‌شده | درصد کل |
|------|-----------|----------|---------|
| **BackOffice** | 61 | 61 | **100%** |
| **FrontOffice** | 40 | 40 | **100%** |
| **CMS Core** | 72 | 72 | **100%** |
| **Package-Based System** | 35 | 35 | **100%** |
| **BackOffice** | 67 | 67 | **100%** |
| **FrontOffice** | 60 | 60 | **100%** |
| **CMS Core** | 89 | 89 | **100%** |
| **Package-Based System** | 52 | 52 | **100%** |
| **Deployment** | 22 | 21 | **95%** |
| **Migration** | 16 | 16 | **100%** |
| **مجموع** | **246** | **245** | **99%** |
| **Migration** | 21 | 21 | **100%** |
| **مجموع** | **311** | **310** | **99.5%** |
---
@@ -106,6 +106,12 @@
- ✅ دکمه پرداخت شارژ کیف‌پول جادویی — MagicWallet.razor فعال شد (حذف «بزودی»)
- ✅ دکمه‌های پرداخت مستقیم خرید پکیج — Index.razor هر دو شاخه فعال شدند (حذف «بزودی»)
### فیکس‌های پرداخت و UX (اسفند ۱۴۰۴ — Phase 11)
-**صفحه موفقیت پرداخت**`TransactionId` بجای `RefId` + موجودی واقعی + `Href="/profile"` (FO:`5ded91a`)
-**حذف دوبار ×۱۰** — FO مستقیم تومان ارسال می‌کند، CMS/ZarinPal ×۱۰ می‌کند (FO:`2f9ef15`)
-**حذف CallbackUrl از درخواست**`Index.razor.cs` و `Checkout.razor.cs` دیگر URL ارسال نمی‌کنند (FO:`2b1dc47`)
- ✅ ۳ کامیت، ۷ فایل تغییر
### Phase 8a+8c: Checkout + Package Pages (اسفند ۱۴۰۴)
- ✅ Checkout wire-up — مهاجرت به `CustomerPurchasePackageAsync` (حذف dead code قدیمی)
- ✅ PackageDetail: `GetPackageAsync``GetCustomerPackageDetailsAsync` — features/specs از API (نه hardcoded)
@@ -114,6 +120,75 @@
- ✅ NuGet bump 0.0.182 → 0.0.186 + local feed source
- ✅ فیکس GwUrl پروداکشن — تصحیح از cms.kbs1.ir به cms.kbs2.ir
### Phase 10a: PackagePurchaseDialog — دیالوگ داینامیک خرید پکیج (اسفند ۱۴۰۴)
-`PackagePurchaseDialog.razor` — دیالوگ ۲ مرحله‌ای: مرحله ۱ = کاشی‌های پکیج (responsive grid)، مرحله ۲ = انتخاب روش پرداخت
- ✅ حذف دیالوگ inline خرید «پکیج پایه» از `Index.razor` — جایگزین با دیالوگ داینامیک
- ✅ بارگذاری پکیج‌ها از `PackageService.GetAllPackagesAsync()` — نمایش عنوان + قیمت + ویژگی‌ها
- ✅ پشتیبانی از ۲ روش پرداخت: مستقیم (درگاه بانکی) + اعتبار دایا (فقط پکیج پایه + دور اول)
- ✅ CSS کلاس‌های جدید: `.pkg-tile`, `.pkg-tile-badge`, `.pkg-payment-option`
- ✅ کامیت: `a3681a8` (FO)
### Phase 10b: ۴ فیکس UI پکیج (اسفند ۱۴۰۴)
-**Toman/Rial**: قیمت از سرور به ریال ← `FormattedPrice` حالا `Price / 10` برای نمایش صحیح تومان
-**لیبل**: «ضریب تخفیف» → «ضریب اعتبار» (دیالوگ + صفحه لیست پکیج‌ها)
-**دکمه بازگشت**: وجود داشت (`ArrowForward` + `BackToList`) — تأیید عملکرد
-**HTML Description**: `@((MarkupString)pkg.Description)` بجای متن ساده
- ✅ کامیت: `3c1a8ff` (FO)
### Phase 11: فیکس‌های پرداخت + تومان/ریال + امنیت Callback URL (اسفند ۱۴۰۴)
#### 11a: اصلاح مدل تومان/ریال (CMS+FO)
> **تصحیح مهم:** دیتابیس به **تومان** ذخیره می‌کند نه ریال. فقط درگاه ZarinPal ریال نیاز دارد (×۱۰).
-`ZarinPalPaymentService.InitiatePaymentAsync` — مبلغ ×۱۰ تبدیل به ریال فقط هنگام ارسال به ZarinPal
-`ZarinPalPaymentService.VerifyPaymentWithAmountAsync` — مبلغ ×۱۰ هنگام verify
- ✅ FrontOffice نمایش مستقیم مبلغ تومان (بدون `/10`) — فیکس `MagicWallet.razor`, `ChargeDiscountWallet.razor.cs`
- ✅ حذف `Price / 10` اضافی در `ClubMembershipContractDialog.razor`
#### 11b: فیکس ZarinPal Verify — رفع خطای Code=-1 (CMS:`721661a`)
> **باگ:** `VerifyPaymentAsync` با ۲ آرگومان مبلغ صفر (0) ارسال می‌کرد → ZarinPal Code=-1 برمی‌گرداند
-`PackageService` — lookup `PaymentTransaction.Amount` + استفاده از overload ۳ آرگومانه
-`TransactionsService` — همان فیکس
-`VerifyDiscountWalletChargeCommandHandler` — مبلغ از `PaymentTransaction` + رفع کپی‌پیست باگ
-`VerifyPackagePurchaseCommandHandler` — مبلغ از `PaymentTransaction`
-`IPaymentGatewayService` — default impl ۳ آرگومانه با `NotImplementedException`
-`MockPaymentGatewayService` + `DayaPaymentService` — اضافه overload ۳ آرگومانه
-۷ فایل تغییر
#### 11c: بهبود صفحه موفقیت پرداخت (FO:`5ded91a`)
-`PaymentCallback.razor` — نمایش `TransactionId` بجای `RefId` برای کد رهگیری
- ✅ نمایش موجودی واقعی کیف‌پول از `WalletService.GetBalancesAsync()` (نه مقدار ثابت)
- ✅ دکمه بازگشت: `Href="/profile"` بجای `history.back()` (جلوگیری از حلقه بازگشت به درگاه)
#### 11d: حذف دوبار ×۱۰ شارژ کیف‌پول (FO:`2f9ef15`)
> **باگ:** FO مبلغ تومان را ×۱۰ تبدیل به ریال می‌کرد، سپس CMS/ZarinPal دوباره ×۱۰ → مبلغ ۱۰۰ برابر
-`MagicWallet.razor.cs` — حذف تبدیل ×۱۰ (ارسال مستقیم تومان)
-`MagicWallet.razor` — فیکس Max و فیلتر preset مبالغ
-`ChargeDiscountWallet.razor.cs` — حذف تبدیل ×۱۰
-`ClubMembershipContractDialog.razor` — حذف `Price/10` اضافی
- ✅ ۴ فایل تغییر
#### 11e: فیکس مسیر Callback کیف‌پول (CMS:`ed2b20a`)
-`PaymentCallbackController` — مسیر redirect از `/magic-wallet` به `/profile/magic-wallet`
- ✅ ایجاد `appsettings.Development.json` — URL‌های محلی (`localhost:32846` و `localhost:5268`)
- ✅ تصحیح کامنت‌های proto: «ریال» → «تومان»
#### 11f: امنیت Callback URL — حذف از ورودی کاربر (CMS:`0107308`, FO:`2b1dc47`)
> **اصلاح امنیتی:** هیچ callback URL نباید از ورودی کاربر بیاید — همه از `appsettings.json` خوانده شوند
-`PackageService` — خواندن `FrontOfficeBaseUrl` از `IConfiguration` بجای `request.CallbackUrl`
-`TransactionsService` — همان فیکس، خواندن از config
- ✅ تأیید: `MagicWallet` و `DiscountWallet` از قبل از `CmsBaseUrl` config می‌خوانند ✅
- ✅ تأیید: `DiscountShop PlaceOrder` از قبل از `CmsBaseUrl` config می‌خواند ✅
- ✅ FO: حذف `CallbackUrl` از `Index.razor.cs` و `Checkout.razor.cs`
- ✅ جدول Callback URL‌ها:
| فلو | Callback URL | منبع |
|-----|-------------|------|
| خرید پکیج | `FrontOfficeBaseUrl/profile/payment-callback?orderId=X` | config |
| کیف‌پول جادویی | `CmsBaseUrl/api/wallet/verify-magic-charge` | config |
| کیف‌پول اعتباری | `CmsBaseUrl/api/wallet/verify-discount-charge` | config |
| فروشگاه اعتباری | `CmsBaseUrl/api/payment/discount-order/callback?orderId=X` | config |
| تراکنش عمومی | `FrontOfficeBaseUrl/profile/payment-callback` | config |
### محتوا و ناوبری
- ✅ بلاگ — لیست + جزئیات + pagination
- ✅ صفحات سایت — About, Contact, FAQ, Terms, Privacy, Licenses
@@ -176,6 +251,14 @@
- ✅ اغنای پاسخ UserWalletService — join با جدول Users برای نمایش نام کاربر
- ✅ ارتقای Proto NuGet به نسخه 0.0.183
### فیکس‌های پرداخت و امنیت (اسفند ۱۴۰۴ — Phase 11)
-**ZarinPal Verify fix** — رفع باگ amount=0 در VerifyPaymentAsync (Code=-1) — ۳ آرگومانه overload
-**تصحیح مدل تومان/ریال** — DB به تومان ذخیره می‌کند، فقط ZarinPal ریال (×۱۰) نیاز دارد
-**Callback URL از config**`PackageService` و `TransactionsService` از `FrontOfficeBaseUrl` config می‌خوانند (نه از ورودی)
-**فیکس مسیر redirect**`/magic-wallet``/profile/magic-wallet` در PaymentCallbackController
-**appsettings.Development.json** — URL‌های محلی برای توسعه (CmsBaseUrl + FrontOfficeBaseUrl)
- ✅ کامیت‌ها: `721661a``ed2b20a``0107308`
### محتوا
- ✅ Blog CRUD — با pagination
- ✅ SitePage Settings — JSON typed
@@ -219,6 +302,21 @@
- ✅ Proto Package Unification — یک package مشترک
- ✅ Binary Tree Reconstruction — از سیستم قدیم
### DataMigration Tool (اسفند ۱۴۰۴)
- ✅ ابزار مستقل مهاجرت داده — .NET 9 Console + Dapper + Polly + Serilog
- ✅ Smart Retry Policy — فقط خطاهای transient SQL (deadlock, timeout, transport) — نه خطاهای منطقی
- ✅ FK Disable/Enable — `NOCHECK`/`CHECK` حول مهاجرت برای حل FK violation
- ✅ TruncateTargetTables — حل مشکل duplicate key (IX_ClubMembership_UserId)
- ✅ Fallback Table Name — اگر جدول مقصد rename شده (`UserWalletChangeLogs``UserWalletHistories`)
- ✅ PostMigration SQL — همه مراحل با `IF COL_LENGTH` / `OBJECT_ID` guard شده
- ✅ کامیت‌ها: `0e8c6fd``8385c90``31cc464`
### EF Migration — Staging (اسفند ۱۴۰۴)
- ✅ اعمال migrations روی DB استیجینگ KBS (`185.252.31.42,2019/KBS`) — موفق
- ✅ اعمال migrations روی DB اپلیکیشن (`194.5.195.53,31433/Foursat`) — موفق
- ✅ آخرین migration: `20260227024734_Q27_HistoryTables_And_RenameWalletHistory` (۵۵ migration مجموع)
- ✅ حل خطای لاگین `Invalid column name 'FirstActivationDate'` — دو DB مختلف بودند
---
## ۶. مستندات (100% کامل)
@@ -321,6 +419,65 @@
- ✅ FO: فیلتر و ستون پکیج در CommissionDashboard + WeeklyBalance
- ✅ NuGet: `0.0.186``0.0.187`
### Phase 9 — Q24-Q30 Business Decisions + History Infrastructure ✅
#### 9a: Q24+Q26 — Balance Threshold + SP Worker ✅ (CMS:`a1024a3`)
- ✅ Q24: آستانه موجودی `Balance <= 1_000_000` ریال برای ورود Magic و خرید مجدد (بجای `== 0`)
- ✅ Q26: `StoredProcedureDeploymentService` (IHostedService) — خواندن فایل‌های `.sql` از embedded resource، مقایسه checksum و اعمال خودکار در startup
#### 9b: Q27 — History Tables Entities ✅ (CMS:`fdbb91d`)
-`PackageHistory` entity — فیلدهای Old*/New* برای Price, ActivationFee, MagicMultiplier, MagicMaxDeposit, MaxBalancesPerLeg, IsActive
-`ClubMembershipCycleHistory` entity — فیلدهای Old*/New* برای IsCurrentCycle, MagicStartedAt, MagicCompletedAt
-`PackageAction` و `ClubMembershipCycleAction` enums
- ✅ EF Configurations + DbSets + Navigation Properties
#### 9c: Q28 — UI Guidance ✅ (FO:`474d364` BO:`6939780`)
- ✅ FrontOffice: ۷ صفحه با MudAlert (G1-G7) — Packages, Checkout, MyPackages, MagicWallet, Commission, Membership, ActivationSection
- ✅ BackOffice: ۶ صفحه با MudAlert (G8-G13) — PackageCRUD, ClubFeatures, ManualPayments, Commission Dashboard, UserPayouts, ClubMembers
#### 9d: Rename + History Interceptor + Migration ✅ (CMS:`10d2ca2`)
- ✅ تغییر نام `UserWalletChangeLog``UserWalletHistory` در ۵۴+ فایل (entities, configs, DTOs, commands, queries, protos, services)
- ✅ تغییر نام ۳۴ فایل و ۱۱ دایرکتوری
- ✅ تغییر نام proto: `userwalletchangelog.proto``userwallethistory.proto`
-`IHasHistory<T>` generic interface — متد `CreateHistorySnapshot` برای ثبت خودکار
-`HistoryTrackingSaveChangesInterceptor` — reflection-based، auto-fill Old* از OriginalValues
-`Package` implements `IHasHistory<PackageHistory>`
- ✅ EF Migration `Q27_HistoryTables_And_RenameWalletHistory`**RenameTable** (حفظ داده) + rename PK/FK/Index via sp_rename
- ✅ NuGet: `0.0.187``0.0.188`
### Phase 10 — استقرار + DataMigration + UI خرید پکیج ✅
#### 10a: DataMigration Tool ✅ (Local — بدون remote)
- ✅ ابزار مستقل مهاجرت داده — .NET 9 Console app + Dapper (bulk copy) + Polly (retry) + Serilog (logging)
- ✅ مهاجرت ۱۸ جدول از DB پروداکشن (`185.252.31.42,2019/Foursat`) به استیجینگ (`KBS`)
- ✅ Smart Retry — فقط خطاهای transient (deadlock/timeout/transport)، نه خطاهای منطقی
- ✅ FK Disable/Enable — `ALTER TABLE NOCHECK/CHECK CONSTRAINT` حول هر مهاجرت
- ✅ TruncateTargetTables — حل duplicate key (`IX_ClubMembership_UserId`) هنگام اجرای مجدد
- ✅ Fallback Table Name — جدول مقصد rename شده؟ (`UserWalletChangeLogs``UserWalletHistories`)
- ✅ PostMigration SQL — همه مراحل با `IF COL_LENGTH`/`OBJECT_ID` guard شده (سازگار با هر دو schema)
- ✅ کامیت‌ها: `0e8c6fd``8385c90` (MERGE fix) → `31cc464` (FK+truncate+PostMigration)
#### 10b: EF Migration Staging ✅
- ✅ اعمال ۵۵ migration روی DB استیجینگ KBS (`185.252.31.42,2019;Database=KBS`)
- ✅ اعمال ۵۵ migration روی DB اپلیکیشن (`194.5.195.53,31433;Database=Foursat`)
- ✅ آخرین migration: `20260227024734_Q27_HistoryTables_And_RenameWalletHistory`
- ✅ حل خطای لاگین: `Invalid column name 'FirstActivationDate'` — CMS به DB دیگری وصل بود
#### 10c: PackagePurchaseDialog — دیالوگ داینامیک خرید (FO:`a3681a8`)
-`PackagePurchaseDialog.razor` — دیالوگ ۲ مرحله‌ای جایگزین دیالوگ hardcoded «پکیج پایه»
- ✅ مرحله ۱: نمایش کاشی‌های پکیج (responsive grid 2-3 ستونه) با عنوان + قیمت + ویژگی‌ها + badge «پایه»
- ✅ مرحله ۲: انتخاب روش پرداخت (مستقیم + اعتبار دایا) با خلاصه پکیج انتخابی
- ✅ بارگذاری از `PackageService.GetAllPackagesAsync()` + `PackagePurchaseResult` record
- ✅ محدودیت دایا: فقط `SupportsDayaPurchase && PurchaseCycleCount == 0`
- ✅ CSS: `.pkg-tile`, `.pkg-tile-badge`, `.pkg-payment-option` در `site.css`
- ✅ NuGet: `0.0.188``0.0.189`
#### 10d: ۴ فیکس UI پکیج (FO:`3c1a8ff`)
-**Toman/Rial**: قیمت از سرور به ریال ← `FormattedPrice` حالا `Price / 10` برای نمایش صحیح تومان
-**لیبل**: «ضریب تخفیف» → «ضریب اعتبار» (دیالوگ + صفحه لیست پکیج‌ها)
-**دکمه بازگشت**: وجود داشت (`ArrowForward` + `BackToList`) — تأیید عملکرد
-**HTML Description**: `@((MarkupString)pkg.Description)` بجای متن ساده
---
## ۸. Timeline (جدول زمانی)
@@ -340,3 +497,6 @@
| اسفند ۱۴۰۴ (هفته ۳) | 🚀 فعال‌سازی درگاه + بهبود UI ادمین + مرج پروداکشن | 97% |
| اسفند ۱۴۰۴ (هفته ۴) | 📦 تحول پکیج‌بیس فاز ۰-۶ (Domain → Migration → Business → Package → CRUD → Commission per-pkg → Deprecation) | 97% |
| اسفند ۱۴۰۴ (هفته ۵) | 📦 فاز 8e: گزارش‌های پورسانت per-package (Proto + CMS + BO + FO) | 98% |
| اسفند ۱۴۰۴ (هفته ۶) | 📦 فاز ۹: Q24-Q30 (آستانه + SP Worker + History Tables + UI Guidance + Rename + Interceptor + Migration) | 99% |
| اسفند ۱۴۰۴ (هفته ۷) | 📦 فاز ۱۰: DataMigration Tool + EF Staging + PackagePurchaseDialog + UI Fixes (Toman/Rial + لیبل + HTML) | 99.5% |
| اسفند ۱۴۰۴ (هفته ۸) | 💳 فاز ۱۱: فیکس ZarinPal Verify + اصلاح تومان/ریال + صفحه موفقیت + حذف دوبار ×۱۰ + امنیت Callback URL | 99.5% |
+8 -6
View File
@@ -1,7 +1,7 @@
# 🗺️ نقشه راه، ریسک‌ها و کارهای باقیمانده
> **Roadmap + Risk Register + Dependencies + Priorities**
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: تحول پکیج‌بیس فاز ۰-۶ + per-package commission + deprecation cleanup)
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: فاز ۱۱ — فیکس‌های پرداخت ZarinPal + امنیت Callback URL + تومان/ریال)
---
@@ -13,10 +13,10 @@
Core Platform ████████████████████████████████████████████████ 98%
Club System ████████████████████████████████████████████████ 98%
E-Commerce ████████████████████████████████████████████████ 98%
Payment ████████████████████████████████████████████████ 97%
Payment ████████████████████████████████████████████████ 99%
Magic Wallet ████████████████████████████████████████████████ 100%
Package-Based ██████████████████████████████████████████████░░ 93%
UI/UX █████████████████████████████████████████████░░░ 93%
Package-Based ██████████████████████████████████████████████░░ 97%
UI/UX █████████████████████████████████████████████░░░ 95%
Deployment ██████████████████████████████████████████████░░ 95%
Documentation ████████████████████████████████████████████████ 100%
```
@@ -209,11 +209,13 @@ DONE (اسفند ۱۴۰۴):
→ مرج پروداکشن هر ۳ ریپو (CMS + FO + BO) ✅
→ اجرای Migration روی پروداکشن ✅
→ 📦 تحول پکیج‌بیس فاز 0-6 ✅ (Domain → Migration → Business → Package → CRUD → Commission per-pkg → Deprecation)
→ 📦 فاز ۹: Q24-Q30 + History + Rename + Interceptor ✅
→ 📦 فاز ۱۰: DataMigration Tool + EF Staging + PackagePurchaseDialog + UI Fixes ✅
→ 💳 فاز ۱۱: فیکس ZarinPal Verify (amount=0) + تصحیح مدل تومان/ریال + صفحه موفقیت پرداخت + حذف دوبار ×۱۰ + امنیت Callback URL ✅
NOW (این ماه):
→ 📦 فاز 7: UI (کاشی‌های داینامیک + فیلتر پکیج + MyPackages)
→ EF Migration نهایی
→ تست کامل پروداکشن
→ فیکس باگ‌های کشف‌شده در تست
NEXT (فروردین):
→ Mobile Responsive (H2)
+92 -9
View File
@@ -1,13 +1,13 @@
# 🔄 نقشه‌راه تحول پکیج‌بیس — تسک‌های گام‌به‌گام
> **وضعیت:** در حال اجرا — **فاز ۰-۸f (UI تکمیل) ✅** | NuGet v0.0.188 | تست باقی‌مانده
> **تاریخ:** ۱۴۰۴/۱۲/۱۰
> **پیش‌نیاز:** [BIZ-PACKAGE-BASED-SYSTEM.md](../business/BIZ-PACKAGE-BASED-SYSTEM.md) **v3** (تکمیل پورسانت per-package)
> **وضعیت:** در حال اجرا — **فاز ۰-۱۰ (تکمیل کد + استقرار staging) ✅** | NuGet v0.0.189 | تست باقی‌مانده
> **تاریخ:** ۱۴۰۴/۱۲/۰۸
> **پیش‌نیاز:** [BIZ-PACKAGE-BASED-SYSTEM.md](../business/BIZ-PACKAGE-BASED-SYSTEM.md) **v6** (تکمیل Q24-Q30 + History + Rename)
> **هدف:** شکستن **۴۸+ تغییر** به تسک‌های اتمیک با ترتیب اجرا و وابستگی‌ها
> **کامیت‌ها:**
> CMS: `8b9c317`→`fe3edd1`→`ae92ab8`→`a9cd2fd`→`8e5c7c5`→`ccb938e`→`0002a5a`→`607f791`→`7176fe4`→`d19c569`→`469d97b`→`161f796`→`8446e0e`→`ce8e248`→`7554d70`→`aaaf7fc`→`dcd1135`
> FrontOffice: `b82cac4`→`71f391a`→`0bbc11e`→`d71d463`→`40882c8`→`a956cb9`→`3bffc13`
> BackOffice: `f1b0085`→`89f5241`→`c96377a`→`8be98ae`→`e020354`
> CMS: `8b9c317`→`fe3edd1`→`ae92ab8`→`a9cd2fd`→`8e5c7c5`→`ccb938e`→`0002a5a`→`607f791`→`7176fe4`→`d19c569`→`469d97b`→`161f796`→`8446e0e`→`ce8e248`→`7554d70`→`aaaf7fc`→`dcd1135`→`a1024a3`→`fdbb91d`→`10d2ca2`
> FrontOffice: `b82cac4`→`71f391a`→`0bbc11e`→`d71d463`→`40882c8`→`a956cb9`→`3bffc13`→`474d364`
> BackOffice: `f1b0085`→`89f5241`→`c96377a`→`8be98ae`→`e020354`→`6939780`
> ⚠️ **تغییرات v3:** پورسانت per-package، carryover مجزا، SP parameters داینامیک، گزارش‌دهی FO/BO per-package
@@ -36,9 +36,19 @@
└─→ 8d: Proto cleanup ✅ CMS:`7554d70` FO:`40882c8` BO:`c96377a`
└─→ 8e: Per-package reports ✅ CMS:`aaaf7fc` FO:`a956cb9` BO:`8be98ae`
└─→ 8f: UI completion ✅ CMS:`dcd1135` FO:`3bffc13` BO:`e020354`
└─→ مرحله ۵: تست + استقرار (۳ روز) ⬜
└─→ مرحله ۹: Q24-Q30 + History + Rename
├─→ 9a: Q24+Q26 (threshold+SP) ✅ CMS:`a1024a3`
├─→ 9b: Q27 History entities ✅ CMS:`fdbb91d`
├─→ 9c: Q28 UI Guidance ✅ FO:`474d364` BO:`6939780`
└─→ 9d: Rename+Interceptor+Mig ✅ CMS:`10d2ca2`
└─→ مرحله ۱۰: استقرار + DataMigration + UI
├─→ 10a: DataMigration Tool ✅ Local: `0e8c6fd`→`31cc464`
├─→ 10b: EF Staging Migrations ✅
├─→ 10c: PackagePurchaseDialog ✅ FO:`a3681a8`
└─→ 10d: UI Fixes (Rial/Toman+لیبل+HTML) ✅ FO:`3c1a8ff`
└─→ مرحله ۵: تست + نهایی ⬜
مسیر بحرانی: ۰→۱→۱.۵→۲→۳→۴→UI→۵ = ~۱۷ روز | انجام‌شده: ۰→8f (~۱۷ روز)
مسیر بحرانی: ۰→۱→۱.۵→۲→۳→۴→UI→۹→۱۰→۵ = ~۲۲ روز | انجام‌شده: ۰→10d (~۲۰ روز)
```
---
@@ -710,6 +720,79 @@ await connection.ExecuteAsync("CMS.sp_CalculateWeeklyBalances",
---
## مرحله ۹ — Q24-Q30 Business Decisions + History Infrastructure ✅
> ✅ تکمیل‌شده | وابستگی: مرحله ۸ | کامیت‌ها: CMS:`a1024a3`→`fdbb91d`→`10d2ca2` FO:`474d364` BO:`6939780`
### 9a: Q24 آستانه موجودی + Q26 SP Worker ✅ (CMS:`a1024a3`)
**Q24 — آستانه موجودی:**
- شرط ورود به Magic و خرید مجدد از `Balance == 0` به `Balance <= 1_000_000` ریال تغییر کرد
- چون قیمت محصولات متفاوته، Balance دقیقاً صفر نمی‌شه
- فایل‌ها: `UserOrderService.cs` (شرط EXIT Magic + Re-purchase guard)
**Q26 — SP Worker:**
- `StoredProcedureDeploymentService` (IHostedService) — در startup فایل‌های `.sql` از embedded resource خوانده می‌شوند
- مقایسه checksum با جدول `__SPChecksums` — فقط SP‌های تغییریافته re-deploy می‌شوند
- فایل‌ها: `StoredProcedureDeploymentService.cs`, embedded `.sql` resources
### 9b: Q27 History Tables Entities ✅ (CMS:`fdbb91d`)
**Entity‌های جدید:**
- `PackageHistory`: فیلدهای Old*/New* برای Price, ActivationFee, MagicMultiplier, MagicMaxDeposit, MaxBalancesPerLeg, IsActive + Action + PerformedBy + Reason
- `ClubMembershipCycleHistory`: فیلدهای Old*/New* برای IsCurrentCycle, MagicStartedAt, MagicCompletedAt + Action + UserId + CycleNumber
**Enums جدید:**
- `PackageAction`: Created, Updated, Activated, Deactivated, PriceChanged, FeaturesChanged
- `ClubMembershipCycleAction`: Created, MagicStarted, MagicCompleted, Closed, AdminModified
**زیرساخت:**
- EF Configurations (indexes, maxLength, precision)
- DbSets در `IApplicationDbContext` و `ApplicationDbContext`
- Navigation Properties: `Package.Histories`, `ClubMembershipCycle.Histories`
### 9c: Q28 UI Guidance ✅ (FO:`474d364` BO:`6939780`)
**FrontOffice — ۷ صفحه با MudAlert آموزشی:**
- G1: Packages.razor — توضیح سیستم پکیج‌بیس
- G2: Checkout — هشدار شارژ کیف‌پول اعتباری
- G3: MyPackages — توضیح وضعیت پکیج‌ها
- G4: MagicWallet — هشدار شرایط خروج + سقف شارژ
- G5: CommissionDashboard — توضیح per-package
- G6: ClubMembership — آموزش چرخه عضویت
- G7: ActivationSection — هشدار هزینه فعال‌سازی
**BackOffice — ۶ صفحه با MudAlert:**
- G8: PackageCRUD — هشدار ثبت تغییرات در History
- G9: ClubFeatures — توضیح ارتباط فیچر-پکیج
- G10: ManualPayments — هشدار مبلغ بر اساس پکیج
- G11: Commission Dashboard — توضیح Pool per-package
- G12: UserPayouts — توضیح فیلتر پکیج
- G13: ClubMembers — اطلاعات چرخه عضویت
### 9d: Rename + History Interceptor + EF Migration ✅ (CMS:`10d2ca2`)
**Rename (86 فایل):**
- `UserWalletChangeLog``UserWalletHistory` در 54+ فایل (entities, configs, DTOs, commands, queries, protos, services)
- 34 فایل rename شده + 11 دایرکتوری rename شده
- Proto: `userwalletchangelog.proto``userwallethistory.proto`
**History Interceptor:**
- `IHasHistory<T>` generic interface در `Domain/Common` — متد `CreateHistorySnapshot(action, performedBy)`
- `HistoryTrackingSaveChangesInterceptor` در `Infrastructure/Persistence/Interceptors` — reflection-based
- شناسایی entity‌های `IHasHistory<>` از ChangeTracker
- فراخوانی `CreateHistorySnapshot` برای Modified/Added
- Auto-fill فیلدهای `Old*` از `OriginalValues` با naming convention
- `Package` implements `IHasHistory<PackageHistory>` — اولین entity
**EF Migration (`Q27_HistoryTables_And_RenameWalletHistory`):**
- ⚠️ EF Core اتوماتیک `DropTable` + `CreateTable` تولید کرد → **دستی اصلاح شد** به `RenameTable` (حفظ داده‌ها)
- `RenameTable` + `RenameIndex` × 2 + `sp_rename` برای PK و FK‌ها
- `CreateTable` برای `ClubMembershipCycleHistories` و `PackageHistories` (جداول جدید)
- Down method: reverse rename + drop new tables
---
## مرحله ۵ — تست و استقرار
> ⏱️ **۳ روز** (v3: +۱) | وابستگی: مرحله ۴
@@ -778,4 +861,4 @@ await connection.ExecuteAsync("CMS.sp_CalculateWeeklyBalances",
---
*آخرین بروزرسانی: ۱۴۰۴/۱۲/۱۰ — فاز ۰-۸f تکمیل (۲۷ کامیت: ۱۷ CMS + ۷ FO + ۵ BO) | NuGet v0.0.188 | T4.2+T4.3+T4.13+F2+F3 کامل | باقی‌مانده: تست + deploy*
*آخرین بروزرسانی: ۱۴۰۴/۱۲/۰۸ — فاز ۰-۹d تکمیل (۳۰ کامیت: ۲۰ CMS + ۸ FO + ۶ BO) | NuGet v0.0.188 | باقی‌مانده: تست + deploy*
+51 -3
View File
@@ -1,7 +1,7 @@
# ⚙️ معماری CMS و زیرساخت فنی
> **منابع ادغام‌شده:** `CMS-README.md`, `ICURRENTUSERSERVICE-IMPLEMENTATION.md`, `FILE-MANAGEMENT-ARCHITECTURE.md`, `FRONTOFFICE-CMS-API-COMPATIBILITY.md`, `BFF-REMOVAL-PLAN.md`, `system-constants.md`
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: Magic Wallet entities + gRPC)
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: فیکس ZarinPal Verify + Callback URL امنیت + appsettings.Development.json)
---
@@ -140,6 +140,7 @@ public class CreateProductCommandHandler
| `CategoryService` | category.proto | GetCategories, Create, Update |
| `SystemConfigService` | config.proto | GetConfig, UpdateConfig |
| `UserWalletService` | userwallet.proto | GetCustomerWallet, InitiateMagicCharge, GetMagicWalletStatus |
| `UserWalletHistoryService` | userwallethistory.proto | *(renamed from UserWalletChangeLogService)* |
### ۴.۲ PaginationState (مشترک)
@@ -195,6 +196,45 @@ Engine: MSSQL 2022-CU16, Collation=Arabic_CI_AS
| `sp_CalculateWeeklyBalances` | محاسبه بالانس هفتگی هر عضو |
| `sp_CalculateWeeklyCommissionPool` | توزیع Pool هفتگی |
### ۵.۴ SP Auto-Deploy Worker (Q26)
```csharp
// StoredProcedureDeploymentService : IHostedService
// در startup:
// 1. خواندن فایل‌های .sql از embedded resource
// 2. مقایسه checksum با جدول __SPChecksums
// 3. فقط SP‌های تغییریافته re-deploy می‌شوند
```
---
## ۵.۵ History Tracking System (Q27)
### IHasHistory<T> Interface
```csharp
public interface IHasHistory<THistory> where THistory : BaseAuditableEntity, new()
{
THistory CreateHistorySnapshot(string action, string? performedBy);
}
```
### HistoryTrackingSaveChangesInterceptor
- **مکان:** `Infrastructure/Persistence/Interceptors/HistoryTrackingSaveChangesInterceptor.cs`
- **مکانیسم:** `SaveChangesInterceptor` — قبل از `SaveChanges` اجرا می‌شود
- **شناسایی:** از `ChangeTracker` entity‌هایی که `IHasHistory<>` پیاده‌سازی کردن (Modified/Added)
- **Auto-fill:** فیلدهای `Old*` از `entry.OriginalValues` با naming convention (مثلاً `OldPrice``OriginalValues["Price"]`)
- **Entity‌های فعال:** `Package``PackageHistory`
### History Tables
| جدول | Entity مرتبط | فیلدهای Old/New |
|------|-------------|----------------|
| `PackageHistories` | Package | Price, ActivationFee, MagicMultiplier, MagicMaxDeposit, MaxBalancesPerLeg, IsActive |
| `ClubMembershipCycleHistories` | ClubMembershipCycle | IsCurrentCycle, MagicStartedAt, MagicCompletedAt |
| `UserWalletHistories` | UserWallet | *(renamed from UserWalletChangeLogs — RenameTable migration)* |
---
## ۶. حذف BFF / Gateway
@@ -265,7 +305,15 @@ FrontOffice Service Layer:
"WorkerCount": 4
},
"Kavenegar": { "ApiKey": "***" },
"ZarinPal": { "MerchantId": "***" },
"DayaLoan": { "UseMock": true }
"ZarinPal": { "MerchantId": "***", "UseSandbox": true },
"DayaLoan": { "UseMock": true },
"CmsBaseUrl": "https://cms.se.kbs1.ir",
"FrontOfficeBaseUrl": "http://localhost:5268"
}
```
> **⚠️ نکات مهم appsettings:**
> - `CmsBaseUrl` — برای callback URL‌های درگاه (شارژ کیف‌پول جادویی/اعتباری)
> - `FrontOfficeBaseUrl` — برای redirect بعد پرداخت (خرید پکیج/تراکنش عمومی)
> - `appsettings.Development.json` — URL‌های localhost برای توسعه محلی
> - همه callback URL‌ها از config خوانده می‌شوند — هیچ URL از ورودی کاربر نمی‌آید (امنیت Open Redirect)
+8 -2
View File
@@ -1,7 +1,7 @@
# 🖥️ BackOffice و FrontOffice — معماری UI
> **منابع ادغام‌شده:** `BACKOFFICE-ARCHITECTURE.md`, `BACKOFFICE-STORE-UNIFICATION.md`, `UI-MODERNIZATION-PLAN.md`, `UI-UNIFICATION-PLAN.md`, `PHASE-1-COMPLETE.md`, `PHASE-3-COMPLETE.md`, `PRODUCT-IMAGES-SQUARE.md`
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: UserAutoComplete + فعال‌سازی درگاه)
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: فیکس‌های پرداخت Phase 11 + صفحه موفقیت + تومان/ریال + امنیت Callback)
---
@@ -119,7 +119,8 @@ FrontOffice/src/FrontOffice/
│ │ └── Contract.razor ← امضای قرارداد
│ ├── Profile/
│ │ ├── Index.razor ← داشبورد پروفایل + تایل Magic
│ │ ── MagicWallet.razor ← 🪄 کیف‌پول جادویی (NEW)
│ │ ── MagicWallet.razor ← 🪄 کیف‌پول جادویی
│ │ └── PaymentCallback.razor ← 💳 صفحه نتیجه پرداخت (TransactionId + موجودی واقعی)
│ ├── Blog/
│ ├── Auth/
│ │ ├── Login.razor
@@ -134,6 +135,7 @@ FrontOffice/src/FrontOffice/
└── Shared/
├── AppImage.razor
├── ProductCard.razor ← مشترک بین Store و DiscountStore
├── PackagePurchaseDialog.razor ← دیالوگ ۲-مرحله‌ای خرید پکیج (NEW)
└── LoadMoreButton.razor
```
@@ -263,6 +265,10 @@ if (user.Identity?.IsAuthenticated == true) {
| UserAutoComplete کامپوننت | ✅ | 100% |
| نمایش نام کاربر در Wallet | ✅ | 100% |
| فعال‌سازی دکمه‌های درگاه | ✅ | 100% |
| PackagePurchaseDialog | ✅ | 100% |
| Toman/Rial فیکس نمایش قیمت | ✅ | 100% |
| صفحه نتیجه پرداخت (PaymentCallback) | ✅ | 100% |
| امنیت Callback URL | ✅ | 100% |
| Mobile Responsive (Phase 7) | ⬜ | 0% |
| Dark Mode | ⬜ | 0% |
| PWA | ⬜ | 0% |
+32 -12
View File
@@ -1,7 +1,7 @@
# 🔄 مهاجرت داده، BFF و Gateway
> **منابع ادغام‌شده:** `BACKOFFICE-BFF-MIGRATION.md`, `customer-facing-capabilities-codex.md`, `DATA-TABLE-MAPPINGS.md`, `DATAMIGRATION-README.md`, `FRONTOFFICE-TO-CMS-MIGRATION.md`, `GATEWAY-REMOVAL-MIGRATION-PLAN.md`, `MIGRATION-PROGRESS.md`
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: Migration پروداکشن)
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: DataMigration Tool + EF Staging Migrations)
---
@@ -136,15 +136,15 @@ Proto-generated classes مستقیم در UI استفاده می‌شوند
```
DataMigration/
├── FourSat.DataMigration/ ← Console app
│ ├── Program.cs
│ ├── Migrators/
│ ├── UserMigrator.cs
│ │ ── ProductMigrator.cs
│ ├── OrderMigrator.cs
│ │ └── ClubMigrator.cs
├── FourSat.DataMigration/ ← Console app (.NET 9 + Dapper + Polly + Serilog)
│ ├── Program.cs ← Entry point
│ ├── appsettings.json ← Source/Target connection strings + TruncateTargetTables
│ ├── Services/
│ │ ── MigrationService.cs ← Smart retry, FK disable/enable, fallback table names
│ ├── Scripts/
│ │ └── PostMigration_DataTransformation.sql ← Guardشده با IF COL_LENGTH/OBJECT_ID
│ └── Mappings/
│ └── TableMappings.cs
│ └── TableMappings.cs ← Source → Target table/column mappings
└── FourSat.GeographySeeder/ ← Seed geography data
├── Program.cs
└── Data/
@@ -152,6 +152,21 @@ DataMigration/
└── cities.json
```
### ۵.۱.۱ ویژگی‌های DataMigration Tool (اسفند ۱۴۰۴)
| ویژگی | توضیح |
|--------|--------|
| **Smart Retry** | فقط خطاهای transient SQL (deadlock, timeout, transport) — نه خطاهای منطقی |
| **FK Disable/Enable** | `ALTER TABLE NOCHECK/CHECK CONSTRAINT ALL` حول هر مهاجرت |
| **TruncateTargetTables** | حل duplicate key (`IX_ClubMembership_UserId`) هنگام re-run |
| **Fallback Table Name** | اگر جدول rename شده (`UserWalletChangeLogs``UserWalletHistories`) |
| **PostMigration Guards** | همه مراحل با `IF COL_LENGTH`/`OBJECT_ID` برای سازگاری با هر دو schema |
| **Polly Retry** | exponential backoff (2s, 8s, 32s) + لاگ structured |
| **Serilog** | لاگ فایل + کنسول با جزئیات هر جدول |
> **کامیت‌ها:** `0e8c6fd` → `8385c90` (MERGE fix) → `31cc464` (FK+truncate+PostMigration)
> **وضعیت:** Local only — بدون remote (در workspace `DataMigration/` قرار دارد)
### ۵.۲ Data Table Mappings
| جدول مبدأ (قدیم) | جدول مقصد (CMS) | نکات |
@@ -178,14 +193,17 @@ DataMigration/
| `populate-weekly-commission-pools.sql` | داده تاریخی Pool |
| `update_products_price_10_percent.sql` | افزایش قیمت ۱۰% |
### ۵.۴ Migrationهای EF Core اجراشده روی Production (اسفند ۱۴۰۴)
### ۵.۴ Migrationهای EF Core اجراشده روی Production/Staging (اسفند ۱۴۰۴)
| Migration | توضیح | DB |
|-----------|--------|----|
|-----------|--------|----||
| `ExpandDiscountProductFullInformation` | گسترش فیلدهای محصول تخفیفی | KBS (Production `45.149.79.127`) |
| `AddMagicWalletFields` (u21) | فیلدهای کیف‌پول جادویی + ClubMembershipCycle | KBS (Production) |
| ۵۵ migration کامل | از Initial تا `Q27_HistoryTables_And_RenameWalletHistory` | KBS Staging (`185.252.31.42,2019/KBS`) |
| ۵۵ migration کامل | از Initial تا `Q27_HistoryTables_And_RenameWalletHistory` | App DB (`194.5.195.53,31433/Foursat`) |
> ⚠️ نکته: Migration `u21` در زمان merge تکراری بود (timestampهای `20260222155925` و `20260222160755`). فایل تکراری حذف شد.
> ✅ **نکته:** CMS به ۲ DB مختلف وصل می‌شود — هر دو باید migrate شوند.
> ✅ Migration `u21` در زمان merge تکراری بود — فایل تکراری حذف شد.
---
@@ -236,3 +254,5 @@ FourSat.GeographySeeder:
| Geography Seeder | ✅ | 100% |
| Proto package unification | ✅ | 100% |
| EF Migration پروداکشن | ✅ | 100% |
| DataMigration Tool (Prod→Staging) | ✅ | 100% |
| EF Migration استیجینگ (KBS + Foursat) | ✅ | 100% |
+246
View File
@@ -0,0 +1,246 @@
# TECH-06 — جریان شارژ Pool کمیسیون هفتگی
> تاریخ: ۱۴۰۵/۰۲/۱۰
> وضعیت: **باگ شناسایی‌شده — منتظر Fix**
> مرتبط با: `ActivateClubMembershipCommandHandler.cs` · `AcceptClubMembershipContractCommandHandler.cs` · `CreateManualPaymentCommandHandler.cs`
---
## ۱. مسیر مشتری جدید (اولین خرید پکیج)
```mermaid
sequenceDiagram
actor U as کاربر (FrontOffice)
participant CB as PaymentCallback.razor
participant PI as Profile/Index.razor
participant CD as ClubMembershipContractDialog
participant CMS as CMS (gRPC)
U->>CB: بازگشت از درگاه<br/>?type=package&orderId=X&Authority=Y
CB->>CMS: CustomerVerifyPackagePurchase(orderId, authority)
Note over CMS: PackageService.VerifyPackagePurchase()
CMS->>CMS: تأیید با درگاه ✓
CMS->>CMS: ActivateClubMembership(ForceActivation=false)
Note over CMS: isNewMembership = true
CMS->>CMS: ClubMembership(IsActive=false) ایجاد
CMS->>CMS: ClubMembershipCycle #1 ایجاد
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ اول ❌
CMS-->>CB: Success=true
CB->>CB: RefreshToken<br/>HasPurchasedPackage=true<br/>IsClubMemberActive=false
U->>PI: کلیک "بازگشت به پروفایل"
PI->>PI: OnAfterRenderAsync<br/>CheckAndShowClubContractModal()
Note over PI: HasPurchasedPackage=true<br/>AND IsClubMemberActive=false → نمایش مودال
PI->>CD: DialogService.ShowAsync (غیرقابل بستن)
U->>CD: مطالعه قرارداد + درخواست OTP
CD->>CMS: CreateNewOtpToken(purpose=signClubContract)
CMS-->>CD: OTP ارسال شد
U->>CD: وارد کردن OTP ۶ رقمی
CD->>CMS: AcceptClubMembershipContract(otp, signGuid)
Note over CMS: AcceptClubMembershipContractCommandHandler
CMS->>CMS: IsActive == false → guard رد می‌شه ✓
CMS->>CMS: IsActive = true
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ دوم ❌
CMS-->>CD: Success=true
CD->>PI: dialog.Close(Ok)
PI->>PI: LoadUserAuthInfo → IsClubMemberActive=true
```
> **نتیجه**: Pool برای عضو جدید **۲ برابر** شارژ می‌شود.
---
## ۲. مسیر خرید مجدد (بعد از تکمیل چرخه Magic)
```mermaid
sequenceDiagram
actor U as کاربر (FrontOffice)
participant CB as PaymentCallback.razor
participant PI as Profile/Index.razor
participant CMS as CMS (gRPC)
Note over CMS: وضعیت: IsActive=true<br/>IsCurrentCycle=true (باگ B6 — ریست نشده)
U->>CB: بازگشت از درگاه (خرید مجدد)
CB->>CMS: CustomerVerifyPackagePurchase(orderId, authority)
CMS->>CMS: ActivateClubMembership(ForceActivation=false)
Note over CMS: isNewMembership = false<br/>existingMembership.IsActive=true<br/>hasCurrentCycle=true
CMS->>CMS: return true زودهنگام ❌
Note over CMS: Cycle جدید ساخته نمی‌شه ❌<br/>Pool شارژ نمی‌شه ❌
CMS-->>CB: Success=true
CB->>CB: RefreshToken → IsClubMemberActive=true
U->>PI: بازگشت به پروفایل
PI->>PI: IsClubMemberActive=true<br/>→ مودال نمایش داده نمی‌شه ✓
Note over PI,CMS: Pool هرگز شارژ نشد ❌<br/>Cycle جدید وجود ندارد ❌
```
> **نتیجه**: Pool برای خرید مجدد **هرگز** شارژ نمی‌شود. ریشه مشکل: باگ B6 — `IsCurrentCycle` هنگام خروج از Magic ریست نمی‌شود.
---
## ۳. مسیر ادمین (BackOffice — فعال‌سازی دستی)
```mermaid
sequenceDiagram
actor A as ادمین (BackOffice)
participant DL as ActivateClubDialog.razor
participant CMS as CMS (gRPC)
A->>DL: باز کردن دیالوگ فعال‌سازی برای کاربر X
DL->>DL: انتخاب UserId و PackageId
A->>DL: کلیک "تایید و فعال‌سازی"
DL->>CMS: ActivateClubMembership(UserId=X, ForceActivation=true)
Note over CMS: ActivateClubMembershipCommandHandler<br/>skip همه validation‌های مالی
alt کاربر جدید (isNewMembership=true)
CMS->>CMS: ClubMembership(IsActive=false) ایجاد
CMS->>CMS: Cycle #1 ایجاد
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ اول ❌
Note over CMS: کاربر هنوز عضو فعال نیست!<br/>IsActive=false
Note over A,CMS: کاربر باید به FO رود و قرارداد امضا کند
Note over A,CMS: AcceptContract → Pool += fee ← شارژ دوم ❌
else خرید مجدد (isNewMembership=false، IsCurrentCycle ریست شده)
CMS->>CMS: IsActive=true, hasCurrentCycle=false → ادامه می‌دهد
CMS->>CMS: Cycle جدید ایجاد
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ یک بار ✅
end
CMS-->>DL: Empty (success)
DL->>A: "عضویت با موفقیت فعال شد"
Note over A,CMS: AcceptContract از BO هرگز فراخوانی نمی‌شود
```
> **نتیجه**: ادمین برای کاربر جدید نیز باعث double-charge می‌شود (چون کاربر بعداً از FO قرارداد امضا می‌کند). برای خرید مجدد رفتار درست است.
---
## ۵. خلاصه باگ‌ها
| سناریو | Pool شارژ واقعی | Pool شارژ انتظاری | Cycle ساخته می‌شود | وضعیت |
|--------|----------------|-------------------|--------------------|--------|
| مشتری جدید (IPG) | **2×fee** | 1×fee | ✅ بله | ❌ Double-charge |
| خرید مجدد مشتری | **0×fee** | 1×fee | ❌ خیر (B6) | ❌ هرگز شارژ نمی‌شود |
| ادمین — ForceActivate کاربر جدید | **2×fee** | 1×fee | ✅ بله | ❌ Double-charge |
| ادمین — ForceActivate خرید مجدد | **1×fee** | 1×fee | ✅ بله | ✅ درست |
| ادمین — ManualPayment (پرداخت دستی) | **1×fee** | 1×fee | ❌ خیر | ⚠️ Pool درست، ولی والدین امتیاز نمی‌گیرند |
---
## ۵. ریشه مشکلات
### باگ A — Double-charge در عضو جدید
**فایل**: `ActivateClubMembershipCommandHandler.cs` — بخش Pool (خط ~۳۱۱)
**علت**: هنگامی که `isNewMembership=true`، Pool شارژ می‌شود؛ بعداً `AcceptContract` هم Pool را شارژ می‌کند.
**Fix**: شارژ Pool در `ActivateClubMembership` را فقط برای `!isNewMembership` انجام بده:
```csharp
// ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری
// عضو جدید: Pool توسط AcceptClubMembershipContract شارژ می‌شه (هنگام امضای قرارداد)
// خرید مجدد: قرارداد مجدد امضا نمی‌شه — Pool همین‌جا شارژ می‌شه
if (!isNewMembership)
{
// ... کد موجود شارژ Pool ...
}
```
### باگ B6 — خرید مجدد کار نمی‌کند
**فایل**: `UserOrderService.cs` — بخش خروج از Magic
**علت**: هنگام خروج از Magic، `cycle.IsCurrentCycle` به `false` ریست نمی‌شود → `ActivateClubMembership` با `hasCurrentCycle=true` زودهنگام برمی‌گردد.
**Fix**: در `ExitMagicMode`:
```csharp
cycle.IsCurrentCycle = false; // ← اضافه شود
```
### باگ C — پرداخت دستی: Cycle هرگز ساخته نمی‌شود
**فایل**: `CreateManualPaymentCommandHandler.cs`
**علت**: پرداخت دستی `ActivateClubMembership` را صدا نمی‌زند → هیچ `ClubMembershipCycle` ساخته نمی‌شود → SP این کاربر را به عنوان "عضو جدید" برای والدینش حساب نمی‌کند.
**تأثیر**: Pool یک‌بار شارژ می‌شود (توسط AcceptContract ✓) ولی balance والدین در sp_CalculateWeeklyBalances افزایش نمی‌یابد (چون Cycle ندارد ❌).
---
## ۴. مسیر پرداخت دستی (BackOffice — ManualPayment)
```mermaid
sequenceDiagram
actor A as ادمین (BackOffice)
participant DL as ManualPaymentDialog.razor
participant CMS as CMS (gRPC)
actor U as کاربر (FrontOffice)
participant PI as Profile/Index.razor
participant CD as ClubMembershipContractDialog
A->>DL: باز کردن دیالوگ پرداخت دستی
DL->>DL: انتخاب کاربر + پکیج + نوع پرداخت + تصویر رسید
A->>DL: کلیک "ثبت پرداخت"
DL->>CMS: CreateManualPayment(userId, packageId, type, referenceNumber)
Note over CMS: CreateManualPaymentCommandHandler
CMS->>CMS: Transaction(DepositExternal1) ایجاد
CMS->>CMS: ManualPayment(Status=Approved) ایجاد ← بدون نیاز به تایید دو مرحله
CMS->>CMS: wallet.Balance += package.Price
CMS->>CMS: wallet.DiscountBalance += package.Price × DiscountMultiplier
CMS->>CMS: user.PackagePurchaseMethod = DirectPurchase
Note over CMS: ❌ ActivateClubMembership صدا زده نمی‌شود<br/>❌ ClubMembershipCycle ساخته نمی‌شود<br/>❌ Pool شارژ نمی‌شود
CMS-->>DL: ManualPaymentId
DL->>A: "پرداخت دستی با موفقیت ثبت شد"
Note over A,U: کاربر باید به FO مراجعه کند
U->>PI: ورود به پروفایل
PI->>PI: OnAfterRenderAsync → CheckAndShowClubContractModal()
Note over PI: HasPurchasedPackage=true (PackagePurchaseMethod=DirectPurchase)<br/>IsClubMemberActive=false → نمایش مودال
PI->>CD: DialogService.ShowAsync (غیرقابل بستن)
U->>CD: امضای قرارداد + OTP
CD->>CMS: AcceptClubMembershipContract(otp, signGuid)
Note over CMS: AcceptClubMembershipContractCommandHandler
CMS->>CMS: user.ClubMembership == null → isNewMembership = true
CMS->>CMS: ClubMembership(IsActive=true) ایجاد
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ یک‌بار ✅
Note over CMS: ❌ ClubMembershipCycle هرگز ساخته نمی‌شود<br/>(AcceptContract از Cycle خبری ندارد)
CMS-->>CD: Success=true
CD->>PI: dialog.Close(Ok)
```
> **نتیجه**:
> - Pool: **1×** شارژ می‌شود ✅ (درست)
> - `ClubMembershipCycle`: **هرگز ساخته نمی‌شود** ❌
> - در `sp_CalculateWeeklyBalances`: کاربر `IsActive=true` دارد → خودش می‌تواند کمیسیون دریافت کند ✅
> - ولی والدین این کاربر **هیچ "عضو جدید" برای این هفته دریافت نمی‌کنند** ❌ (چون SP از `ClubMembershipCycles.PackagePurchasedAt` می‌خواند)
---
## ۶. validation داشبورد کمیسیون
پس از رفع باگ A، validation باید از **فقط یک منبع** استفاده کند:
```csharp
// درست: فقط ClubMembershipCycles.PackagePurchasedAt
// این جدول برای هر خرید (چه جدید چه مجدد) یک رکورد دارد
var activations = await _context.ClubMembershipCycles
.CountAsync(c => c.PackageId == packageId
&& c.PackagePurchasedAt >= weekDef.StartDate
&& c.PackagePurchasedAt < weekDef.EndDate);
```
> قبل از رفع باگ A، validation فعلی (firstActivations + cycleActivations) تصادفاً با double-charge جبران می‌شد.
+334
View File
@@ -0,0 +1,334 @@
# TECH-07 — لاگ کامل Session 1404/02/10 (2026-04-30)
> نوع سند: **گزارش کار**
> تاریخ: ۱۴۰۵/۰۲/۱۰
> مرتبط با: CMS · BackOffice · FrontOffice · Database
---
## فهرست مطالب
1. [بخش اول — رفع باگ Double-Charge Pool](#۱-رفع-باگ-double-charge-pool)
2. [بخش دوم — بررسی داده‌های هفته‌های ۲۲ و ۲۳](#۲-بررسی-داده‌های-هفته‌های-۲۲-و-۲۳)
3. [بخش سوم — ویژگی Network Tree (اطلاعات هفتگی)](#۳-ویژگی-network-tree-نوع-فعالسازی--پکیج)
4. [بخش چهارم — بهبود UI نمودار درختی](#۴-بهبود-ui-نمودار-درختی)
5. [بخش پنجم — رفع باگ Pagination فروشگاه تخفیف](#۵-رفع-باگ-pagination-فروشگاه-تخفیف)
6. [خلاصه فایل‌های تغییریافته](#خلاصه-فایل‌های-تغییریافته)
7. [وظایف باقی‌مانده (Pending)](#وظایف-باقی‌مانده)
---
## ۱. رفع باگ Double-Charge Pool
### مشکل
در جریان فعال‌سازی عضویت باشگاه، Pool کمیسیون هفتگی **دوبار** شارژ می‌شد:
- بار اول: در `ActivateClubMembership` (از طریق `VerifyPackagePurchase`)
- بار دوم: در `AcceptClubMembershipContract` (تأیید قرارداد توسط کاربر)
همچنین `CreateManualPayment` هم یک مسیر مستقل داشت که بدون Check هفته، Pool اشتباه را شارژ می‌کرد.
### ریشه مشکل
تابع `GetOrCreateCurrentWeeklyPool` بدون در نظر گرفتن هفته واقعی `PackagePurchasedAt`، Pool هفته جاری را انتخاب می‌کرد.
### فایل‌های اصلاح‌شده
#### `ActivateClubMembershipCommandHandler.cs`
```csharp
// قبل: همیشه Pool هفته جاری را شارژ می‌کرد
// بعد: فقط یک‌بار در محل صحیح (AcceptContract) شارژ می‌شود
// حذف: شارژ Pool از داخل ActivateClubMembership (for isNewMembership scenario)
```
#### `AcceptClubMembershipContractCommandHandler.cs`
```csharp
// اضافه: بررسی هفته قرارداد — اگر هفته PackagePurchasedAt با هفته جاری فرق دارد
// از Pool هفته مناسب استفاده می‌کند نه Pool هفته جاری
```
#### `CreateManualPaymentCommandHandler.cs`
```csharp
// اصلاح: Cross-week fix — Pool هفته صحیح بر اساس تاریخ پرداخت دستی
```
#### `sp_CalculateWeeklyCommissionPool.sql` (SP در Infrastructure)
```sql
-- اصلاح: IsCurrentCycle check برای جلوگیری از Double-Count
-- هر کاربر فقط یک‌بار در محاسبه Pool شمرده می‌شود
```
---
## ۲. بررسی داده‌های هفته‌های ۲۲ و ۲۳
### تشخیص
با اجرای diagnostic SQL روی DB، دو anomaly کشف شد:
#### هفته ۲۲ — Pool Ghost (PoolId=10056)
| فیلد | مقدار |
|------|-------|
| TotalPoolAmount | 2,520,000 |
| AllCycles | 0 |
| ریشه | هانیه سادات عشاقی (UserId=189) — خرید 1404/01/12 (هفته ۲۱) ولی AcceptContract در 17:02 دقیقه بعد Pool هفته ۲۲ را شارژ کرد |
**دلیل:** CreatedAt و ModifiedAt timestamp مغایرت داشت — Pool در هفته ۲۲ ایجاد شد اما Cycle در هفته ۲۱ بود.
**اصلاح دستی DB (Pending):**
```sql
UPDATE CMS.WeeklyCommissionPools
SET TotalPoolAmount = 0, LastModified = GETUTCDATE()
WHERE Id = 10056 AND WeekDefinitionId = 22;
```
#### هفته ۲۳ — Pool ناقص (PoolId=10054)
| فیلد | مقدار |
|------|-------|
| TotalPoolAmount | 0 |
| IsCalculated | False |
| ریشه | محمدصادق عسلی (UserId=190) — خرید هفته ۲۳، Cycle وجود دارد ولی Pool=0 (قبل از fix) |
**اصلاح دستی DB (Pending):**
```sql
UPDATE CMS.WeeklyCommissionPools
SET TotalPoolAmount = 2520000, LastModified = GETUTCDATE()
WHERE Id = 10054 AND WeekDefinitionId = 23;
```
---
## ۳. ویژگی Network Tree (نوع فعالسازی + پکیج)
### هدف
صفحه `/network/tree` در BackOffice باید در هر node نشان دهد:
- آیا این کاربر در هفته انتخابی **عضو جدید** بوده یا **تمدید کرده**؟
- نام پکیجی که خریداری کرده؟
### پیاده‌سازی Full-Stack
#### الف) SP_GetNetworkTree (dbbkup/SP_GetNetworkTree.sql)
```sql
-- اضافه شد:
OUTER APPLY (
SELECT TOP 1 cc.*
FROM CMS.ClubMembershipCycles cc
WHERE cc.ClubMembershipId = cm.Id
AND cc.PackagePurchasedAt >= @WeekStartDate
AND cc.PackagePurchasedAt < @WeekEndDate
AND (@ActivationWeekDefinitionId IS NULL OR @WeekStartDate IS NOT NULL)
) AS cc_target
-- ستون‌های جدید در output:
IsActivatedInTargetWeek -- آیا در هفته انتخابی فعال شده؟
IsNewActivation -- 1=اولین فعالسازی (CycleNumber=1), 0=تمدید, NULL=بدون Cycle
PackageName -- نام پکیج اون هفته
PackageId -- شناسه پکیج
```
**نکته:** منطق هفته‌بندی از `cm.ActivatedAt` به `Cycle.PackagePurchasedAt` تغییر کرد.
**Deploy SP:**
```
SP مستقیم روی DB اجرا شد (نه EmbeddedResource Infrastructure)
اجرا شد در: /tmp/DbDiag با C# script
تأیید شد: SELECT OBJECT_ID('[CMS].[GetNetworkTree]') → موجود
```
#### ب) Application Layer
**`NetworkTreeNodeDto.cs`** — فیلدهای جدید:
```csharp
bool? IsNewActivation
string? PackageName
long? PackageId
```
**`NetworkTreeDto.cs`** — همین فیلدها
**`GetNetworkTreeQueryHandler.cs`**:
```csharp
// خواندن از DataReader:
IsNewActivation = reader.IsDBNull(reader.GetOrdinal("IsNewActivation"))
? null
: reader.GetInt32(reader.GetOrdinal("IsNewActivation")) == 1,
PackageName = reader["PackageName"] as string,
PackageId = reader.IsDBNull(reader.GetOrdinal("PackageId"))
? null
: reader.GetInt64(reader.GetOrdinal("PackageId"))
```
#### ج) Proto (networkmembership.proto)
```protobuf
// NetworkTreeNodeModel — فیلدهای جدید:
google.protobuf.BoolValue is_new_activation = 22;
string package_name = 23;
google.protobuf.Int64Value package_id = 24;
```
**NuGet Package:** `Foursat.CMSMicroservice.Protobuf` → از `0.0.194` به **`0.0.195`** bump و push شد.
#### د) Mapping (NetworkMembershipProfile.cs)
```csharp
PackageName = node.PackageName ?? string.Empty,
PackageId = node.PackageId.HasValue ? node.PackageId.Value : null,
IsNewActivation = node.IsNewActivation.HasValue ? node.IsNewActivation.Value : null
```
#### هـ) BackOffice — NetworkTreeViewer.razor
**DataGrid — دو ستون جدید:**
```razor
<!-- ستون نوع فعالسازی -->
<PropertyColumn Property="x => x.IsNewActivation" Title="نوع فعالسازی">
@if (context.Item.IsNewActivation == true)
{
<MudChip Color="Color.Success">🆕 عضو جدید</MudChip>
}
else if (context.Item.IsNewActivation == false)
{
<MudChip Color="Color.Secondary">🔄 خرید مجدد</MudChip>
}
</PropertyColumn>
<!-- ستون پکیج -->
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
```
**JS (jsNodes):**
```js
isNewActivation: n.IsNewActivation,
packageName: n.PackageName ?? ""
```
---
## ۴. بهبود UI نمودار درختی
### مشکل اولیه
بج «🆕 جدید» با `position: absolute` از گوشه کارت بیرون می‌زد و با محتوای دیگر برخورد می‌کرد.
### فایل‌های تغییریافته
#### `admin-org-chart.js` (wwwroot/js)
**ساختار کارت بازنویسی شد:**
```
┌─────────────────────────────┐
│ [Avatar] نام کاربر │
│ پکیج نقره... │ ← inline زیر اسم
│ L13 چپ عضو جدید │ ← pill در meta row
├─────────────────────────────┤
│ ✓ فعال 1404/12/24 │
└─────────────────────────────┘
```
**تغییرات:**
- `activationTypeBadge` (absolute positioning) → `activationTypePill` (inline span)
- `highlightBadge` (✨ floating) → حذف شد
- `packageBadge` به زیر اسم کاربر منتقل شد (نه footer)
- ابعاد کارت: `160×80``178×92` px
#### `admin-org-chart.css` (wwwroot/css)
```css
/* جدید: activation pill به جای badge */
.admin-node-card .activation-pill { /* inline flex */ }
.admin-node-card .new-member-pill { background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; }
.admin-node-card .renewal-pill { background: #ede7f6; color: #5e35b1; border: 1px solid #b39ddb; }
/* بهبود: پکیج روشن‌تر */
.admin-node-card .package-name-badge { background: #eceff1; color: #546e7a; border: 1px solid #b0bec5; }
```
---
## ۵. رفع باگ Pagination فروشگاه تخفیف
### مشکل
در صفحه `/discount-store/products`، دکمه «نمایش محصولات بیشتر» کار نمی‌کرد — همیشه صفحه اول برمی‌گشت.
### ریشه مشکل
**فایل غایب:** `DiscountProductProfile.cs` (Mapster) وجود نداشت.
**جریان mapping:**
```
GetDiscountProductsRequest (proto)
↓ request.Adapt<GetDiscountProductsQuery>()
GetDiscountProductsQuery
```
بدون profile، auto-mapping دو مشکل داشت:
1. `request.SearchQuery (string)``query.SearchTerm (string?)` → نامتطابق نام، NULL می‌شد
2. `request.PageNumber (int)``query.PaginationQuery.PageNumber`**Mapster نمی‌توانست به nested object مپ کند**`PaginationQuery = null` → default: `PageNumber=1` همیشه!
### راه‌حل
**فایل جدید:** `CMS/src/CMSMicroservice.WebApi/Common/Mappings/DiscountProductProfile.cs`
```csharp
config.NewConfig<GetDiscountProductsRequest, GetDiscountProductsQuery>()
.Map(dest => dest.SearchTerm,
src => string.IsNullOrEmpty(src.SearchQuery) ? null : src.SearchQuery)
.Map(dest => dest.CategoryId,
src => src.CategoryId != null ? src.CategoryId.Value : (long?)null)
.Map(dest => dest.IsActive,
src => src.IsActive != null ? src.IsActive.Value : (bool?)null)
.Map(dest => dest.PaginationQuery, src => new PaginationState
{
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
PageSize = src.PageSize > 0 ? src.PageSize : 12
});
```
همچنین `GetDiscountProductsResponseDto → GetDiscountProductsResponse` هم به صورت صریح مپ شد تا `MetaData` و `Models` درست انتقال یابند.
---
## خلاصه فایل‌های تغییریافته
| فایل | نوع تغییر | پروژه |
|------|-----------|-------|
| `ActivateClubMembershipCommandHandler.cs` | Fix — حذف Double-Charge | CMS Application |
| `AcceptClubMembershipContractCommandHandler.cs` | Fix — Cross-week Pool | CMS Application |
| `CreateManualPaymentCommandHandler.cs` | Fix — Cross-week Pool | CMS Application |
| `sp_CalculateWeeklyCommissionPool.sql` | Fix — IsCurrentCycle | CMS Infrastructure |
| `SP_GetNetworkTree.sql` | Feature — IsNewActivation, PackageName, PackageId | DB/dbbkup |
| `NetworkTreeNodeDto.cs` | Feature — فیلدهای جدید | CMS Application |
| `NetworkTreeDto.cs` | Feature — فیلدهای جدید | CMS Application |
| `GetNetworkTreeQueryHandler.cs` | Feature — خواندن فیلدهای جدید | CMS Application |
| `networkmembership.proto` | Feature — ۳ فیلد جدید در NetworkTreeNodeModel | Protobuf |
| `NetworkMembershipProfile.cs` | Feature — mapping فیلدهای جدید | CMS WebApi |
| `NetworkTreeViewer.razor` | Feature — DataGrid ستون‌های جدید | BackOffice |
| `admin-org-chart.js` | Feature+Fix — inline pill، پکیج زیر اسم | BackOffice wwwroot |
| `admin-org-chart.css` | Feature+Fix — استایل pill‌های مرتب | BackOffice wwwroot |
| `DiscountProductProfile.cs` | Fix — Pagination mapping صحیح | CMS WebApi (جدید) |
### NuGet Package
| پکیج | نسخه قبل | نسخه جدید |
|------|----------|-----------|
| `Foursat.CMSMicroservice.Protobuf` | 0.0.194 | **0.0.195** |
---
## وظایف باقی‌مانده
### ضروری — اصلاح داده‌های DB
```sql
BEGIN TRANSACTION;
-- هفته ۲۲: Pool Ghost (هانیه سادات عشاقی ← AcceptContract هفته اشتباه)
UPDATE CMS.WeeklyCommissionPools
SET TotalPoolAmount = 0, LastModified = GETUTCDATE()
WHERE Id = 10056 AND WeekDefinitionId = 22;
-- هفته ۲۳: Pool ناقص (محمدصادق عسلی ← Pool قبل از Fix ایجاد شده بود)
UPDATE CMS.WeeklyCommissionPools
SET TotalPoolAmount = 2520000, LastModified = GETUTCDATE()
WHERE Id = 10054 AND WeekDefinitionId = 23;
COMMIT;
```
### بهبود آینده
- [ ] `SP_GetNetworkTree.sql` به Infrastructure EmbeddedResource اضافه شود (auto-deploy)
- [ ] `DiscountProductDto` در Application — اضافه کردن فیلد `Created` از DB
- [ ] تست pagination فروشگاه پس از restart CMS
+303
View File
@@ -0,0 +1,303 @@
# TECH-08 — لاگ Session 1404/02/23 (2026-05-13)
> نوع سند: **گزارش کار**
> تاریخ: ۱۴۰۵/۰۲/۲۳
> مرتبط با: CMS · FrontOffice
> کامیت CMS: `683ed37` (branch: `kub-stage`)
> کامیت FrontOffice: `231da2c` (branch: `kub-stage`)
---
## فهرست مطالب
1. [هدف و خلاصه](#هدف-و-خلاصه)
2. [تغییرات CMS (Backend)](#تغییرات-cms-backend)
3. [تغییرات FrontOffice](#تغییرات-frontoffice)
4. [معماری GuestActionGate](#معماری-guestactiongate)
5. [فلوچارت تجربه کاربر](#فلوچارت-تجربه-کاربر)
6. [فایل‌های تغییر یافته](#فایلهای-تغییر-یافته)
---
## هدف و خلاصه
هدف این session:
1. **نمایش ۶ محصول پرفروش معمولی + ۶ محصول پرفروش فروشگاه اعتباری** در لندینگ پیج FrontOffice، زیر هدر اصلی (۳ محصول در هر ردیف، دو section مجزا).
2. **دسترسی guest** (کاربر بدون لاگین) به مرور محصولات برای پرزنت به مشتریان بالقوه.
3. **Hybrid auth flow**: کاربر guest محصولات را می‌بیند؛ اگر روی "افزودن به سبد" کلیک کرد، مودال لاگین باز می‌شود و پس از ورود موفق، عمل به صورت خودکار انجام می‌شود.
---
## تغییرات CMS (Backend)
### ۱. `discountproduct.proto`
```proto
// اضافه شده به GetDiscountProductsRequest
google.protobuf.StringValue sort_by = 9;
// اضافه شده به DiscountProductDto
int32 sale_count = 12;
```
**چرا:** برای واکشی پرفروش‌ترین محصولات فروشگاه اعتباری باید امکان sort بر اساس `sale_count` وجود داشته باشد. قبلاً این فیلد در DTO برگردانده نمی‌شد.
### ۲. `CMSMicroservice.Protobuf.csproj`
نسخه از `0.0.195` به `0.0.196` بالا رفت تا پکیج NuGet جدید publish شود.
### ۳. `GetDiscountProductsQuery.cs`
```csharp
public string? SortBy { get; set; }
```
### ۴. `GetDiscountProductsQueryHandler.cs`
```csharp
// قبل: همیشه OrderByDescending(p => p.Created)
// بعد: dynamic sort با fallback
if (!string.IsNullOrEmpty(request.SortBy))
query = query.ApplyOrder(request.SortBy);
else
query = query.OrderByDescending(p => p.Created);
// و در SELECT:
SaleCount = p.SaleCount,
```
از extension method موجود `ApplyOrder` (کتابخانه `System.Linq.Dynamic.Core`) استفاده شد تا نیازی به تغییر جداگانه نباشد.
### ۵. `DiscountProductProfile.cs` (Mapster)
```csharp
// Request mapping
.Map(dest => dest.SortBy, src => string.IsNullOrEmpty(src.SortBy) ? null : src.SortBy)
// Response mapping
SaleCount = p.SaleCount,
```
---
## تغییرات FrontOffice
### ۱. `GuestActionGate.cs` (فایل جدید)
```
FrontOffice.Main/Utilities/GuestActionGate.cs
```
سرویس utility جدید که هر action نیازمند لاگین را wrap می‌کند:
```csharp
public async Task<bool> RunAsync(Func<Task> action)
{
if (await _authService.IsAuthenticatedAsync())
{
await action();
return true;
}
await _authDialogService.ShowAuthDialogAsync();
if (await _authService.IsAuthenticatedAsync())
{
await action();
return true;
}
return false;
}
```
در `ConfigureServices.cs` به صورت Scoped ثبت شد:
```csharp
services.AddScoped<GuestActionGate>();
```
### ۲. `ProductService.cs`
```csharp
public Task<ProductListResult> GetTopSellingAsync(int count = 6)
=> GetProductsPagedAsync(sortBy: "SaleCount desc", page: 1, pageSize: count);
```
### ۳. `DiscountProductService.cs`
```csharp
// پارامتر جدید به GetProductsAsync اضافه شد
public async Task<DiscountProductListResult> GetProductsAsync(
..., string? sortBy = null)
{
if (!string.IsNullOrWhiteSpace(sortBy))
request.SortBy = sortBy;
...
}
public Task<DiscountProductListResult> GetTopSellingAsync(int count = 6)
=> GetProductsAsync(page: 1, pageSize: count, sortBy: "SaleCount desc");
```
### ۴. `Index.razor` و `Index.razor.cs`
دو section جدید در لندینگ پیج زیر hero اضافه شد:
**Section 1 — محصولات پرفروش معمولی:**
- عنوان: "محصولات پرفروش"
- ۶ کارت (۳ در هر ردیف با MudGrid)
- هر کارت: تصویر، نام، قیمت با VAT، دکمه "افزودن به سبد"
- دکمه "بیشتر" → `/products`
**Section 2 — محصولات پرفروش فروشگاه اعتباری:**
- عنوان: "فروشگاه اعتباری"
- ۶ کارت (۳ در هر ردیف)
- هر کارت: تصویر، نام، قیمت، درصد تخفیف
- دکمه "بیشتر" → `/discount-store`
**Loading state:** در حین بارگذاری یک spinner نشان داده می‌شود و سپس section‌ها fade-in می‌شوند.
**Data loading (parallel):**
```csharp
var topRegTask = ProductService.GetTopSellingAsync(6);
var topDiscTask = DiscountProductService.GetTopSellingAsync(6);
var featuredPostsTask = BlogPostService.GetFeaturedPostsAsync(2);
await Task.WhenAll(topRegTask, topDiscTask, featuredPostsTask);
```
**Cart actions با GuestActionGate:**
```csharp
private async Task AddRegularToCart(Product p)
=> await GuestGate.RunAsync(() => Cart.Add(p, 1));
private async Task AddDiscountToCart(DiscountProductCard p)
=> await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
```
### ۵. Hybridize کردن صفحات موجود
#### صفحات لیست و جزئیات محصول (GuestActionGate):
| فایل | تغییر |
|------|-------|
| `Store/Products.razor.cs` | `AddToCart``GuestGate.RunAsync(...)` |
| `Store/ProductDetail.razor.cs` | `AddToCart` و `RemoveFromCart``GuestGate.RunAsync(...)` |
| `DiscountStore/Products.razor.cs` | `AddToCart``GuestGate.RunAsync(...)` |
| `DiscountStore/ProductDetail.razor.cs` | `AddToCart``GuestGate.RunAsync(...)` |
#### صفحات Cart و Checkout (Soft Auth Gate):
```csharp
protected override async Task OnInitializedAsync()
{
if (!await AuthService.IsAuthenticatedAsync())
{
await AuthDialogService.ShowAuthDialogAsync();
}
// ادامه بارگذاری...
}
```
این pattern روی:
- `Store/Cart.razor.cs`
- `Store/CheckoutSummary.razor.cs`
- `DiscountStore/Cart.razor.cs`
- `DiscountStore/Checkout.razor.cs`
اعمال شد. اگر guest مستقیماً وارد سبد خرید شود، مودال لاگین نشان داده می‌شود.
### ۶. `MembershipPage.razor` (fix متنی)
```diff
- شارژ ۵۶ میلیون تومان کیف پول فروشگاه اعتباری
+ شارژ برابر ارزش پکیج فعال در کیف پول فروشگاه اعتباری
```
متن hardcode‌شده با مقدار دینامیک جایگزین شد.
---
## معماری GuestActionGate
```
کاربر کلیک می‌کند
GuestActionGate.RunAsync(action)
├─► آیا لاگین است؟ ──YES──► action() اجرا می‌شود ✅
NO
AuthDialogService.ShowAuthDialogAsync()
(مودال OTP باز می‌شود)
├─► آیا لاگین شد؟ ──YES──► action() اجرا می‌شود ✅
NO (بستن مودال)
return false (هیچ اتفاقی نمی‌افتد) ❌
```
این pattern **defense-in-depth** است: `CartService.Add` هم به تنهایی چک `IsAuthenticatedAsync` دارد؛ `GuestActionGate` لایه UX روی آن اضافه می‌کند.
---
## فلوچارت تجربه کاربر
```
کاربر وارد لندینگ پیج می‌شود (بدون لاگین)
├─► ۶ محصول پرفروش معمولی نمایش داده می‌شود
├─► ۶ محصول پرفروش اعتباری نمایش داده می‌شود
├─► "بیشتر" کلیک → /products یا /discount-store
│ (صفحات لیست کامل، بدون لاگین قابل مرور)
├─► روی محصول کلیک → صفحه جزئیات
│ (بدون لاگین قابل مشاهده)
└─► "افزودن به سبد" کلیک
مودال لاگین (OTP)
├─► ورود موفق → محصول به سبد اضافه می‌شود ✅
└─► بستن مودال → هیچ اتفاقی نمی‌افتد
```
---
## فایل‌های تغییر یافته
### CMS — کامیت `683ed37`
```
src/CMSMicroservice.Protobuf/Protos/discountproduct.proto (+2)
src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj (~2)
src/CMSMicroservice.Application/DiscountShopCQ/Queries/
GetDiscountProducts/GetDiscountProductsQuery.cs (+1)
GetDiscountProducts/GetDiscountProductsQueryHandler.cs (+7 -3)
src/CMSMicroservice.WebApi/Common/Mappings/DiscountProductProfile.cs (+3)
```
### FrontOffice — کامیت `231da2c`
```
src/FrontOffice.Main/Utilities/GuestActionGate.cs (NEW +42)
src/FrontOffice.Main/ConfigureServices.cs (+1)
src/FrontOffice.Main/Utilities/ProductService.cs (+3)
src/FrontOffice.Main/Utilities/DiscountProductService.cs (+8)
src/FrontOffice.Main/Pages/Index.razor (+~180)
src/FrontOffice.Main/Pages/Index.razor.cs (+45)
src/FrontOffice.Main/Pages/Store/Products.razor.cs (+5)
src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs (+5)
src/FrontOffice.Main/Pages/Store/Cart.razor.cs (+8)
src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs (+8)
src/FrontOffice.Main/Pages/DiscountStore/Products.razor.cs (+5)
src/FrontOffice.Main/Pages/DiscountStore/ProductDetail.razor.cs (+5)
src/FrontOffice.Main/Pages/DiscountStore/Cart.razor.cs (+8)
src/FrontOffice.Main/Pages/DiscountStore/Checkout.razor.cs (+8)
src/FrontOffice.Main/Pages/Club/MembershipPage.razor (~1)
```