docs: update BUSINESS-02, BUSINESS-03, TECH-03

BUSINESS-02:
- فرمول هایبرید: حذف MIN، اضافه validation کیف‌پول اعتباری
- فلوی خرید: اضافه مرحله بررسی موجودی + UserWalletChangeLog
- نام‌گذاری جدید کیف‌پول‌ها: اصلی، اعتباری، پاداش تیمی
- جدول وضعیت: اضافه WalletChangeLog + Validation

BUSINESS-03:
- بخش ۹ جدید: ExpirePendingOrdersService (۱۵ دقیقه)
- دیاگرام Mermaid فلوی انقضا

TECH-03:
- فیکس URL پروداکشن (kbs1→kbs2) + هشدار
- ۴ کامیت جدید در بخش ۹.۲
- بخش ۹.۳ فیکس URL پروداکشن
- بخش ۹.۴ نام‌گذاری کیف‌پول‌ها
This commit is contained in:
masoodafar-web
2026-02-24 00:29:03 +03:30
parent 0115142faf
commit c78850f86e
4 changed files with 671 additions and 11 deletions
+554
View File
@@ -0,0 +1,554 @@
# 📦 سیستم مبتنی بر پکیج (Package-Based System)
> **وضعیت:** تحلیل و بررسی — منتظر تایید
> **تاریخ:** اسفند ۱۴۰۴
> **تاثیرگذاری:** زیاد — بخش‌های متعدد سیستم تحت تاثیر قرار می‌گیرد
---
## ۱. خلاصه فیچر
**وضعیت فعلی:** سیستم فقط یک پکیج پایه (۵۶ میلیون تومان) دارد و همه چیز حول آن می‌چرخد.
**وضعیت هدف:** سیستم چندین پکیج با قیمت‌ها و ویژگی‌های متفاوت پشتیبانی می‌کند. هر پکیج روش‌های پرداخت، محاسبه پورسانت، شارژ کیف پول و فیچرهای مختص خود را دارد.
```
مثال پکیج‌ها:
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ 🥈 نقره‌ای │ 🥇 طلایی │ 💎 الماسی │ ⭐ ویژه │
│ ۵.۶M تومان │ ۵۶M تومان │ ؟؟ تومان │ ؟؟ تومان │
│ │ (پکیج پایه) │ │ │
│ فقط مستقیم │ دایا+مستقیم │ فقط مستقیم │ فقط مستقیم │
│ فیچر محدود │ همه فیچرها │ همه فیچرها │ همه+اختصاصی │
└──────────────┴──────────────┴──────────────┴──────────────┘
```
---
## ۲. وضعیت فعلی سیستم (AS-IS)
### ۲.۱ فلوی فعلی فعالسازی
```mermaid
flowchart TD
A["کاربر وارد سیستم می‌شود"] --> B{"روش پرداخت"}
B -->|"خرید الماس دایا"| C["DayaLoan — ۵۶M"]
B -->|"پرداخت مستقیم"| D["درگاه بانکی — ۵۶M"]
C --> E["بررسی موفقیت پرداخت"]
D --> E
E --> F["شارژ کیف پول"]
F --> G["مدال قرارداد باشگاه مشتریان"]
G --> H["تایید OTP + امضا"]
H --> I["فعال‌سازی عضویت باشگاه"]
I --> J["اختصاص فیچرها"]
I --> K["ایجاد Cycle"]
I --> L["اضافه به Commission Pool"]
J --> M["✅ کاربر فعال — لینک معرف"]
```
### ۲.۲ جریان پول فعلی
```
کاربر ۵۶M پرداخت می‌کند
├── Balance (کیف پول عادی) += ۵۶,۰۰۰,۰۰۰ ریال
├── DiscountBalance (اعتباری) += ۱۱۲,۰۰۰,۰۰۰ ریال (×۲)
└── Club Activation:
├── CommissionPool += ۲۵,۲۰۰,۰۰۰ ریال (ClubActivationFee)
└── GiftValue = ۲۵,۲۰۰,۰۰۰ ریال (اطلاع‌رسانی)
```
### ۲.۳ مقادیر Hardcoded فعلی (`SystemConstants.cs`)
| ثابت | مقدار | کاربرد |
|------|-------|--------|
| `BasePackageAmount` | ۵۶,۰۰۰,۰۰۰ | قیمت پکیج |
| `DayaLoanAmount` | ۵۶,۰۰۰,۰۰۰ | مبلغ وام دایا |
| `ClubActivationFee` | ۲۵,۲۰۰,۰۰۰ | سهم هفتگی Commission Pool |
| `ClubMembershipGiftValue` | ۲۵,۲۰۰,۰۰۰ | ارزش هدیه حق عضویت |
| `MagicWalletMultiplier` | ×۲.۵ | ضریب کیف پول جادویی |
### ۲.۴ مشکلات فعلی
| # | مشکل | فایل |
|---|------|------|
| ۱ | پکیج ID=4 **hardcoded** در `InitiateBasePackagePaymentCommandHandler` | Application/Commands |
| ۲ | مبلغ ۵۶M **hardcoded** در `SystemConstants` و چندین handler | Domain/Common |
| ۳ | فیچرها **همه یکجا** assign می‌شن (۴ فیچر ثابت: چتیکا، بیمه، تریپ، لرن) | ActivateClubMembershipHandler |
| ۴ | Commission Pool فقط با `ClubActivationFee` ثابت پر می‌شه | ActivateClubMembershipHandler |
| ۵ | `DiscountBalance = Amount × 2` — ضریب hardcoded | VerifyPayment handlers |
| ۶ | فرانت‌اند فقط یک مسیر خرید نشون میده | FrontOffice pages |
---
## ۳. طراحی پیشنهادی (TO-BE)
### ۳.۱ فلوی جدید فعالسازی
```mermaid
flowchart TD
A["کاربر وارد سیستم"] --> B["صفحه پکیج‌ها<br/>(کاشی‌های نقره‌ای/طلایی/الماسی/...)"]
B -->|"کلیک روی پکیج"| C{"نوع پکیج"}
C -->|"پکیج پایه (طلایی)"| D["مدال با دو گزینه:<br/>۱. خرید الماس دایا<br/>۲. پرداخت مستقیم"]
C -->|"پکیج‌های دیگر"| E["مدال با یک گزینه:<br/>فقط پرداخت مستقیم<br/>+ توضیحات + قیمت"]
D -->|"دایا"| F["فلوی دایا"]
D -->|"مستقیم"| G["درگاه پرداخت"]
E --> G
F --> H["پرداخت موفق"]
G --> H
H --> I["شارژ کیف پول<br/>(متناسب با قیمت پکیج)"]
I --> J["مدال قرارداد باشگاه"]
J --> K["OTP + امضا"]
K --> L["فعال‌سازی<br/>+ اختصاص فیچرهای پکیج"]
L --> M["✅ کاربر فعال"]
```
### ۳.۲ تغییرات Entity — Package
**فعلی:**
```csharp
public class Package : BaseAuditableEntity
{
public string Title { get; set; }
public string Description { get; set; }
public string ImagePath { get; set; }
public long Price { get; set; }
}
```
**پیشنهادی:**
```csharp
public class Package : BaseAuditableEntity
{
public string Title { get; set; }
public string Description { get; set; }
public string ImagePath { get; set; }
public long Price { get; set; } // قیمت پکیج (ریال)
// === فیلدهای جدید ===
public int SortOrder { get; set; } // ترتیب نمایش
public bool IsActive { get; set; } = true; // فعال/غیرفعال
public bool IsBasePackage { get; set; } // آیا پکیج پایه است؟
public bool SupportsDayaPurchase { get; set; } // پشتیبانی از خرید دایا
public bool SupportsDirectPurchase { get; set; } = true; // پشتیبانی از پرداخت مستقیم
// === محاسبات مالی ===
public long ActivationFee { get; set; } // سهم Commission Pool
public long GiftValue { get; set; } // ارزش هدیه
public decimal DiscountMultiplier { get; set; } = 2.0m; // ضریب شارژ DiscountBalance
// === Navigation ===
public virtual ICollection<PackageFeature> PackageFeatures { get; set; }
public virtual ICollection<UserPackagePurchase> Purchases { get; set; }
}
```
### ۳.۳ Entity جدید — PackageFeature (پل بین پکیج و فیچر)
```csharp
/// <summary>
/// مشخص می‌کند هر پکیج چه فیچرهایی را فعال می‌کند
/// </summary>
public class PackageFeature : BaseAuditableEntity
{
public long PackageId { get; set; }
public virtual Package Package { get; set; }
public long ClubFeatureId { get; set; }
public virtual ClubFeature ClubFeature { get; set; }
public bool IsIncluded { get; set; } = true; // آیا این فیچر در پکیج هست؟
}
```
### ۳.۴ تغییرات Entity — ClubMembership
```csharp
public class ClubMembership : BaseAuditableEntity
{
// ... فیلدهای فعلی حفظ می‌شوند ...
// === فیلد جدید ===
public long PackageId { get; set; } // کدام پکیج خریداری شده
public virtual Package Package { get; set; }
}
```
### ۳.۵ تغییرات Entity — ClubMembershipCycle
```csharp
public class ClubMembershipCycle : BaseAuditableEntity
{
// ... فیلدهای فعلی حفظ می‌شوند ...
// === فیلد جدید ===
public long PackageId { get; set; } // پکیج این سایکل
public virtual Package Package { get; set; }
// PackageAmount قبلاً وجود دارد — از Package.Price پر می‌شود
}
```
### ۳.۶ تغییرات Entity — WeeklyCommissionPool
```csharp
public class WeeklyCommissionPool : BaseAuditableEntity
{
// ... فیلدهای فعلی حفظ می‌شوند ...
// === فیلد جدید ===
public long PackageId { get; set; } // Pool جداگانه برای هر پکیج
public virtual Package Package { get; set; }
}
```
### ۳.۷ جریان پول جدید
```
پکیج نقره‌ای (۵.۶M):
├── Balance += ۵,۶۰۰,۰۰۰
├── DiscountBalance += ۱۱,۲۰۰,۰۰۰ (×۲)
└── CommissionPool += ActivationFee مخصوص نقره‌ای
پکیج طلایی/پایه (۵۶M):
├── Balance += ۵۶,۰۰۰,۰۰۰
├── DiscountBalance += ۱۱۲,۰۰۰,۰۰۰ (×۲)
└── CommissionPool += ۲۵,۲۰۰,۰۰۰
پکیج الماسی (??M):
├── Balance += ??
├── DiscountBalance += ?? (×۲)
└── CommissionPool += ActivationFee مخصوص الماسی
```
---
## ۴. محاسبه پورسانت — تغییرات
### ۴.۱ وضعیت فعلی
```
یک WeeklyCommissionPool برای کل هفته
TotalAmount = مجموع ActivationFee همه فعالسازی‌ها
ValuePerBalance = TotalAmount ÷ مجموع Balance‌ها
همه یکسان محاسبه می‌شوند
```
### ۴.۲ وضعیت هدف
```
برای هر پکیج، یک WeeklyCommissionPool جداگانه:
Pool_نقره‌ای:
TotalAmount = مجموع ActivationFee خریداران نقره‌ای این هفته
Balance‌ها = فقط از شبکه خریداران نقره‌ای
ValuePerBalance = Pool_نقره‌ای ÷ Balance_نقره‌ای
Pool_طلایی:
TotalAmount = مجموع ActivationFee خریداران طلایی این هفته
Balance‌ها = فقط از شبکه خریداران طلایی
ValuePerBalance = Pool_طلایی ÷ Balance_طلایی
```
### ۴.۳ نکته مهم: ساختار شبکه یکی است
```
[Ali]
/ \
[Sara] [Reza] ← شبکه باینری یکی‌ست
/ \ / \
[M1] [M2] [M3] [M4]
ولی محاسبات جدا:
- Ali با پکیج طلایی → پورسانت از Pool طلایی
- Sara با پکیج نقره‌ای → پورسانت از Pool نقره‌ای
- Reza با پکیج طلایی → پورسانت از Pool طلایی
```
### ۴.۴ تغییرات Stored Procedure
**`sp_CalculateWeeklyBalances`** باید:
- پارامتر `@PackageId` بگیرد
- فقط کاربرانی که این پکیج را خریده‌اند فیلتر کند
- برای هر پکیج جداگانه اجرا شود
**`sp_CalculateWeeklyCommissionPool`** باید:
- پارامتر `@PackageId` بگیرد
- Pool مخصوص آن پکیج را بخواند
- پرداخت‌ها فقط به خریداران آن پکیج اختصاص یابد
---
## ۵. فیچرهای باشگاه مشتریان بر اساس پکیج
### ۵.۱ وضعیت فعلی
وقتی کاربر فعال می‌شود، **همه ۴ فیچر** یکجا assign می‌شوند:
```csharp
// ActivateClubMembershipCommandHandler — خط ~350
var allFeatureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
foreach (var featureId in allFeatureIds)
{
userClubFeatures.Add(new UserClubFeature { ... });
}
```
### ۵.۲ وضعیت هدف
فیچرها بر اساس جدول `PackageFeature` تعیین می‌شوند:
| فیچر | نقره‌ای | طلایی (پایه) | الماسی |
|------|---------|-------------|--------|
| چتیکا | ❌ | ✅ | ✅ |
| بیمه | ❌ | ✅ | ✅ |
| تریپ | ✅ | ✅ | ✅ |
| لرن | ✅ | ✅ | ✅ |
| فیچر VIP | ❌ | ❌ | ✅ |
*مقادیر بالا نمونه‌ای هستند — قابل تنظیم از BackOffice*
### ۵.۳ تغییر در ActivateClubMembershipHandler
```
قبلی:
GetAllFeatureIds() → assign all
جدید:
Package.PackageFeatures
.Where(pf => pf.IsIncluded)
.Select(pf => pf.ClubFeatureId)
→ assign only included features
```
---
## ۶. تغییرات UI — FrontOffice
### ۶.۱ صفحه پکیج‌ها (کاشی‌ها)
```
┌─────────────────────────────────────────────────────┐
│ انتخاب پکیج باشگاه مشتریان │
├─────────────┬──────────────┬──────────────┬─────────┤
│ │ │ │ │
│ 🥈 نقره‌ای │ 🥇 طلایی │ 💎 الماسی │ ⭐ ویژه │
│ ۵.۶M │ ۵۶M │ ؟؟M │ ؟؟M │
│ │ │ │ │
│ ● لرن │ ● چتیکا │ ● همه │ ● همه │
│ ● تریپ │ ● بیمه │ ● + VIP │ ● +... │
│ │ ● تریپ │ │ │
│ │ ● لرن │ │ │
│ │ │ │ │
│ [انتخاب] │ [انتخاب] │ [انتخاب] │[انتخاب]│
└─────────────┴──────────────┴──────────────┴─────────┘
```
### ۶.۲ مدال پرداخت — پکیج پایه (طلایی)
```
┌─────────────────────────────────────────┐
│ خرید پکیج طلایی — ۵۶M تومان │
│ │
│ توضیحات: ... │
│ │
│ روش‌های پرداخت: │
│ ┌─────────────────────────────────┐ │
│ │ 💎 خرید از طریق الماس دایا │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ 💳 پرداخت مستقیم (درگاه بانکی) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
### ۶.۳ مدال پرداخت — پکیج‌های دیگر (نقره‌ای و بالاتر)
```
┌─────────────────────────────────────────┐
│ خرید پکیج نقره‌ای — ۵.۶M تومان │
│ │
│ توضیحات: ... │
│ ویژگی‌ها: لرن، تریپ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ 💳 پرداخت و فعال‌سازی │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
---
## ۷. بخش‌های تحت تاثیر (Impact Analysis)
### ۷.۱ جدول تاثیرپذیری
| # | لایه | فایل/بخش | نوع تغییر | شدت |
|---|------|----------|-----------|-----|
| ۱ | **Domain** | `Package.cs` | اضافه فیلد | 🟡 متوسط |
| ۲ | **Domain** | `PackageFeature.cs`**جدید** | Entity جدید | 🔴 زیاد |
| ۳ | **Domain** | `ClubMembership.cs` | اضافه `PackageId` | 🟡 متوسط |
| ۴ | **Domain** | `ClubMembershipCycle.cs` | اضافه `PackageId` | 🟡 متوسط |
| ۵ | **Domain** | `WeeklyCommissionPool.cs` | اضافه `PackageId` | 🔴 زیاد |
| ۶ | **Domain** | `SystemConstants.cs` | حذف hardcode‌ها → خوانش از Package | 🟡 متوسط |
| ۷ | **Application** | `ActivateClubMembershipCommandHandler` | فیچر بر اساس پکیج | 🔴 زیاد |
| ۸ | **Application** | `InitiateBasePackagePaymentCommandHandler` | حذف ID=4 hardcoded | 🟡 متوسط |
| ۹ | **Application** | `VerifyBasePackagePaymentCommandHandler` | شارژ متناسب با پکیج | 🔴 زیاد |
| ۱۰ | **Application** | `VerifyPackagePurchasePaymentCommandHandler` | شارژ متناسب با پکیج | 🔴 زیاد |
| ۱۱ | **Application** | `ManualPaymentCommandHandler` | شارژ متناسب با پکیج | 🟡 متوسط |
| ۱۲ | **Application** | `CustomerPurchasePackageCommandHandler` | پشتیبانی روش‌های پرداخت پکیج | 🟡 متوسط |
| ۱۳ | **Infra** | `sp_CalculateWeeklyBalances` | پارامتر PackageId | 🔴 زیاد |
| ۱۴ | **Infra** | `sp_CalculateWeeklyCommissionPool` | Pool جداگانه هر پکیج | 🔴 زیاد |
| ۱۵ | **Infra** | `WeeklyCommissionCalculationService` | Loop روی پکیج‌ها | 🟡 متوسط |
| ۱۶ | **Infra** | EF Configurations | جدول جدید + FK‌ها | 🟡 متوسط |
| ۱۷ | **Infra** | Database Migration | schema changes | 🟡 متوسط |
| ۱۸ | **Proto** | `package.proto` | فیلدهای جدید پکیج | 🟢 کم |
| ۱۹ | **Proto** | `clubmembership.proto` | PackageId در response | 🟢 کم |
| ۲۰ | **Proto** | `commission.proto` | PackageId در pool/payout | 🟢 کم |
| ۲۱ | **FrontOffice** | صفحه انتخاب پکیج | UI جدید (کاشی‌ها) | 🔴 زیاد |
| ۲۲ | **FrontOffice** | مدال پرداخت | دو مدال متفاوت | 🔴 زیاد |
| ۲۳ | **FrontOffice** | `MyPackages.razor` | نمایش نوع پکیج | 🟡 متوسط |
| ۲۴ | **FrontOffice** | `ActivateClubDialog.razor` | ارتباط با پکیج | 🟡 متوسط |
| ۲۵ | **BackOffice** | صفحه مدیریت پکیج‌ها | CRUD فیلدهای جدید | 🟡 متوسط |
| ۲۶ | **BackOffice** | صفحه فیچر پکیج‌ها — **جدید** | ماتریس پکیج×فیچر | 🔴 زیاد |
| ۲۷ | **BackOffice** | `ActivateClubDialog.razor` | انتخاب پکیج | 🟡 متوسط |
### ۷.۲ ریسک‌ها
| ریسک | احتمال | شدت | راه‌حل |
|------|--------|-----|--------|
| داده‌های فعلی — کاربران بدون PackageId | قطعی | زیاد | Migration: کاربران فعلی → PackageId = پکیج پایه |
| Commission Pool فعلی بدون PackageId | قطعی | زیاد | Migration: Pool‌های موجود → PackageId = پکیج پایه |
| SP تغییر → محاسبات اشتباه | متوسط | بحرانی | تست جامع + محیط staging |
| مبالغ hardcoded در جاهای پراکنده | زیاد | متوسط | Audit کامل کدبیس |
| عدم سازگاری FrontOffice/BackOffice | متوسط | متوسط | تست end-to-end |
---
## ۸. فازبندی پیاده‌سازی
### فاز ۱ — زیرساخت (Domain + DB) ≈ ۳-۴ روز
| تسک | شرح |
|-----|------|
| T1.1 | بروزرسانی `Package` entity (فیلدهای جدید) |
| T1.2 | ایجاد `PackageFeature` entity + EF Configuration |
| T1.3 | اضافه کردن `PackageId` به `ClubMembership` |
| T1.4 | اضافه کردن `PackageId` به `ClubMembershipCycle` |
| T1.5 | اضافه کردن `PackageId` به `WeeklyCommissionPool` |
| T1.6 | Database Migration + Seed data (پکیج پایه + فیچرها) |
| T1.7 | Migration: کاربران/Pool‌های فعلی → PackageId = پکیج پایه |
| T1.8 | بروزرسانی Proto‌ها |
### فاز ۲ — منطق کسب‌وکار (Application) ≈ ۴-۵ روز
| تسک | شرح |
|-----|------|
| T2.1 | بروزرسانی `ActivateClubMembershipCommandHandler` — فیچر بر اساس پکیج |
| T2.2 | بروزرسانی Verify handlers — شارژ کیف پول متناسب با پکیج |
| T2.3 | حذف مقادیر hardcoded از `SystemConstants` → خوانش از Package |
| T2.4 | بروزرسانی `InitiateBasePackagePayment` → Generic `InitiatePackagePayment` |
| T2.5 | بروزرسانی `ManualPaymentCommandHandler` — پشتیبانی پکیج متغیر |
| T2.6 | CRUD پکیج با فیلدهای جدید (gRPC handlers) |
| T2.7 | CRUD `PackageFeature` (ماتریس پکیج×فیچر) |
### فاز ۳ — محاسبه پورسانت ≈ ۳-۴ روز
| تسک | شرح |
|-----|------|
| T3.1 | بروزرسانی `sp_CalculateWeeklyBalances` — فیلتر بر اساس PackageId |
| T3.2 | بروزرسانی `sp_CalculateWeeklyCommissionPool` — Pool جداگانه |
| T3.3 | بروزرسانی `WeeklyCommissionCalculationService` — Loop روی پکیج‌ها |
| T3.4 | تست محاسبات با داده واقعی |
### فاز ۴ — UI (FrontOffice + BackOffice) ≈ ۴-۵ روز
| تسک | شرح |
|-----|------|
| T4.1 | صفحه کاشی‌های پکیج (FrontOffice) |
| T4.2 | مدال پرداخت پکیج پایه (دایا + مستقیم) |
| T4.3 | مدال پرداخت پکیج‌های دیگر (فقط مستقیم) |
| T4.4 | بروزرسانی `MyPackages.razor` — نمایش نوع پکیج |
| T4.5 | بروزرسانی `ActivateClubDialog.razor` — ارتباط با پکیج |
| T4.6 | BackOffice: CRUD پکیج با فیلدهای جدید |
| T4.7 | BackOffice: صفحه ماتریس فیچرهای پکیج |
| T4.8 | BackOffice: `ActivateClubDialog` — انتخاب پکیج |
### فاز ۵ — تست و استقرار ≈ ۲-۳ روز
| تسک | شرح |
|-----|------|
| T5.1 | تست end-to-end فلوی خرید هر پکیج |
| T5.2 | تست محاسبه پورسانت جداگانه |
| T5.3 | تست migration داده‌های فعلی |
| T5.4 | Deploy به staging + تست |
| T5.5 | Deploy به production |
---
## ۹. Seed Data — پکیج‌های اولیه
```sql
-- Migration: Seed packages
INSERT INTO Packages (Title, Description, Price, IsActive, IsBasePackage,
SupportsDayaPurchase, SupportsDirectPurchase, ActivationFee, GiftValue,
DiscountMultiplier, SortOrder)
VALUES
('نقره‌ای', 'پکیج نقره‌ای باشگاه مشتریان', 5600000, 1, 0,
0, 1, ???, ???, 2.0, 1),
('طلایی', 'پکیج طلایی باشگاه مشتریان (پایه)', 56000000, 1, 1,
1, 1, 25200000, 25200000, 2.0, 2);
-- Migration: ربط فیچرها به پکیج‌ها
INSERT INTO PackageFeatures (PackageId, ClubFeatureId, IsIncluded) VALUES
-- نقره‌ای: فقط تریپ و لرن
(@silverId, @tripId, 1),
(@silverId, @learnId, 1),
-- طلایی: همه فیچرها
(@goldId, @chatikaId, 1),
(@goldId, @bimeId, 1),
(@goldId, @tripId, 1),
(@goldId, @learnId, 1);
-- Migration: کاربران فعلی → پکیج پایه
UPDATE ClubMemberships SET PackageId = @goldId WHERE PackageId IS NULL;
UPDATE ClubMembershipCycles SET PackageId = @goldId WHERE PackageId IS NULL;
UPDATE WeeklyCommissionPools SET PackageId = @goldId WHERE PackageId IS NULL;
```
---
## ۱۰. سوالات باز (نیاز به تصمیم‌گیری)
| # | سوال | گزینه‌ها |
|---|------|---------|
| ۱ | `ActivationFee` و `GiftValue` پکیج نقره‌ای چقدر باشد؟ | نسبت به قیمت؟ مقدار ثابت؟ |
| ۲ | آیا کاربر می‌تواند بعداً پکیج خود را ارتقا دهد (upgrade)؟ | بله → فقط مابه‌التفاوت / خیر |
| ۳ | `DiscountMultiplier` برای همه پکیج‌ها ×۲ باشد؟ | یکسان / متفاوت به ازای هر پکیج |
| ۴ | ضریب `MagicWallet` (×۲.۵) برای پکیج‌های کوچکتر هم همان باشد؟ | بله / خیر |
| ۵ | فیچرهای پکیج نقره‌ای دقیقاً کدام‌ها هستند؟ | لرن+تریپ؟ فقط لرن؟ |
| ۶ | آیا یک کاربر می‌تواند چند پکیج همزمان داشته باشد؟ | فقط یکی / امکان خرید چندتا |
| ۷ | نام و تعداد دقیق پکیج‌ها چیست؟ | نقره‌ای+طلایی؟ بیشتر؟ |
| ۸ | کاربرانی که با دایا فعال شدن، چه پکیجی دارند؟ | طلایی (پایه) |
---
## ۱۱. تخمین زمانی
| فاز | مدت | وابستگی |
|-----|------|---------|
| فاز ۱ — زیرساخت | ۳-۴ روز | — |
| فاز ۲ — منطق | ۴-۵ روز | فاز ۱ |
| فاز ۳ — پورسانت | ۳-۴ روز | فاز ۱ |
| فاز ۴ — UI | ۴-۵ روز | فاز ۲ |
| فاز ۵ — تست | ۲-۳ روز | فاز ۳, ۴ |
| **مجموع** | **~۱۶-۲۱ روز کاری** | |
> فازهای ۲ و ۳ قابل موازی‌سازی هستند.
+42 -9
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`
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: فعال‌سازی درگاه ZarinPal + تنظیمات محیطی)
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: فیکس WalletChangeLog + validation کیف‌پول اعتباری + نام‌گذاری جدید کیف‌پول‌ها)
---
@@ -24,9 +24,9 @@ flowchart TD
PYMS --> WALLETS
subgraph WALLETS["3 Wallet System"]
W1["💰 Balance\nنقدی"]
W2["🌟 NetworkBalance\nشبکه"]
W3["🏷️ DiscountBalance\nاعتباری"]
W1["💰 Balance\nکیف پول اصلی"]
W2["🌟 NetworkBalance\nپاداش تیمی"]
W3["🏷️ DiscountBalance\nکیف پول اعتباری"]
end
```
@@ -137,7 +137,11 @@ flowchart TD
قیمت محصول = 1,000,000 ریال
MaxDiscountPercent محصول = 40% (هر محصول درصد تخفیف مخصوص خود را دارد)
سهم تخفیف = MIN(1,000,000 × 40%, DiscountBalanceکاربر) = 400,000
سهم تخفیف = قیمت × MaxDiscountPercent% = 400,000
⚠️ اگر DiscountBalance < سهم تخفیف → خطا: «موجودی کیف پول اعتباری کافی نیست»
(دیگر MIN استفاده نمی‌شود — کاربر باید موجودی کافی داشته باشد)
باقیمانده → ZarinPal IPG = 1,000,000 - 400,000 = 600,000
─────────
مجموع = 1,000,000
@@ -151,11 +155,15 @@ MaxDiscountPercent محصول = 40% (هر محصول درصد تخفیف مخص
flowchart TD
A["کاربر عضو باشگاه\nمشاهده محصول"] --> B["قیمت تخفیف‌خورده نمایش داده می‌شود"]
B --> C["افزودن به سبد\nمحاسبه MaxDiscount% هر محصول"]
C --> D["سهم تخفیف = MIN(قیمت×MaxDiscount%, DiscountBalance)"]
D --> E["باقیمانده = مجموع - سهم تخفیف"]
C --> D["سهم تخفیف = قیمت × MaxDiscount%"]
D --> V{"DiscountBalance >= سهم تخفیف?"}
V -->|خیر| X["❌ خطا: موجودی کیف پول اعتباری کافی نیست"]
V -->|بله| E["باقیمانده = مجموع - سهم تخفیف"]
E --> F{"باقیمانده > 0?"}
F -->|بله| G["کسر DiscountBalance\n+ Redirect → ZarinPal IPG\nباقیمانده + 10% VAT"]
F -->|خیر| H["فقط کسر از DiscountBalance\nبدون درگاه → ثبت مستقیم"]
G --> LOG["ثبت UserWalletChangeLog"]
H --> LOG
```
### ۵.۳ دسترسی فروشگاه اعتباری
@@ -164,8 +172,21 @@ flowchart TD
|------|--------|
| `IsClubMember = true` | دسترسی به Discount Store |
| `IsClubMember = false` | فقط Regular Store |
| `DiscountBalance > 0` | می‌تواند از تخفیف استفاده کند |
| `DiscountBalance = 0` | پرداخت ۱۰۰% از طریق ZarinPal IPG |
| `DiscountBalance >= سهم تخفیف` | خرید مجاز |
| `DiscountBalance < سهم تخفیف` | ❌ خطا: موجودی کیف پول اعتباری کافی نیست |
### ۵.۴ UserWalletChangeLog (اسفند ۱۴۰۴ — فیکس)
> **باگ:** هنگام خرید از فروشگاه اعتباری، `DiscountBalance` در دیتابیس کم می‌شد ولی هیچ
> `UserWalletChangeLog` ثبت نمی‌شد → کاربر در تاریخچه کیف‌پول چیزی نمی‌دید.
فیکس در ۳ هندلر:
| هندلر | سناریو | فیکس |
|--------|---------|------|
| `PlaceOrderCommandHandler` | پرداخت کامل با DiscountBalance (بدون درگاه) | ✅ ثبت log با `ChangeDiscountValue = -amount` |
| `CompleteOrderPaymentCommandHandler` | پرداخت ترکیبی (درگاه + DiscountBalance) | ✅ ثبت log بعد از verify موفق درگاه |
| `VerifyDiscountWalletChargeCommandHandler` | شارژ کیف‌پول اعتباری | ✅ ثبت log با `ChangeDiscountValue = +amount` |
---
@@ -221,6 +242,18 @@ service PaymentService {
| وام دایا | ✅ کامل | Mock mode فعال در staging |
| پرداخت ترکیبی | ✅ کامل | Discount + IPG |
| Pool هفتگی | ✅ کامل | SP + Hangfire |
| WalletChangeLog | ✅ فیکس شده | لاگ تغییرات کیف‌پول در ۳ هندلر اضافه شد |
| Validation کیف‌پول اعتباری | ✅ فیکس شده | ارور اگر موجودی کافی نباشد |
| پرداخت دستی | ⬜ طراحی | نیاز به تصمیم مدیریت |
| Refund | ⬜ طراحی | فقط در PYMS تعریف‌شده |
| کیف‌پول جادویی (Magic) | ✅ کامل | فاز 1-6 پیاده‌سازی شده — Production فعال |
### ۸.۱ نام‌گذاری استاندارد کیف‌پول‌ها (اسفند ۱۴۰۴)
| فیلد دیتابیس | نام قدیم (UI) | نام جدید (UI) |
|-------------|--------------|---------------|
| `Balance` | عادی / اعتباری / نقدی | **کیف پول اصلی** |
| `DiscountBalance` | تخفیفی / تخفیف | **کیف پول اعتباری** |
| `NetworkBalance` | شبکه / طلایی / پورسانت | **پاداش تیمی** |
> تغییرات UI در ۱۲ فایل (FrontOffice: 5, BackOffice: 7) اعمال شد.
+40 -1
View File
@@ -1,7 +1,7 @@
# 🛒 فروشگاه، موجودی و محصولات
> **منابع ادغام‌شده:** `discount-shop-business.md`, `DISCOUNT-STORE-STATUS.md`, `package-purchase-system.md`, `INVENTORY-IMPROVEMENTS.md`, `INVENTORY-REFACTORING-STATUS.md`, `PRODUCT-BUNDLE-FEATURE.md`, `SHOP-UNIFICATION.md`
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: VAT 10%)
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: ExpirePendingOrders ۱۵ دقیقه + فروشگاه اعتباری نام‌گذاری)
---
@@ -226,5 +226,44 @@ graph TD
| Lazy Loading | ✅ کامل | 100% |
| موجودی خودکار | ✅ کامل | 100% |
| تصاویر مربعی | ✅ کامل | 100% |
| انقضای سفارشات Pending | ✅ کامل | 100% |
| باندل محصولات | ⬜ طراحی | 30% |
| مقایسه محصول | ⬜ ایده | 0% |
---
## ۹. انقضای خودکار سفارشات Pending (ExpirePendingOrdersService)
> سرویس پس‌زمینه‌ای که سفارشات فروشگاه اعتباری را بعد از ۱۵ دقیقه منقضی می‌کند.
### ۹.۱ پارامترها
| پارامتر | مقدار | توضیح |
|---------|-------|-------|
| `ExpirationTime` | **۱۵ دقیقه** | مدت زمان مجاز برای پرداخت |
| `CheckInterval` | ۵ دقیقه | فاصله بررسی |
### ۹.۲ عملکرد
```mermaid
flowchart TD
A["هر ۵ دقیقه\nExpirePendingOrdersService"] --> B["جستجوی DiscountOrders\nPaymentStatus=Pending\nCreated < (now - 15 min)"]
B --> C{"سفارشی یافت شد?"}
C -->|خیر| A
C -->|بله| D["آزادسازی رزرو موجودی\nReleaseReservationAsync"]
D --> E["PaymentStatus → Reject\nDeliveryStatus → Cancelled"]
E --> F["Transaction.PaymentStatus → Reject"]
F --> G["Log: Expired order #X"]
G --> A
```
### ۹.۳ فایل
```
CMS/src/CMSMicroservice.Infrastructure/BackgroundServices/ExpirePendingOrdersService.cs
```
رجیستر شده در `ConfigureServices.cs`:
```csharp
services.AddHostedService<ExpirePendingOrdersService>();
```
+35 -1
View File
@@ -1,7 +1,7 @@
# 🚀 استقرار، CI/CD و زیرساخت
> **منابع ادغام‌شده:** `CICD-PIPELINE-GUIDE.md`, `DEPLOYMENT-README.md`, `INFRASTRUCTURE-GUIDE.md`, `INGRESS-NGINX-WARNING.md`, `OFFLINE-DEPLOYMENT-GUIDE.md`, `SERVER-MIRRORS-CONFIG.md`
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: PVC آپلود + K8s Secret برای config دائمی + جدا کردن appsettings هر برنچ)
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: فیکس URL پروداکشن + چری‌پیک فیکس‌های WalletChangeLog/Validation/Expiry)
---
@@ -473,21 +473,55 @@ flowchart TD
| `9288d06` | feat: externalize appsettings to K8s Secret — config persists independently |
| `e72673c` | chore(staging): remove appsettings.Production.json (فقط kub-stage) |
| `3ebe0f9` | chore(production): remove appsettings.Staging.json (فقط production) |
| `f3ac5ad` | fix: add missing UserWalletChangeLog for discount shop purchases |
| `0457ef6` | fix: validate discount wallet balance before applying discount |
| `e206b71` | fix: reduce discount order expiry from 30 to 15 minutes |
| `2620a24` | fix: correct production URLs from kbs1 to kbs2 in cms-config |
> کامیت‌های PVC و Secret به هر دو شاخه push شده‌اند.
> ⚠️ کامیت‌های حذف appsettings فقط به برنچ مربوطه push شده — cherry-pick نکنید!
### ۹.۳ فیکس URL پروداکشن (اسفند ۱۴۰۴)
> **مشکل:** در `cms-config.yaml` پروداکشن، URL‌ها به اشتباه `kbs1.ir` (استیج) بودند.
> زرین‌پال callback را به سرور استیج می‌فرستاد → خطای 401 → `Code=-1` (خطای ناشناخته).
| فیلد | مقدار اشتباه | مقدار صحیح |
|------|-------------|------------|
| `CmsBaseUrl` | `https://cms.kbs1.ir` | `https://cms.kbs2.ir` |
| `FrontOfficeBaseUrl` | `https://kbs1.ir` | `https://kbs2.ir` |
```bash
# فیکس مستقیم روی سرور (بدون نیاز به rebuild)
kubectl apply -f cms-config.yaml
kubectl rollout restart deployment/cms
```
### ۹.۴ نام‌گذاری کیف‌پول‌ها (اسفند ۱۴۰۴)
> تغییر عنوان کیف‌پول‌ها در تمام UI (FrontOffice: 5 فایل، BackOffice: 7 فایل):
| فیلد | نام قدیم | نام جدید |
|------|---------|----------|
| `Balance` | عادی / نقدی | **کیف پول اصلی** |
| `DiscountBalance` | تخفیفی / تخفیف | **کیف پول اعتباری** |
| `NetworkBalance` | شبکه / طلایی | **پاداش تیمی** |
**تنظیمات محیطی Production (`appsettings.Production.json`):**
| تنظیم | مقدار |
|--------|-------|
| `ZarinPal.MerchantId` | `4225d555-5fa9-4df0-9b61-1ce152cbbba8` |
| `ZarinPal.UseSandbox` | `false` |
| `CmsBaseUrl` | `https://cms.kbs2.ir` |
| `FrontOfficeBaseUrl` | `https://kbs2.ir` |
| `SeedWorkers.MagicWalletCycleSeed.Enabled` | `true` |
| `Kestrel.Endpoints.Grpc.Protocols` | `Http2` |
| `Seq.ServerUrl` | `http://seq-svc:5341` |
| `ConnectionStrings.Default` | `Server=mssql-svc;Database=KBS` |
> ⚠️ **مهم:** URL‌ها باید `kbs2.ir` باشند نه `kbs1.ir` — اشتباه در URL باعث خطای 401 زرین‌پال می‌شود.
```bash
# k8s-health-check.sh (namespace = default)
kubectl get pods