# 🔄 نقشه‌راه تحول پکیج‌بیس — تسک‌های گام‌به‌گام > **وضعیت:** تایید‌شده — آماده شروع > **تاریخ:** ۱۴۰۴/۱۲/۰۶ > **پیش‌نیاز:** [BIZ-PACKAGE-BASED-SYSTEM.md](../business/BIZ-PACKAGE-BASED-SYSTEM.md) **v3** (تکمیل پورسانت per-package) > **هدف:** شکستن **۴۸+ تغییر** به تسک‌های اتمیک با ترتیب اجرا و وابستگی‌ها > ⚠️ **تغییرات v3:** پورسانت per-package، carryover مجزا، SP parameters داینامیک، گزارش‌دهی FO/BO per-package --- ## 📊 نمای کلی ``` مرحله ۰: فیکس باگ فوری (۱ روز) └─→ مرحله ۱: زیرساخت Domain + DB (۴ روز) ← +۱ روز (v3: NetworkWeeklyBalance) ├─→ مرحله ۲: منطق کسب‌وکار (۴ روز) ← موازی │ └─→ مرحله ۴: UI (۵ روز) ← +۲ (v3: FO/BO گزارش per-package) └─→ مرحله ۳: پورسانت (۴ روز) ← +۱ (v3: SP params + carryover) └─→ مرحله ۵: تست + استقرار (۳ روز) ← +۱ مسیر بحرانی: ۰→۱→۲→۴→۵ = ~۱۷ روز (v2: ۱۳ روز) ``` --- ## مرحله ۰ — فیکس باگ‌های فوری > ⏱️ ۱ روز | وابستگی: ندارد | ریسک: کم ### ✅ وضعیت باگ‌ها (بررسی اولیه لازم) | # | باگ | Handler | شرح فیکس | |---|------|---------|----------| | B1 | DiscountBalance شارژ نمی‌شود | `VerifyGoldenPackagePurchaseCommandHandler` | اضافه `DiscountBalance += Amount × 2` + WalletChangeLog | | B2 | UserPackagePurchase ساخته نمی‌شود | `VerifyGoldenPackagePurchaseCommandHandler` | ساخت record بعد verify موفق | | B3 | UserPackagePurchase ساخته نمی‌شود | `VerifyPackagePurchaseCommandHandler` | ساخت record بعد verify موفق | | B4 | UserPackagePurchase ساخته نمی‌شود | `VerifyBasePackagePaymentCommandHandler` | ساخت record بعد verify موفق | #### دستور کار B1: ``` 1. باز کردن VerifyGoldenPackagePurchaseCommandHandler.cs 2. پیدا کردن جایی که Balance شارژ می‌شود 3. اضافه کردن: wallet.DiscountBalance += command.Amount * 2; // + ساخت WalletChangeLog برای DiscountBalance 4. تست: verify → چک DiscountBalance در DB ``` #### دستور کار B2-B4 (الگوی مشترک): ``` 1. بعد از verify موفق و شارژ wallet: var purchase = new UserPackagePurchase { UserId = userId, PackageId = packageId, // فعلاً BasePackageId = 4 PurchaseDate = DateTime.UtcNow, Amount = amount, PurchaseMethod = purchaseMethod, // ZarinPal, BFF, etc. TransactionId = transactionId, IsVerified = true }; _context.UserPackagePurchases.Add(purchase); 2. تست: verify → چک UserPackagePurchases table ``` --- ## مرحله ۱ — زیرساخت (Domain + DB) > ⏱️ ۳ روز | وابستگی: مرحله ۰ | ریسک: متوسط (migration) ### T1.1 — بروزرسانی Package Entity (۱۱ فیلد جدید — v3) **فایل:** `CMS/src/CMSMicroservice.Domain/Entities/Package.cs` ```diff + 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; } + public decimal DiscountMultiplier { get; set; } = 2.0m; + public decimal MagicWalletMultiplier { get; set; } = 2.5m; + // === v3: تنظیمات پورسانت per-package === + public int MaxBalancesPerLeg { get; set; } = 300; // نقره‌ای=۳۰ + public int MaxNetworkLevel { get; set; } = 15; + // === v3: سقف کیف‌پول جادویی per-package === + public long MagicWalletMaxDeposit { get; set; } = 1_000_000_000; + public long MagicWalletMaxCredit { get; set; } = 2_500_000_000; + + public virtual ICollection PackageFeatures { get; set; } ``` **EF Config:** `PackageConfiguration.cs` - حداکثر یک `IsBasePackage = true` (Index filter) - Precision for decimal fields ### T1.2 — ایجاد PackageFeature Entity **فایل جدید:** `CMS/src/CMSMicroservice.Domain/Entities/PackageFeature.cs` ```csharp 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; } ``` ### T1.3-T1.6 — اضافه PackageId به entityها | Entity | فیلد | Required? | توضیح | v3? | |--------|------|-----------|-------|-----| | ClubMembership | `long? PackageId` | nullable (بعد migration → required) | آخرین پکیج | | | ClubMembershipCycle | `long PackageId` | required | پکیج این چرخه | | | WeeklyCommissionPool | `long PackageId` | required + Unique(WeekDefId, PkgId) | Pool هر پکیج | | | UserCommissionPayout | `long? PackageId` | nullable + **Unique(UserId, WeekId, PkgId)** | ردیابی | 🔄 | | **NetworkWeeklyBalance** | **`long PackageId`** | **required + Unique(UserId, WeekId, PkgId)** | **تعادل per-package** | **🆕** | ### T1.7 — حذف SystemConstants (v3: ۷ ثابت) **فایل:** `CMS/src/CMSMicroservice.Domain/Common/SystemConstants.cs` ```diff - public const long BasePackageAmount = 56_000_000; - public const long DayaLoanAmount = 56_000_000; - public const long ClubActivationFee = 25_200_000; - public const long ClubMembershipGiftValue = 25_200_000; - public const decimal MagicWalletMultiplier = 2.5m; - // === v3: انتقال به Package entity === - public const int CommissionMaxWeeklyBalancesPerLeg = 300; - public const int CommissionMaxNetworkLevel = 15; ``` > ⚠️ **قبل از حذف:** grep تمام مصرف‌کننده‌ها → جایگزین با `Package.Property` > ⚠️ **v3:** `CommissionMaxWeeklyBalancesPerLeg` و `CommissionMaxNetworkLevel` هم باید per-package شوند ### T1.8 — Database Migration ```bash dotnet ef migrations add AddPackageBasedSystem ``` **شامل:** - ستون‌های جدید Package - جدول PackageFeatures - FKها در 4 entity - Unique constraint ### T1.9 — Data Migration Script ```sql -- 1. بروزرسانی پکیج فعلی (ID=4 → اضافه فیلدهای جدید) UPDATE "CMS"."Packages" SET "SortOrder" = 2, "IsActive" = true, "IsBasePackage" = true, "SupportsDayaPurchase" = true, "SupportsDirectPurchase" = true, "ActivationFee" = 25200000, "DiscountMultiplier" = 2.0, "MagicWalletMultiplier" = 2.5, -- v3: تنظیمات پورسانت "MaxBalancesPerLeg" = 300, "MaxNetworkLevel" = 15, "MagicWalletMaxDeposit" = 1000000000, "MagicWalletMaxCredit" = 2500000000 WHERE "Id" = 4; -- 2. Link existing data to base package UPDATE "CMS"."ClubMemberships" SET "PackageId" = 4 WHERE "PackageId" IS NULL; UPDATE "CMS"."ClubMembershipCycles" SET "PackageId" = 4 WHERE "PackageId" IS NULL; UPDATE "CMS"."WeeklyCommissionPools" SET "PackageId" = 4 WHERE "PackageId" IS NULL; UPDATE "CMS"."UserCommissionPayouts" SET "PackageId" = 4 WHERE "PackageId" IS NULL; -- v3: NetworkWeeklyBalance هم PackageId می‌گیره UPDATE "CMS"."NetworkWeeklyBalances" SET "PackageId" = 4 WHERE "PackageId" IS NULL; -- 3. Seed Silver package (شامل فیلدهای v3) INSERT INTO "CMS"."Packages" (..., "MaxBalancesPerLeg", "MaxNetworkLevel", "MagicWalletMaxDeposit", "MagicWalletMaxCredit", ...) VALUES ('پکیج نقره‌ای', 5600000, ..., 30, 15, 100000000, 250000000, ...); ``` ### T1.10 — بروزرسانی Protoها | Proto File | تغیر | v3? | |-----------|-------|-----| | package.proto | فیلدهای جدید Package message (۱۱ فیلد) | 🔄 | | clubmembership.proto | package_id در request/response | | | commission.proto | **`package_id` + `package_title`** در ۴ message | **🆕** | | commission.proto | **Message جدید: `CustomerCommissionPackageSummary`** | **🆕** | | commission.proto | **فیلتر `package_id` در Requestها** | **🆕** | ### T1.11 — اضافه PackageId به NetworkWeeklyBalance (🆕 v3) **فایل:** `CMS/src/CMSMicroservice.Domain/Entities/NetworkWeeklyBalance.cs` ```diff + public long PackageId { get; set; } + public virtual Package Package { get; set; } ``` **EF Config:** اضافه Unique Index: ```csharp builder.HasIndex(e => new { e.UserId, e.WeekDefinitionId, e.PackageId }).IsUnique(); builder.HasOne(e => e.Package).WithMany().HasForeignKey(e => e.PackageId); ``` > ⚠️ **تاثیر حجم:** رکوردهای تعادل ×N (تعداد پکیج). مثلاً ۱۰۰۰ کاربر × ۲ پکیج = ۲۰۰۰ رکورد هفتگی ### T1.12 — Data Migration: NetworkWeeklyBalance (🆕 v3) ```sql -- رکوردهای موجود → پکیج پایه UPDATE "CMS"."NetworkWeeklyBalances" SET "PackageId" = (SELECT "Id" FROM "CMS"."Packages" WHERE "IsBasePackage" = true LIMIT 1) WHERE "PackageId" IS NULL; ``` --- ## مرحله ۲ — منطق کسب‌وکار > ⏱️ ۴ روز | وابستگی: مرحله ۱ | ریسک: بالا (رگرسیون) ### T2.1 — Generic Verify Handler **هدف:** ادغام VerifyGolden + VerifyBase + VerifyGeneric → یک handler **الگوریتم:** ``` 1. دریافت TransactionId از request 2. خواندن Transaction → PackageId → Package entity 3. verify با درگاه (ZarinPal/BFF/...) 4. اگر موفق: a. wallet.Balance += Package.Price b. wallet.DiscountBalance += Package.Price × Package.DiscountMultiplier c. ساخت WalletChangeLog (Balance) d. ساخت WalletChangeLog (DiscountBalance) e. ساخت UserPackagePurchase record f. اگر اولین خرید: JoinNetwork g. بروزرسانی ClubMembershipCycle.PackageId 5. return success + receipt ``` ### T2.2 — Generic Purchase Handler **هدف:** ادغام PurchaseGolden + PurchasePackage + InitiateBase → یک handler **تغییرات:** - حذف فیلتر `Title.Contains("طلایی")` - حذف `BasePackageId = 4` - خواندن Package entity از DB بر اساس `request.PackageId` - Gateway URL + Amount از Package.Price ### T2.3-T2.4 — ActivateClubMembership بهبود **تغییرات:** ```diff - var features = await GetAllFeatureIds(); // همه فیچرها + var features = await GetPackageFeatures(packageId); // فیچرهای پکیج - membership.PackageAmount = SystemConstants.BasePackageAmount; + membership.PackageAmount = package.Price; - var activationFee = SystemConstants.ClubActivationFee; + var activationFee = package.ActivationFee; ``` ### T2.5-T2.6 — Re-Purchase Logic **EXIT Magic Mode — تغییرات:** ```diff wallet.WalletMode = WalletMode.Normal; wallet.MagicCompletedAt = DateTime.UtcNow; cycle.MagicCompletedAt = DateTime.UtcNow; + user.PackagePurchaseMethod = PackagePurchaseMethod.None; + membership.IsActive = false; + cycle.IsCurrentCycle = false; ``` **Guard تغییرات:** ```diff - if (user.PackagePurchaseMethod != PackagePurchaseMethod.None) - throw new RpcException("قبلاً پکیج خریداری شده"); + if (user.PackagePurchaseMethod != PackagePurchaseMethod.None + && !HasCompletedMagicCycle(membership)) + throw new RpcException("چرخه جاری هنوز تکمیل نشده"); ``` ### T2.7 — JWT Claims جدید ```diff claims.Add("HasPurchasedPackage", "true"); + claims.Add("CanRepurchase", HasCompletedMagicCycle(membership).ToString()); + claims.Add("PackageId", membership.PackageId?.ToString() ?? ""); + claims.Add("PackageTitle", package?.Title ?? ""); ``` --- ## مرحله ۳ — محاسبه پورسانت (موازی با مرحله ۲) > ⏱️ **۴ روز** (v3: +۱) | وابستگی: مرحله ۱ | ریسک: بحرانی (مالی) ### T3.1-T3.2 — SPs + PackageId + پارامترهای داینامیک (🔄 v3) ```sql -- sp_CalculateWeeklyBalances — v3: حذف hardcode ALTER PROCEDURE sp_CalculateWeeklyBalances @WeekDefinitionId BIGINT, @PackageId BIGINT, @MaxBalancesPerLeg INT, -- v3: از Package entity (نه ۳۰۰ hardcode!) @MaxNetworkLevel INT -- v3: از Package entity (نه ۱۵ hardcode!) AS BEGIN -- فیلتر: فقط کاربرانی که این پکیج را دارند -- carryover: فقط رکوردهای PackageId = @PackageId -- cap: از @MaxBalancesPerLeg (نه ۳۰۰) -- depth: CTE تا @MaxNetworkLevel (نه ۱۵) INSERT INTO "CMS"."NetworkWeeklyBalances" ("PackageId", ...) SELECT @PackageId, ... FROM "CMS"."UserWallets" w INNER JOIN "CMS"."ClubMemberships" m ON m."UserId" = w."UserId" WHERE m."PackageId" = @PackageId AND m."IsActive" = true; END; ``` ### T3.3 — Loop Service (🔄 v3: ارسال تنظیمات پکیج) ```csharp // WeeklyCommissionCalculationService.cs var activePackages = await _context.Packages .Where(p => p.IsActive && !p.IsDeleted) .ToListAsync(); foreach (var package in activePackages) { _logger.LogInformation( "Calculating commission for package {Id}: {Title} " + "(MaxBalances={Max}, MaxLevel={Level})", package.Id, package.Title, package.MaxBalancesPerLeg, package.MaxNetworkLevel); // v3: پاس دادن تنظیمات پکیج await strategy.CalculateWeeklyBalancesAsync( weekId, package.Id, package.MaxBalancesPerLeg, package.MaxNetworkLevel); await strategy.CalculateWeeklyPoolAsync(weekId, package.Id); } ``` ### T3.4 — OrmCommissionCalculationStrategy (🔄 v3) **تغییرات:** ```diff - var maxBalances = SystemConstants.CommissionMaxWeeklyBalancesPerLeg; // 300 - var maxLevel = SystemConstants.CommissionMaxNetworkLevel; // 15 + // پارامتر از بیرون — per-package + int maxBalances = maxBalancesPerLeg; // e.g., نقره‌ای=30, پایه=300 + int maxLevel = maxNetworkLevel; - // فیلتر کاربران + // فیلتر کاربران بر اساس پکیج + .Where(m => m.PackageId == packageId && m.IsActive) - // carryover + // carryover: فقط رکوردهای همان PackageId + .Where(b => b.PackageId == packageId && b.WeekDefinitionId == prevWeekId) ``` ### T3.5 — SpCommissionCalculationStrategy (🆕 v3) ```csharp // قبل: فقط WeekDefinitionId await connection.ExecuteAsync("CMS.sp_CalculateWeeklyBalances", new { WeekDefinitionId = weekId, ForceRecalculate = true }); // بعد (v3): پکیج + تنظیمات داینامیک await connection.ExecuteAsync("CMS.sp_CalculateWeeklyBalances", new { WeekDefinitionId = weekId, PackageId = package.Id, MaxBalancesPerLeg = package.MaxBalancesPerLeg, MaxNetworkLevel = package.MaxNetworkLevel, ForceRecalculate = true }); ``` ### T3.6 — Carryover per-package (🆕 v3) > ⚠️ **بحرانی:** week-shifting باید فقط رکوردهای همان PackageId را shift کند ``` هفته ۱۰ → هفته ۱۱: علی: carryover_پایه = {Left: surplus, Right: surplus} ← جداگانه علی: carryover_نقره‌ای = {Left: 0, Right: 0} ← جداگانه ✖ اشتباه: قاطی کردن carryover پایه و نقره‌ای! ✔ صحیح: هر PackageId فقط carryover خودش را می‌بینه ``` ### ⚠️ نکته بحرانی > پورسانت = پول واقعی. **هر تغییر در SPs باید:** > 1. ابتدا در staging با داده واقعی تست شود > 2. نتایج قبل و بعد مقایسه شوند > 3. Rollback plan آماده باشد > 4. در production ابتدا read-only اجرا شود (بدون commit) --- ## مرحله ۴ — UI (FrontOffice + BackOffice) > ⏱️ **۵ روز** (v3: +۲) | وابستگی: مرحله ۲ + ۳ | ریسک: متوسط ### T4.1 — کاشی‌های پکیج داینامیک **فایل:** `FrontOffice/src/.../Pages/Package/Packages.razor` ```razor @* قبل: hardcoded *@ @* بعد: *@ @foreach (var package in _packages.OrderBy(p => p.SortOrder)) { } ``` ### T4.2 — مودال پرداخت شرطی ```razor @if (_selectedPackage.SupportsDirectPurchase) { پرداخت آنلاین } @if (_selectedPackage.SupportsDayaPurchase) { اقساط دایا } ``` ### T4.3 — MyPackages + Re-Purchase ```razor @if (_canRepurchase) { 🎉 چرخه جادویی تکمیل شد! می‌توانید پکیج جدید بخرید. خرید پکیج جدید } else { } ``` ### T4.8 — FrontOffice: CommissionDashboard per-package (🆕 v3) **فایل:** `FrontOffice/src/.../Pages/Commission/CommissionDashboardPage.razor` ```razor @* v3: کارت‌های خلاصه بر اساس پکیج *@ @foreach (var summary in _packageSummaries) { @summary.PackageTitle

