diff --git a/business/BIZ-PACKAGE-BASED-SYSTEM.md b/business/BIZ-PACKAGE-BASED-SYSTEM.md index 11a21a9..861ea41 100644 --- a/business/BIZ-PACKAGE-BASED-SYSTEM.md +++ b/business/BIZ-PACKAGE-BASED-SYSTEM.md @@ -1,8 +1,8 @@ # 📦 سیستم مبتنی بر پکیج (Package-Based System) > **وضعیت:** در حال پیاده‌سازی — **فاز ۰-۴ تکمیل ✅** | فاز ۵-۶ در انتظار -> **تاریخ بروزرسانی:** ۷ اسفند ۱۴۰۴ -> **نسخه:** v5 (قرارداد یک‌بار + فیچر DIFF + تاریخچه فعال‌سازی) +> **تاریخ بروزرسانی:** ۸ اسفند ۱۴۰۴ +> **نسخه:** v6 (آستانه موجودی + SP Worker + History Tables + UI Guidance) > **تاثیرگذاری:** زیاد — **۹۵+** تغییر در ۶ لایه (۵۱ اصلی + ۴۴ سایدافکت) > **کامیت‌ها:** `8b9c317` (Phase 0) → `ae92ab8` (Phase 1) → `a9cd2fd` (Phase 1.5) → `8e5c7c5` (Phase 2) → `ccb938e` (Phase 3) → `0002a5a` (Phase 4) @@ -43,6 +43,13 @@ | Q21 | ردیابی فعال‌سازی ClubMembership | ✅ **FirstActivation + LastActivation** — ۴ فیلد: `FirstActivationDate` + `FirstPackageId` + `LastActivationDate` + `LastPackageId` | | Q22 | تشخیص فعال‌شدگان هفته | ✅ **از `LastActivationDate`** — هر کسی که `LastActivationDate` در بازه هفته باشد | | Q23 | Carryover تعادل هفتگی | ✅ **strictly per-package** — اگر کاربر پکیج عوض کرد، carryover پکیج قبلی شمرده نمی‌شود | +| Q24 | آستانه موجودی برای ورود Magic و خرید مجدد | ✅ **کمتر از ۱۰۰,۰۰۰ تومان** — چون قیمت محصولات متفاوته، `Balance == 0` عملاً غیرممکنه. آستانه ثابت ۱,۰۰۰,۰۰۰ ریال | +| Q25 | DayaLoans محدودیت پکیج | ✅ **فقط پکیج پایه** — `SupportsDayaPurchase` فقط روی پکیج پایه `true` هست. تغییر نمی‌کنه | +| Q26 | مدیریت Stored Procedures | ✅ **SP Worker (IHostedService)** — در startup، فایل‌های `.sql` از embedded resource خوانده و با checksum مقایسه و اعمال می‌شوند | +| Q27 | History Tables یکسان‌سازی | ✅ **نام‌گذاری مشابه master** + ثبت خودکار تغییرات در EF interceptor/domain events | +| Q28 | UI Guidance (آموزش/هشدار) | ✅ **مودال + متن inline** — در سراسر FO/BO توضیحات آموزشی و هشداری برای سیستم پکیج‌بیس | +| Q29 | شرط EXIT Magic | ✅ **آخرین پکیج فعال** — از `ClubMembershipCycle.PackageId` (چرخه فعلی) → `Package.MagicWalletMaxDeposit` | +| Q30 | Carryover توضیح | ✅ **باقی‌مانده تعادل هفتگی** — پای قوی‌تر surplus نگه می‌داره. تغییر پکیج = ریست implicit (lookup پکیج جدید → ۰) | --- @@ -1466,3 +1473,280 @@ SELECT 'Balances', COUNT(*) FROM "CMS"."NetworkWeeklyBalances" WHERE "PackageId" > فاز ۲ و ۳ **موازی** → مسیر بحرانی: ۰→۱→۲→۴→۵ = **~۲۴ روز** > نسبت به v4 (**۲۲ روز**): **+۲ روز** بخاطر FeatureDiffService + قرارداد یک‌بار + First/Last Activation > نسبت به v3 (**۱۷ روز**): **+۷ روز** — بزرگ‌ترین سهم: Magic Wallet + WalletChangeLog + فیچر DIFF + First/Last + +--- + +## ۱۴. تصمیمات جدید v6 (Q24–Q30) + +> **تاریخ:** ۸ اسفند ۱۴۰۴ +> **زمینه:** مرور نهایی قبل از تست و استقرار — ۷ نکته جدید شناسایی شد + +### Q24 — آستانه موجودی برای ورود به Magic و خرید مجدد 🔴 + +> **مشکل:** شرط فعلی `wallet.Balance == 0` عملاً غیرممکنه — چون قیمت محصولات متفاوته (مثلاً ۳۵۰,۰۰۰ تومان، ۱,۲۰۰,۰۰۰ تومان، ...) کاربر هیچ‌وقت نمی‌تونه دقیقاً به صفر برسه. + +**تصمیم:** آستانه ثابت **۱,۰۰۰,۰۰۰ ریال (۱۰۰,۰۰۰ تومان)** — هم برای ورود به Magic، هم برای تشخیص تکمیل چرخه. + +**مکان‌های تاثیر:** + +| # | فایل | شرط فعلی | شرط جدید | +|---|------|----------|----------| +| 1 | `UserOrderService.cs` L342 | `if (wallet.Balance == 0)` — ورود به Magic | `if (wallet.Balance <= MagicWalletEntryThreshold)` | +| 2 | `UserOrderService.cs` L342 | `if (wallet.Balance == 0)` — EXIT Magic check | `if (wallet.Balance <= MagicWalletEntryThreshold)` | +| 3 | خرید مجدد guard | `PackagePurchaseMethod == None` | + `Balance <= Threshold` | + +**تعریف ثابت:** +```csharp +// SystemConstants.cs یا Package entity +public const long MagicWalletEntryThreshold = 1_000_000; // 100,000 تومان = 1,000,000 ریال +``` + +**فلوی جدید:** +``` +کاربر خرید می‌کند → Balance = 850,000 ریال (کمتر از 1M) + → سیستم: Balance <= 1,000,000 ✅ → ورود به Magic Mode + → قبلاً: Balance == 0 ❌ → کاربر گیر می‌افتاد! + +Magic تکمیل → Balance = 200,000 ریال + → سیستم: Balance <= 1,000,000 ✅ → EXIT Magic → مجاز به خرید مجدد + → قبلاً: Balance == 0 ❌ → کاربر تا ابد در Magic! +``` + +> ⚠️ **نکته:** این آستانه **ثابت (global)** تعریف می‌شه — نه per-package. چون مربوط به قیمت محصولات فروشگاهه، نه پکیج. + +--- + +### Q25 — DayaLoans فقط پکیج پایه + +> **تایید:** وام دایا فعلاً فقط برای پکیج پایه فعاله و تغییر نمی‌کنه. + +**وضعیت فعلی:** +- فیلد `Package.SupportsDayaPurchase` روی پکیج پایه = `true`، نقره‌ای = `false` +- `CheckAndProcessDayaLoansCommandHandler` فقط پکیج‌هایی که `SupportsDayaPurchase = true` دارن رو بررسی می‌کنه +- این یک تصمیم بیزینسی ثابته — اگر در آینده تغییر کرد، ادمین از BackOffice فلگ رو تغییر می‌ده + +--- + +### Q26 — SP Worker (مدیریت خودکار Stored Procedures) 🔴 + +> **مشکل:** الان SPها **کاملاً دستی** deploy می‌شن. اگه SP تغییر کنه، باید یادمون باشه دستی اجرا کنیم. + +**تصمیم:** یک `IHostedService` به نام `StoredProcedureDeploymentService` ایجاد بشه. + +**الگوریتم:** +``` +1. Startup → خواندن فایل‌های .sql از Embedded Resources +2. برای هر فایل: + a. محاسبه SHA256 checksum محتوا + b. خواندن checksum قبلی از جدول "CMS"."StoredProcedureVersions" + c. اگر وجود نداشت یا checksum فرق داشت: + → اجرای CREATE OR ALTER PROCEDURE + → ذخیره checksum جدید + → لاگ: "SP {name} deployed/updated" + d. اگر checksum برابر بود: + → لاگ: "SP {name} is up-to-date, skipping" +3. اجرا بعد از EF Migration (ترتیب startup) +``` + +**جدول جدید:** +```sql +CREATE TABLE "CMS"."StoredProcedureVersions" ( + "Id" BIGSERIAL PRIMARY KEY, + "Name" VARCHAR(256) NOT NULL UNIQUE, -- نام SP + "Checksum" VARCHAR(64) NOT NULL, -- SHA256 hash + "DeployedAt" TIMESTAMP NOT NULL, -- آخرین deploy + "Version" INT NOT NULL DEFAULT 1 -- شمارنده نسخه +); +``` + +**فایل‌های SP فعلی (embed شوند):** + +| # | فایل | خطوط | +|---|------|------| +| 1 | `sp_CalculateWeeklyBalances.sql` | ۳۷۲ | +| 2 | `sp_CalculateWeeklyCommissionPool.sql` | ۲۶۷ | + +**csproj تغییرات:** +```xml + + + +``` + +--- + +### 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 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 قابل استفاده مجدد *@ + + +@* BackOffice — متن inline *@ + + 💡 تغییر قیمت روی کاربران فعلی اثر ندارد — فقط خریدهای جدید + +``` + +--- + +### Q29 — شرط EXIT Magic — تایید: آخرین پکیج فعال + +> **تایید:** شرط EXIT از `ClubMembershipCycle` فعلی (`IsCurrentCycle=true`) → `PackageId` → `Package.MagicWalletMaxDeposit` خوانده می‌شه. + +**فلوی دقیق:** +``` +1. UserOrderService.ProcessOrderAsync → wallet.Balance کم می‌شه +2. if (Balance <= Threshold): ← Q24 آستانه جدید + a. اگر Normal + خرید کرده + باشگاه فعال → ENTER Magic + b. اگر Magic: + → خواندن چرخه فعلی → PackageId → Package.MagicWalletMaxDeposit + → if (MagicTotalDeposited >= MaxDeposit) → EXIT Magic + → else: هنوز Magic (می‌تونه شارژ کنه) +``` + +> **Fallback:** اگه چرخه فعالی نبود → `IsBasePackage = true` (پکیج پایه) + +--- + +### Q30 — Carryover — توضیح ساده + +> **Carryover = باقی‌مانده تعادل از هفته قبل** + +**مثال ساده:** +``` +هفته ۱۰: + علی: تیم چپ = ۵۰ نفر فعال، تیم راست = ۳۰ نفر فعال + تعادل = MIN(50, 30) = 30 + باقی‌مانده: چپ = 50-30 = 20 ← CARRYOVER راست = 0 + +هفته ۱۱: + اعضای جدید: چپ = 5، راست = 10 + جمع با carryover: چپ = 5+20 = 25، راست = 10+0 = 10 + تعادل = MIN(25, 10) = 10 + باقی‌مانده: چپ = 25-10 = 15 ← CARRYOVER بعدی +``` + +**Per-package carryover (Q23):** +``` +علی پکیج پایه داره: + carryover_پایه = {چپ: 20, راست: 0} + +علی پکیج رو عوض می‌کنه → نقره‌ای: + ❌ carryover_پایه دیگه شمرده نمی‌شه! + ✅ carryover_نقره‌ای = {چپ: 0, راست: 0} ← شروع از صفر + + (چون سیستم هفته بعد دنبال carryover با PackageId=نقره‌ای + می‌گرده و پیدا نمی‌کنه → default صفر) +``` + +**Cap (سقف):** +``` +اگه تعادل بیشتر از MaxBalancesPerLeg بشه → بریده می‌شه (flush): + تعادل = 500، سقف = 300 → CappedBalance = 300، Flushed = 200 + ⚠️ Flushed از بین می‌ره — carry نمی‌شه! +``` diff --git a/deployment/PACKAGE-MIGRATION-GUIDE.md b/deployment/PACKAGE-MIGRATION-GUIDE.md index f6a2a82..ab08bbe 100644 --- a/deployment/PACKAGE-MIGRATION-GUIDE.md +++ b/deployment/PACKAGE-MIGRATION-GUIDE.md @@ -629,12 +629,19 @@ kubectl set image deployment/backoffice bo=bo:rollback-point | F5 | SystemConfiguration per-package | 🟡 | تنظیمات global vs per-package | | F6 | MagicWalletChargePage hardcoded | 🟡 | ×۲.۵ و سقف در ۶ جا FO | | F7 | Validators async per-package | 🟢 | سقف 1B → مقدار واقعی | +| F8 | آستانه موجودی ورود به Magic (Q24) | 🔴 | `Balance == 0` → `Balance <= 1M ریال` | +| F9 | SP Worker — مدیریت خودکار SP (Q26) | 🔴 | IHostedService + checksum + auto-deploy | +| F10 | History Tables — یکسان‌سازی + خودکار (Q27) | 🟡 | نام‌گذاری + IHasHistory + interceptor | +| F11 | UI Guidance — آموزش و هشدار (Q28) | 🟡 | مودال + متن inline در FO/BO | -> **این موارد ریسک عملیاتی ندارند** و می‌توانند در فاز بعدی انجام شوند. سیستم فعلی با fallback مناسب کار می‌کند. +> **F8 و F9 ریسک عملیاتی دارند** و باید قبل از production پیاده شوند. +> **F10 و F11** می‌توانند در فاز بعدی انجام شوند. سایر موارد (F1-F7) با fallback فعلی مشکلی ندارند. --- -## ضمیمه: ۲۳ تصمیم بیزینسی پیاده‌شده +## ضمیمه: ۳۰ تصمیم بیزینسی (Q1–Q30) + +### پیاده‌شده (Q1–Q23): | # | تصمیم | وضعیت | |---|-------|-------| @@ -662,6 +669,18 @@ kubectl set image deployment/backoffice bo=bo:rollback-point | Q22 | تشخیص هفته از LastActivationDate | ✅ `607f791` | | Q23 | Carryover strictly per-package | ✅ `607f791` | +### تصمیمات جدید v6 (Q24–Q30) — در انتظار پیاده‌سازی: + +| # | تصمیم | وضعیت | اولویت | +|---|-------|-------|--------| +| Q24 | آستانه موجودی ≤ ۱۰۰,۰۰۰ تومان (ورود Magic + خرید مجدد) | ⬜ | 🔴 P0 | +| Q25 | DayaLoans فقط پکیج پایه — تایید (بدون تغییر کد) | ✅ تایید | — | +| Q26 | SP Worker — auto-deploy با checksum | ⬜ | 🔴 P0 | +| Q27 | History Tables — نام‌گذاری یکسان + IHasHistory + interceptor | ⬜ | 🟡 P1 | +| Q28 | UI Guidance — مودال + متن آموزشی/هشداری در FO/BO | ⬜ | 🟡 P1 | +| Q29 | شرط EXIT Magic — تایید: آخرین پکیج فعال (بدون تغییر کد) | ✅ تایید | — | +| Q30 | Carryover — تایید: توضیح مستند شد (بدون تغییر کد) | ✅ تایید | — | + --- -*آخرین بروزرسانی: ۸ اسفند ۱۴۰۴ — آماده تست و استقرار* +*آخرین بروزرسانی: ۸ اسفند ۱۴۰۴ — v6: Q24-Q30 اضافه شد (آستانه موجودی + SP Worker + History + UI Guidance)*