# 🔄 نقشه‌راه تحول پکیج‌بیس — تسک‌های گام‌به‌گام > **وضعیت:** در حال اجرا — **فاز ۰-۱۰ (تکمیل کد + استقرار 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`→`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 --- ## 📊 نمای کلی ``` مرحله ۰: فیکس باگ فوری (۱ روز) ✅ `8b9c317` + `fe3edd1` └─→ مرحله ۱: زیرساخت Domain + DB (۴ روز) ✅ `ae92ab8` └─→ مرحله ۱.۵: Migration + Seed ✅ `a9cd2fd` ├─→ مرحله ۲: منطق کسب‌وکار (۴ روز) ✅ `8e5c7c5` │ └─→ مرحله ۳: بازسازی لایه Package ✅ `ccb938e` │ └─→ مرحله ۴: CRUD + Legacy Fixes ✅ `0002a5a` │ └─→ مرحله UI (۵ روز) ✅ └─→ مرحله ۳: پورسانت (۴ روز) ✅ `607f791`+`7176fe4` └─→ مرحله ۶: Deprecation cleanup ✅ `d19c569` └─→ مرحله ۷: Migration + Cleanup ✅ ├─→ 7a: Cosmetic cleanup ✅ CMS:`469d97b` FO:`b82cac4` BO:`f1b0085` ├─→ 7b: FO RPC migration ✅ CMS:`161f796` FO:`71f391a` └─→ 7c: Delete deprecated ✅ CMS:`8446e0e` └─→ مرحله ۸: FO/BO Completion ├─→ 8a: Checkout wire-up ✅ FO:`0bbc11e` ├─→ 8b: BO CRUD expansion ✅ CMS:`ce8e248` BO:`89f5241` ├─→ 8c: FO Package pages ✅ FO:`d71d463` └─→ 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→۹→۱۰→۵ = ~۲۲ روز | انجام‌شده: ۰→10d (~۲۰ روز) ``` --- ## مرحله ۰ — فیکس باگ‌های فوری ✅ > ✅ تکمیل‌شده | کامیت: `8b9c317` + `fe3edd1` ### ✅ وضعیت باگ‌ها (بررسی اولیه لازم) | # | باگ | 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) ✅ > ✅ تکمیل‌شده | کامیت: `ae92ab8` (Phase 1) + `a9cd2fd` (Phase 1.5 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; ``` --- ## مرحله ۲ — منطق کسب‌وکار ✅ > ✅ تکمیل‌شده | کامیت: `8e5c7c5` (Phase 2) + `ccb938e` (Phase 3) + `0002a5a` (Phase 4) ### 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 ?? ""); ``` --- ## مرحله ۳ — محاسبه پورسانت (موازی با مرحله ۲) ✅ > ✅ تکمیل‌شده | کامیت: `607f791` + `7176fe4` | ⏱️ **۴ روز** | ریسک: بحرانی (مالی) ### 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) --- ## مرحله ۷ — Migration + Cleanup (سه‌گانه) ✅ > ✅ تکمیل‌شده | ⏱️ **۱ روز** | ریسک: پایین ### فاز 7a — Cosmetic Cleanup ✅ > کامیت: CMS `469d97b` | FO `b82cac4` | BO `f1b0085` **CMS:** - حذف orphaned `PurchasePackage` handler (۳ فایل، بدون caller) - فیکس doc-comments: `طلایی` → `پکیج` در ۶ فایل (enums, entities, handlers) **FrontOffice:** - حذف hardcoded `پکیج طلایی` از `MyPackages.razor` و `Packages.razor` - اضافه `PackageTitle` property به `UserPackageStatusDto` record **BackOffice:** - تغییر label `پکیج طلایی` → `خرید پکیج` در `UserNetworkInfo.razor` ### فاز 7b — FrontOffice RPC Migration ✅ > کامیت: CMS `161f796` | FO `71f391a` **CMS:** - `CustomerPurchasePackage`: embed `orderId` در callback URL قبل از ارسال به درگاه - `$"{request.CallbackUrl}{separator}orderId={purchase.Id}"` **FrontOffice:** - `Profile/Index.razor.cs`: مهاجرت `InitiateBasePackagePaymentAsync` → `CustomerPurchasePackageAsync` - `Profile/PaymentCallback.razor`: مهاجرت `VerifyBasePackagePaymentAsync` → `CustomerVerifyPackagePurchaseAsync` - پارامترهای جدید: `PackageId`, `CallbackUrl`, `PurchaseMethod`, `OrderId`, `Authority`, `Status` ### فاز 7c — Delete Deprecated Handlers ✅ > کامیت: CMS `8446e0e` (14 فایل، 1125 حذف) **حذف ۴ handler CQRS (۱۲ فایل):** - `PurchaseGoldenPackage/` (Command, Handler, Validator) - `VerifyGoldenPackagePurchase/` (Command, Handler, Validator) - `InitiateBasePackagePayment/` (Command, Handler, Validator) - `VerifyBasePackagePayment/` (Command, Handler, Validator) **Cleanup:** - `PackageService.cs`: حذف ۴ gRPC override method (proto RPCs حالا auto-throw `Unimplemented`) - `PackageProfile.cs`: حذف ۶ Mapster mapping block + ۴ using directive - Build: 0 Error ✅ --- ## مرحله ۸ — FrontOffice Checkout + NuGet > 🔄 در حال اجرا | فاز 8a تکمیل ✅ ### فاز 8a — Checkout Wire-up ✅ > کامیت: FO `0bbc11e` **Checkout.razor.cs:** - حذف dead code: `ProcessPayment()` از flow قدیمی `TransactionsContract + UserOrderContract` استفاده می‌کرد - Rewrite با `CustomerPurchasePackageAsync` (مثل Profile/Index.razor.cs) - Callback URL → `/profile/payment-callback` (از صفحه verify موجود استفاده مجدد) - حذف DI بلااستفاده: `UserOrderContract`, `TransactionContract` - حذف usings: `Transactions`, `UserOrder`, `WellKnownTypes` **Profile/Index.razor.cs (cosmetic):** - Rename `basePackage` → `selectedPackage`, `tempCallbackUrl` → `callbackUrl` ### فاز 8b — BackOffice Package CRUD Expansion ✅ > کامیت: CMS `ce8e248` | BO `89f5241` (7 فایل، +138/-23) **NuGet Rebuild:** - Proto version bump: `0.0.184` → `0.0.185` - Pack و deploy به local feed (`/nupkg`) - BackOffice NuGet.config: اضافه local feed source **CreateDialog.razor (۱۲ فیلد جدید):** - `SortOrder` — MudNumericField ترتیب نمایش - `ActivationFee` — MudNumericField هزینه فعال‌سازی - `DiscountMultiplier` — MudNumericField ضریب تخفیف - `MagicWalletMultiplier` — MudNumericField ضریب کیف پول جادویی - `MagicWalletMaxDeposit` — MudNumericField سقف واریز جادویی - `MagicWalletMaxCredit` — MudNumericField سقف اعتبار جادویی - `MaxBalancesPerLeg` — MudNumericField حداکثر تعادل هر پا - `MaxNetworkLevel` — MudNumericField حداکثر سطح شبکه - `IsActive` — MudCheckBox فعال/غیرفعال - `IsBasePackage` — MudCheckBox پکیج پایه - `SupportsDirectPurchase` — MudCheckBox پرداخت مستقیم - `SupportsDayaPurchase` — MudCheckBox اعتبار دایا **UpdateDialog.razor:** همان ۱۲ فیلد **PackageMainPage Grid (۴ ستون جدید):** - `Price` — فرمت‌شده با N0 - `SortOrder` — ترتیب - `IsActive` — MudChip فعال/غیرفعال - `IsBasePackage` — MudChip پایه/عادی **سایر:** - Dialog size: `MaxWidth.Small` → `MaxWidth.Medium` - CreateNew defaults: `IsActive=true, DiscountMultiplier=2.0, MagicWalletMultiplier=2.5, ...` - فیکس `HasPurchasedGoldenPackage` → `HasPurchasedPackage` در `UserNetworkInfo.razor` ### فاز 8c — FrontOffice Package Pages ✅ > کامیت: FO `d71d463` (5 فایل، +109/-56) **NuGet:** `0.0.182` → `0.0.185` + local feed source **PackageDetail.razor.cs:** - مهاجرت `GetPackageAsync` (admin RPC) → `GetCustomerPackageDetailsAsync` (customer RPC) - Features: از hardcoded ثابت → از `PackageFeature` API داینامیک - Specifications: از hardcoded → از `PackageFeature.IsHighlighted` API - حذف ۵ hardcoded feature string + ۴ hardcoded specification **PackageService.cs:** - `PackageDto`: اضافه ۸ فیلد جدید (ActivationFee, DiscountMultiplier, MagicWalletMultiplier, etc.) - `GetAllPackagesAsync`: مپ فیلدهای جدید از `CustomerPackageModel` - `GetUserPackageStatusAsync`: از stub → اتصال واقعی به `GetUserPackageStatusAsync` RPC **Packages.razor:** - Un-exclude از build (حذف `` + ``) - جایگزینی ۳ feature bullet hardcoded → dynamic features: - `SupportsDirectPurchase` → پرداخت مستقیم - `SupportsDayaPurchase` → پرداخت با اعتبار دایا - `DiscountMultiplier` → ضریب تخفیف: X.Xx - `MagicWalletMultiplier` → کیف پول جادویی: X.Xx - `IsBasePackage` → پکیج پایه ⭐ ### فاز 8d — Proto Cleanup ✅ > کامیت: CMS `7554d70` (2 فایل، -103) | FO `40882c8` | BO `c96377a` **حذف ۴ deprecated RPC:** - `PurchaseGoldenPackage` — جایگزین: `CustomerPurchasePackage` - `VerifyGoldenPackagePurchase` — جایگزین: `CustomerVerifyPackagePurchase` - `InitiateBasePackagePayment` — جایگزین: `CustomerPurchasePackage` - `VerifyBasePackagePayment` — جایگزین: `CustomerVerifyPackagePurchase` **حذف ۸ deprecated message type:** - `PurchaseGoldenPackageRequest` / `PurchaseGoldenPackageResponse` - `VerifyGoldenPackagePurchaseRequest` / `VerifyGoldenPackagePurchaseResponse` - `InitiateBasePackagePaymentRequest` / `InitiateBasePackagePaymentResponse` - `VerifyBasePackagePaymentRequest` / `VerifyBasePackagePaymentResponse` **حفظ شده:** `GetUserPackageStatus` RPC + messages (هنوز در استفاده) **NuGet:** `0.0.185` → `0.0.186` (همه ریپوها) ### فاز 8e — Per-Package Commission Reports ✅ > کامیت: CMS `aaaf7fc` | FO `a956cb9` | BO `8be98ae` **Proto (commission.proto):** - اضافه `package_id` فیلتر به ۴ request message: `GetUserCommissionPayoutsRequest`, `GetUserWeeklyBalancesRequest`, `GetMyCommissionPayoutsRequest`, `GetMyWeeklyBalancesRequest` - اضافه `package_id` + `package_title` به ۴ response model: `UserCommissionPayoutModel`, `UserWeeklyBalanceModel`, `CustomerCommissionPayoutModel`, `CustomerWeeklyBalanceModel` **CMS (12 فایل):** - ۴ Query record: اضافه `public long? PackageId { get; init; }` - ۴ Handler: اضافه `.Include(x => x.Package)` + فیلتر `Where(x => x.PackageId == request.PackageId.Value)` + map `PackageId`/`PackageTitle` - ۳ Response DTO: اضافه `PackageId` + `PackageTitle` - `CommissionProfile.cs`: تنظیم mapping‌های Mapster برای admin + customer **BackOffice (6 فایل):** - کامپوننت جدید `PackageSelect.razor/.cs`: dropdown قابل استفاده مجدد با بارگذاری پکیج‌ها از `PackageContract` - `UserPayouts.razor/.cs`: فیلتر PackageSelect + ستون پکیج با MudChip - `BalancesReport.razor`: فیلتر PackageSelect + ستون پکیج با MudChip + mapping PackageTitle **FrontOffice (7 فایل):** - `CommissionDtos.cs`: اضافه `PackageId` + `PackageTitle` به `CommissionPayoutDto` و `WeeklyBalanceDto` - `CommissionService.cs`: اضافه پارامتر `packageId` به `GetMyCommissionPayoutsAsync` و `GetMyWeeklyBalanceAsync` - `CommissionDashboardPage.razor/.cs`: فیلتر dropdown پکیج + ستون «پکیج» با MudChip (دسکتاپ + موبایل) - `WeeklyBalancePage.razor/.cs`: فیلتر MudSelect پکیج + نمایش MudChip پکیج در بخش اطلاعات هفته **NuGet:** `0.0.186` → `0.0.187` (همه ریپوها) ### فاز 8f — UI Completion (T4.2 + T4.3 + T4.13 + F2 + F3) ✅ > کامیت: CMS `dcd1135` | FO `3bffc13` | BO `e020354` **CMS (T4.13 — PackageFeature CRUD):** - Proto: اضافه `repeated int64 feature_ids` به ۴ message (Create/Update Request, Get/GetAll Response) - `CreateNewPackageCommand/Handler`: sync FeatureIds → ساخت `PackageFeature` records - `UpdatePackageCommand/Handler`: sync FeatureIds → حذف قبلی‌ها + ساخت جدید - `GetPackage/GetAllPackageByFilter`: اضافه `.Include(x => x.PackageFeatures)` + map FeatureIds **BackOffice (F2 + T4.13):** - **F2:** کامپوننت جدید `ChangeParentDialog.razor/.cs` — مودال جابجایی در شبکه با NewParentId, NewLeg, Reason - **F2:** دکمه «تغییر والد» در `UserNetworkInfo.razor` - **T4.13:** checkbox matrix فیچرها در `CreateDialog` و `UpdateDialog` — بارگذاری از `ConfigurationContractClient` **FrontOffice (T4.2 + T4.3 + F3):** - **T4.2:** پرداخت شرطی در `Checkout.razor` بر اساس `SupportsDirectPurchase`/`SupportsDayaPurchase` - **T4.3:** منطق خرید مجدد در `MyPackages.razor` — بارگذاری `MagicWalletStatus` + CTA شرطی + progress bar - **F3:** نمایش PV سفارش در `Store/OrderDetail.razor` — `CalculateOrderPVAsync` + جدول PV هر محصول **NuGet:** `0.0.187` → `0.0.188` (همه ریپوها) --- ## مرحله ۴ — 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 — مودال پرداخت شرطی ✅ FO:`3bffc13` **پیاده‌سازی:** - `Checkout.razor.cs`: اضافه `PackageService` injection، بارگذاری پکیج‌ها با `GetAllPackagesAsync()` - `Checkout.razor`: دکمه‌های پرداخت شرطی بر اساس `SupportsDirectPurchase` و `SupportsDayaPurchase` - اضافه `DayaLoanPayment()` method + alert برای عدم وجود روش پرداخت - `Pack` record: اضافه `SupportsDirectPurchase` و `SupportsDayaPurchase` ### T4.3 — MyPackages + Re-Purchase ✅ FO:`3bffc13` **پیاده‌سازی:** - `MyPackages.razor.cs`: بارگذاری `MagicWalletStatus` از `WalletService.GetMagicWalletStatusAsync()` - فرمول خرید مجدد: `WalletMode == 0 && PurchaseCycleCount >= 1 && MagicRemainingDeposit == 0` - `MyPackages.razor`: CTA شرطی «🎉 چرخه جادویی تکمیل شد!» + دکمه «خرید پکیج جدید» - بخش پیشرفت کیف پول جادویی: مبلغ واریزی، باقی‌مانده، اعتبار دریافتی + progress bar ### T4.8 — FrontOffice: CommissionDashboard per-package (🆕 v3) ✅ FO:`a956cb9` **پیاده‌سازی:** - `CommissionDtos.cs`: اضافه `PackageId` + `PackageTitle` به `CommissionPayoutDto` و `WeeklyBalanceDto` - `CommissionService.cs`: اضافه پارامتر `packageId` به `GetMyCommissionPayoutsAsync` و `GetMyWeeklyBalanceAsync` - `CommissionDashboardPage.razor`: اضافه dropdown فیلتر پکیج + ستون «پکیج» با MudChip + نمایش پکیج در card موبایل - `CommissionDashboardPage.razor.cs`: inject `PackageService`، فیلد `_filterPackageId`، بارگذاری لیست پکیج‌ها ### T4.9 — FrontOffice: WeeklyBalance per-package (🆕 v3) ✅ FO:`a956cb9` **پیاده‌سازی:** - `WeeklyBalancePage.razor`: اضافه MudSelect فیلتر پکیج کنار WeekSelector + نمایش MudChip پکیج در بخش اطلاعات هفته - `WeeklyBalancePage.razor.cs`: inject `PackageService`، فیلد `_filterPackageId`، ارسال به `CommissionService.GetMyWeeklyBalanceAsync` ### T4.10-T4.12 — BackOffice: گزارش‌های پورسانت per-package (🆕 v3) ✅ BO:`8be98ae` **پیاده‌سازی:** - کامپوننت جدید `PackageSelect.razor/.cs`: dropdown قابل استفاده مجدد با بارگذاری پکیج‌ها از `PackageContract` - `UserPayouts.razor/.cs`: فیلتر PackageSelect + ستون پکیج با MudChip - `BalancesReport.razor`: فیلتر PackageSelect + ستون پکیج با MudChip + mapping `PackageTitle` ### T4.13 — BackOffice: Package CRUD + Quick Access فیچرها (🆕 v3) ✅ CMS:`dcd1135` BO:`e020354` **CMS پیاده‌سازی:** - Proto: اضافه `repeated int64 feature_ids` به ۴ message (Create/Update Request, Get/GetAll Response) - `CreateNewPackageCommand/Handler`: اضافه `FeatureIds` + ساخت `PackageFeature` records - `UpdatePackageCommand/Handler`: اضافه `FeatureIds` + sync (حذف قبلی‌ها + ساخت جدید) - `GetPackageQueryHandler`: اضافه `.Include(x => x.PackageFeatures)` + map `FeatureIds` - `GetAllPackageByFilterQueryHandler`: اضافه `.Include(x => x.PackageFeatures)` قبل از `PaginatedListAsync` - NuGet: `0.0.187` → `0.0.188` **BO پیاده‌سازی:** - `CreateDialog.razor/.cs`: بارگذاری `ClubFeatures` از `ConfigurationContractClient` + checkbox matrix - `UpdateDialog.razor/.cs`: همان pattern + pre-populate از `Model.FeatureIds` - Mapster: `Adapt()` خودکار `FeatureIds` را map می‌کند --- ## مرحله ۹ — 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` 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` — اولین 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: +۱) | وابستگی: مرحله ۴ ### 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) - [x] UI: کاشی‌های داینامیک FrontOffice - [x] 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 | --- *آخرین بروزرسانی: ۱۴۰۴/۱۲/۰۸ — فاز ۰-۹d تکمیل (۳۰ کامیت: ۲۰ CMS + ۸ FO + ۶ BO) | NuGet v0.0.188 | باقی‌مانده: تست + deploy*