زیرمجموعه: @summary.DownlineCount نفر

تیم اول: @summary.LeftLegMembers | تیم دوم: @summary.RightLegMembers

تعادل: @summary.TotalBalances

💰 پاداش: @summary.CommissionFormatted

}
📦 مجموع پاداش هفته: @_totalCommission ``` ### T4.9 — FrontOffice: WeeklyBalance per-package (🆕 v3) > تعادل تیم اول/دوم به تفکیک هر پکیج نمایش داده شود ### T4.10-T4.12 — BackOffice: گزارش‌های پورسانت per-package (🆕 v3) **تغییرات مشترک:** - اضافه dropdown فیلتر پکیج (همه / پایه / نقره‌ای / ...) - اضافه ستون پکیج در جدول‌ها - CSV export شامل ستون پکیج | صفحه | تغییر | |------|-------| | Commission Dashboard | فیلتر + خلاصه per-package | | Weekly Reports | فیلتر + CSV per-package | | Balances Report | فیلتر + ستون پکیج | | User Payouts | ستون پکیج + فیلتر | ### T4.13 — BackOffice: Package CRUD + Quick Access فیچرها (🆕 v3) > ادمین موقع ساخت/ویرایش پکیج، فیچرها را با checkbox در همان فرم تعیین کند ```razor @* در PackageCreateDialog / PackageEditDialog: *@ فیچرهای پکیج @foreach (var feature in _allFeatures) { } ``` --- ## مرحله ۵ — تست و استقرار > ⏱️ **۳ روز** (v3: +۱) | وابستگی: مرحله ۴ ### Checklist تست **خرید + فعال‌سازی:** - [ ] خرید پکیج نقره‌ای (ZarinPal) - [ ] خرید پکیج پایه (ZarinPal) - [ ] خرید پکیج پایه (Daya Loan) - [ ] خرید پکیج پایه (Manual Payment) - [ ] فعالسازی باشگاه با پکیج نقره‌ای → فیچرهای محدود - [ ] فعالسازی باشگاه با پکیج پایه → همه فیچرها **چرخه Magic + خرید مجدد:** - [ ] تکمیل چرخه Magic → ریست وضعیت - [ ] خرید مجدد بعد تکمیل چرخه (همان پکیج) - [ ] خرید مجدد با پکیج متفاوت (پایه → نقره‌ای) **پورسانت per-package (v3):** - [ ] Commission Pool جداگانه هر پکیج - [ ] تعادل per-package: MaxBalancesPerLeg متفاوت (پایه=۳۰۰, نقره‌ای=۳۰) - [ ] Carryover مجزا: shift فقط رکوردهای همان PackageId - [ ] SP پارامترها صحیح: @MaxBalancesPerLeg و @MaxNetworkLevel از Package - [ ] NetworkWeeklyBalance رکوردها: ۲ پکیج = ۲× رکورد **گزارش per-package (v3):** - [ ] FO: مشتری کارت‌های خلاصه per-package را می‌بیند - [ ] FO: مجموع پاداش = جمع همه پکیج‌ها - [ ] BO: فیلتر dropdown پکیج کار می‌کند - [ ] BO: CSV export شامل ستون پکیج **Migration + سایر:** - [ ] Data Migration — PackageId در رکوردهای قبلی (شامل NetworkWeeklyBalance) - [ ] JWT claims جدید (CanRepurchase, PackageId) - [ ] UI: کاشی‌های داینامیک FrontOffice - [ ] UI: ماتریس فیچر + Quick Access BackOffice - [ ] Rollback: بدون data loss --- ## 📅 تقویم پیشنهادی (v3) | هفته | روز | تسک | |------|-----|------| | هفته ۱ | روز ۱ | مرحله ۰: فیکس ۴ باگ | | | روز ۲-۳ | مرحله ۱: Package entity (۱۱ فیلد) + PackageFeature | | | روز ۴-۵ | مرحله ۱: FKها + NetworkWeeklyBalance + Migration | | هفته ۲ | روز ۶-۷ | مرحله ۲: Generic handlers + re-purchase | | | روز ۶-۸ | مرحله ۳: SP params + carryover per-package (موازی) | | | روز ۸-۱۰ | مرحله ۲: Guards + JWT + Manual | | هفته ۳ | روز ۱۱-۱۲ | مرحله ۴: FrontOffice UI + گزارش per-package | | | روز ۱۳-۱۴ | مرحله ۴: BackOffice UI + گزارش per-package | | | روز ۱۵-۱۷ | مرحله ۵: تست + deploy | --- ## 🔗 ارجاعات | مستند | محتوا | |-------|-------| | [BIZ-PACKAGE-BASED-SYSTEM.md](../business/BIZ-PACKAGE-BASED-SYSTEM.md) | طراحی فنی — **۴۸+ تغییر** (v3) + باگ‌ها | | [PACKAGE-TRANSFORMATION-UX.md](PACKAGE-TRANSFORMATION-UX.md) | تاثیر UX بر فرانت‌ها | | [FEATURE-BACKLOG.md](FEATURE-BACKLOG.md) | بکلاگ ۱۲ RPC آماده | | [GRPC-SERVICES-AUDIT.md](../cms/GRPC-SERVICES-AUDIT.md) | آدیت ۳۴۲ RPC | --- *آخرین بروزرسانی: ۱۴۰۴/۱۲/۰۶ — v3 (پورسانت per-package + گزارش‌دهی + carryover)*