docs: fix all discrepancies based on comprehensive code audit

Corrections verified against actual CMS/BackOffice/FrontOffice source code:

- ClubActivationFee: 25,200,000 (not 25,000,000)
- Tree depth: no limit (15 is commission calculation depth only)
- IPG wallet charge: Balance=56M + Discount=56M
- DayaLoan wallet charge: Balance=56M + Discount=112M (2×)
- Discount: per-product MaxDiscountPercent (not fixed 30%)
- VAT: 10% (ShopVAT) vs 9% (discount store PlaceOrder)
- Kavenegar template: 'Afrino' only (not verify-foursat)
- SMS sender: 1000001110100
- DayaLoan job: every 20min (not 15min)
- Commission job: Sunday 00:05 (not Saturday)
- Network tree: on User entity (not separate NetworkNode table)
- UserWallets entity (not UserWalletBalances)
- OTP: 6 digits, 5 attempts, 2min TTL, 60s cooldown
- Removed non-existent constants (ClubJoiningPercentage, ClubActivationThreshold)
- Fixed Hangfire Chatika interval: every 5min
- Removed InventorySync from recurring jobs list
This commit is contained in:
masoodafar-web
2026-02-18 22:58:40 +03:30
parent efff5e9cd5
commit 3c729304db
10 changed files with 130 additions and 107 deletions
+36 -30
View File
@@ -9,10 +9,10 @@
| مفهوم | توضیح |
|-------|--------|
| **عضویت باشگاه** | خرید پکیج طلایی (۵۶M) → فعالسازی (۲۵M) → عضو فعال باشگاه |
| **درخت باینری** | هر کاربر حداکثر ۲ فرزند مستقیم (چپ/راست) — حداکثر ۱۵ سطح |
| **کمیسیون هفتگی** | محاسبه بر اساس تعادل چپ/راست — هفته شمسی شنبه‌تا‌جمعه |
| **۳ کیف پول** | `Balance` (نقدی) + `NetworkBalance` (شبکه‌ای) + `DiscountBalance` (تخفیفی) |
| **عضویت باشگاه** | خرید پکیج طلایی (۵۶M) → فعالسازی (۲۵M) → عضو فعال باشگاه |
| **درخت باینری** | هر کاربر حداکثر ۲ فرزند مستقیم (چپ/راست) — بدون محدودیت عمق |
| **کمیسیون هفتگی** | محاسبه بر اساس تعادل چپ/راست — یکشنبه ۰۰:۰۵ (Hangfire cron) |
| **۳ کیف پول** | `Balance` (نقدی) + `NetworkBalance` (طلایی/کمیسیون) + `DiscountBalance` (تخفیفی) |
---
@@ -25,14 +25,14 @@
/ \ / \
L1 L2 R1 R2
/ \ / \ / \ / \
... ... ... ... ... ← حداکثر ۱۵ سطح
... ... ... ... ... ← بدون محدودیت عمق
```
**قوانین:**
- هر نود حداکثر ۲ فرزند (Binary)
- جایگذاری: `LegPosition` ∈ {Left, Right}
- عضو جدید → در اولین جای خالی از چپ‌ترین مسیر قرار می‌گیرد
- `SP_GetNetworkTree`Stored Procedure بازگشتی
- هر نود حداکثر ۲ فرزند (Binary)`MaxDirectChildrenPerLeg = 1`
- جایگذاری: `LegPosition` ∈ {Left=0, Right=1} (enum `NetworkLeg`)
- مدل شبکه مستقیم روی entity `User` — فیلدهای `NetworkParentId`, `LegPosition`, `NetworkChildren`
- محاسبه کمیسیون تا عمق ۱۵ سطح (`CommissionMaxNetworkLevel = 15`)اما درخت بدون محدودیت رشد می‌کند
---
@@ -56,11 +56,16 @@
امضای قرارداد ← AcceptContract
├─→ شارژ ۳ کیف پول (Balance=56M, Network=56M, Discount=56M)
├─→ کسر هزینه فعالسازی (25M از Balance)
├─→ واریز 25.2M به Pool هفتگی
├─→ شارژ ۲ کیف پول: Balance += 56M, DiscountBalance += 56M
│ (NetworkBalance شارژ نمی‌شود — فقط برای کمیسیون)
├─→ کسر هزینه فعالسازی (25.2M از Balance)
├─→ واریز 25.2M به Pool هفتگی (ClubActivationFee)
├─→ قرارگیری در درخت باینری
└─→ رفرش JWT Token (claims جدید)
⚠️ در خرید با وام دایا:
├─→ Balance += 56M
└─→ DiscountBalance += 112M (دو برابر — DayaLoanAmount × 2)
```
---
@@ -109,17 +114,18 @@ SP: sp_CalculateWeeklyBalances → sp_CalculateWeeklyCommissionPool
## ۵. تنظیمات سیستمی (SystemConstants)
| ثابت | مقدار | توضیح |
| ثابت (SystemConstants) | مقدار | توضیح |
|------|-------|--------|
| `Club.ActivationFee` | 25,000,000 | هزینه فعالسازی (تومان) |
| `Club.GiftValue` | 25,200,000 | واریز به Pool |
| `GoldenPackageAmount` | 56,000,000 | قیمت پکیج طلایی |
| `DayaLoanAmount` | 56,000,000 | مبلغ وام دایا |
| `MaxWeeklyBalancesPerLeg` | 300 | سقف هفتگی هر پا |
| `MaxNetworkLevel` | 15 | حداکثر عمق درخت |
| `ClubJoiningPercentage` | 0.35 | درصد پیوستن |
| `ClubActivationThreshold` | 0.5 | آستانه فعالسازی |
| `MaxCalculationAttempts` | 3 | حداکثر تلاش محاسبه |
| `ClubActivationFee` | 25,200,000 | هزینه فعالسازی (ریال) |
| `ClubMembershipGiftValue` | 25,200,000 | واریز به Pool |
| `BasePackageAmount` | 56,000,000 | قیمت پکیج طلایی (ریال) |
| `DayaLoanAmount` | 56,000,000 | مبلغ وام دایا (ریال) |
| `CommissionMaxWeeklyBalancesPerLeg` | 300 | سقف هفتگی هر پا |
| `CommissionMaxNetworkLevel` | 15 | عمق محاسبه کمیسیون (نه محدودیت درخت) |
| `MaxDirectChildrenPerLeg` | 1 | حداکثر فرزند مستقیم هر پا |
| `MinimumWithdrawAmount` | 1,000,000 | حداقل مبلغ برداشت (ریال) |
| `ShopVAT` | 0.1 (10%) | مالیات ارزش افزوده |
| `CommissionCalculationMethod` | "SP" | روش محاسبه = Stored Procedure |
---
@@ -127,8 +133,8 @@ SP: sp_CalculateWeeklyBalances → sp_CalculateWeeklyCommissionPool
| سناریو | فلو | وضعیت |
|--------|------|--------|
| **وام دایا** | درخواست وام → تأیید خودکار → شارژ ۳ کیف‌پول (۱۶۸M) | ✅ پیاده‌شده |
| **درگاه مستقیم** | IPG (ZarinPal) → callback → شارژ | ✅ پیاده‌شده |
| **وام دایا** | درخواست وام → تأیید → Balance=56M + Discount=112M (مجموع ۱۶۸M) | ✅ پیاده‌شده |
| **درگاه مستقیم** | IPG → callback → Balance=56M + Discount=56M (مجموع ۱۱۲M) | ✅ پیاده‌شده |
| **پرداخت دستی** | کارت‌به‌کارت → آپلود رسید → تأیید ادمین → شارژ | ⚠️ طراحی‌شده |
---
@@ -136,7 +142,7 @@ SP: sp_CalculateWeeklyBalances → sp_CalculateWeeklyCommissionPool
## ۷. یکپارچه‌سازی وام دایا
```
Hangfire Worker (هر ۱۵ دقیقه)
Hangfire Worker (هر ۲۰ دقیقه — cron: */20 * * * *)
بررسی درخواست‌های pending
@@ -145,13 +151,13 @@ Hangfire Worker (هر ۱۵ دقیقه)
ارسال به API دایا (Mock/Real switchable)
دریافت نتیجه → شارژ ۳ کیف‌پول
دریافت نتیجه → شارژ ۲ کیف‌پول
├─→ Balance = 56M
─→ NetworkBalance = 56M
└─→ DiscountBalance = 56M (مجموع: 168M)
├─→ Balance += 56M (DayaLoanAmount)
─→ DiscountBalance += 112M (DayaLoanAmount × 2 — دو برابر)
مجموع: 168M
Polly Retry: 3 attempts, Exponential backoff
Hangfire retry: [AutomaticRetry(Attempts = 3)]
```
---
+20 -12
View File
@@ -63,7 +63,7 @@ Redirect کاربر → صفحه پرداخت ZarinPal
### ۳.۱ معماری
```
Hangfire Recurring Job (هر ۱۵ دقیقه)
Hangfire Recurring Job (هر ۲۰ دقیقه — cron: */20 * * * *)
DayaLoanProcessorJob.Execute()
@@ -73,10 +73,10 @@ DayaLoanProcessorJob.Execute()
برای هر درخواست:
├─→ ارسال به DayaLoan API (با Polly retry ×3)
├─→ در صورت تأیید: شارژ ۳ کیف‌پول (هرکدام ۵۶M)
├─→ ارسال به DayaLoan API ([AutomaticRetry(Attempts=3)])
├─→ در صورت تأیید: Balance += 56M, DiscountBalance += 112M (2×)
├─→ ثبت Transaction + Log
└─→ در صورت رد: بروزرسانی Status=Rejected + ارسال SMS
└─→ در صورت رد: Status=Rejected + ارسال SMS
```
### ۳.۲ Mock Mode
@@ -95,9 +95,11 @@ DayaLoanProcessorJob.Execute()
| آیتم | مقدار |
|------|-------|
| مبلغ وام | ۵۶,۰۰۰,۰۰۰ تومان |
| شارژ هر کیف‌پول | ۵۶,۰۰۰,۰۰۰ تومان |
| مجموع شارژ | ۱۶۸,۰۰۰,۰۰۰ تومان |
| مبلغ وام (DayaLoanAmount) | ۵۶,۰۰۰,۰۰۰ ریال |
| شارژ Balance | ۵۶,۰۰۰,۰۰۰ ریال |
| شارژ DiscountBalance | ۱۱۲,۰۰۰,۰۰۰ ریال (دو برابر — DayaLoanAmount × 2) |
| مجموع شارژ | ۱۶۸,۰۰۰,۰۰۰ ریال |
| NetworkBalance | شارژ نمی‌شود |
| بازپرداخت | طبق شرایط دایا |
---
@@ -134,13 +136,15 @@ DayaLoanProcessorJob.Execute()
### ۵.۱ فرمول
```
قیمت محصول = 1,000,000 تومان
تخفیف باشگاه = 30%
قیمت محصول = 1,000,000 ریال
MaxDiscountPercent محصول = 40% (هر محصول درصد تخفیف مخصوص خود را دارد)
پرداخت از DiscountBalance = 1,000,000 × 0.30 = 300,000
پرداخت نقدی (IPG) = 1,000,000 × 0.70 = 700,000
سهم تخفیف = MIN(1,000,000 × 40%, DiscountBalanceکاربر) = 400,000
پرداخت نقدی (IPG) = 1,000,000 - 400,000 = 600,000
─────────
مجموع = 1,000,000
ℹ️ تخفیف ثابت ۳۰% نیست — فیلد Product.MaxDiscountPercent (0-100) تعیین‌کننده است.
```
### ۵.۲ فلوی خرید فروشگاه تخفیفی
@@ -204,7 +208,11 @@ service PaymentService {
## ۷. مالیات و VAT
```
VAT = 10% (configurable via SystemConstants)
دو نرخ VAT در کد:
SystemConstants.ShopVAT = 0.1 (10%) — تنظیم سیستمی
VAT_RATE = 0.09 (9%) — در فروشگاه تخفیفی (DiscountShop PlaceOrder)
قیمت نمایشی = قیمت پایه × (1 + VAT)
در صورتحساب: قیمت پایه + مالیات جداگانه نمایش داده می‌شود
```
+5 -2
View File
@@ -16,8 +16,8 @@
├───────────────────────┼─────────────────────────────────┤
│ • همه کاربران │ • فقط اعضای باشگاه │
│ • پرداخت 100% نقدی │ • پرداخت ترکیبی (تخفیف+نقد) │
│ • قیمت عادی │ • تخفیف ۳۰% از DiscountBalance
│ • بدون محدودیت │ • وابسته به موجودی تخفیفی
│ • قیمت عادی │ • تخفیف بر اساس MaxDiscountPercent
│ • VAT = 10% │ • VAT = 9% (در کد PlaceOrder)
├───────────────────────┴─────────────────────────────────┤
│ Shared: Products, Categories, │
│ Inventory, ProductImages (1:1 square) │
@@ -93,6 +93,9 @@ public class Inventory {
public int MinimumStock { get; set; } // حداقل موجودی (هشدار)
public bool TrackInventory { get; set; } // آیا موجودی رصد شود؟
}
// فیلد کلیدی در Product:
public int MaxDiscountPercent { get; set; } // 0 تا 100 — درصد تخفیف در فروشگاه تخفیفی
```
### ۳.۳ فلوی سفارش و موجودی
+13 -11
View File
@@ -63,10 +63,12 @@ JWT Claims:
| مشکل | راه‌حل | وضعیت |
|------|---------|--------|
| OTP تکراری | Rate limiting: ۱ درخواست هر ۶۰ ثانیه | ✅ |
| OTP تکراری | Cooldown: ۶۰ ثانیه بین درخواست‌ها | ✅ |
| شماره نامعتبر | Regex validation ایران `^09\d{9}$` | ✅ |
| حمله brute-force | قفل حساب بعد از ۵ تلاش ناموفق | ✅ |
| Race condition ثبت‌نام | Unique constraint + transaction | ✅ |
| حمله brute-force | MaxAttempts: ۵ تلاش برای تأیید کد | ✅ |
| انقضای کد | TTL: ۲ دقیقه | ✅ |
| طول کد | ۶ رقم | ✅ |
| قالب SMS | Kavenegar template: `Afrino` | ✅ |
---
@@ -136,15 +138,15 @@ RequestContractOtp → ارسال SMS
### ۴.۲ ذخیره‌سازی قرارداد
```csharp
// از کد: UserContract : BaseAuditableEntity
public class UserContract {
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string ContractVersion { get; set; } // e.g., "v1.2"
public string ContractText { get; set; } // snapshot متن
public DateTime AcceptedAt { get; set; }
public string OtpVerificationId { get; set; }
public string IpAddress { get; set; }
public string UserAgent { get; set; }
public long UserId { get; set; }
public virtual User User { get; set; }
public long ContractId { get; set; } // FK → Contract
public virtual Contract Contract { get; set; }
public string SignGuid { get; set; } // GUID یکتای امضا
public string SignedPdfFile { get; set; } // فایل PDF امضاشده
// فیلدهای BaseAuditableEntity: CreatedAt, ModifiedAt, ...
}
```
+14 -17
View File
@@ -132,13 +132,9 @@ FileManagementService → ذخیره در فایل‌سیستم + ثبت در DB
{
"Kavenegar": {
"ApiKey": "***",
"SenderNumber": "10008663",
"Templates": {
"OTP": "verify-foursat",
"ContractOTP": "contract-verify",
"WelcomeClub": "club-welcome",
"OrderConfirm": "order-confirm"
}
"SenderNumber": "1000001110100",
"DefaultTemplate": "Afrino",
"_note": "تمام OTPها با قالب Afrino ارسال می‌شوند"
}
}
```
@@ -183,16 +179,17 @@ CREATE TABLE SystemConfigurations (
| Category | Key | Value | توضیح |
|----------|-----|-------|--------|
| Club | `Club.ActivationFee` | 25000000 | هزینه فعالسازی |
| Club | `Club.GiftValue` | 25200000 | واریز Pool |
| Club | `GoldenPackageAmount` | 56000000 | قیمت پکیج طلایی |
| Club | `MaxNetworkLevel` | 15 | حداکثر عمق درخت |
| Club | `MaxWeeklyBalance` | 300 | سقف هفتگی |
| Payment | `VAT.Percentage` | 10 | مالیات ارزش افزوده |
| Payment | `DayaLoanAmount` | 56000000 | مبلغ وام |
| Store | `DiscountPercentage` | 30 | تخفیف باشگاه |
| Store | `ProductsPerPage` | 12 | تعداد در صفحه |
| System | `SmsProvider` | Kavenegar | ارائه‌دهنده SMS |
| Club | `ClubActivationFee` | 25200000 | هزینه فعالسازی (ریال) |
| Club | `ClubMembershipGiftValue` | 25200000 | واریز Pool |
| Club | `BasePackageAmount` | 56000000 | قیمت پکیج طلایی (ریال) |
| Club | `CommissionMaxNetworkLevel` | 15 | عمق محاسبه کمیسیون |
| Club | `CommissionMaxWeeklyBalancesPerLeg` | 300 | سقف هفتگی |
| Payment | `ShopVAT` | 0.1 (10%) | مالیات (فروشگاه تخفیفی: 9%) |
| Payment | `DayaLoanAmount` | 56000000 | مبلغ وام (ریال) |
| Payment | `MinimumWithdrawAmount` | 1000000 | حداقل برداشت (ریال) |
| Store | `MaxDiscountPercent` | per-product | 0-100، هر محصول جداگانه |
| Store | `ProductsPerPage` | 12 (FO) / 10 (CMS) | تعداد در صفحه |
| System | `CommissionCalculationMethod` | "SP" | Stored Procedure |
| System | `MaintenanceMode` | false | حالت تعمیر |
---