# 📦 Product Bundle Feature (پکیج محصولات) > **وضعیت:** ⏸️ Postponed - مستند شده برای پیاده‌سازی آینده > > **تاریخ:** ۱۲ دی ۱۴۰۴ (1 January 2026) --- ## 📋 خلاصه نیازمندی امکان ایجاد **پکیج محصولات** که: - یک محصول با نوع "پکیج" ایجاد می‌شود (همه فیلدها مثل محصول عادی) - این پکیج شامل **چند محصول** است - هنگام **خرید پکیج**، موجودی **تمام محصولات داخل** کم می‌شود - هنگام **مرجوعی**، موجودی تمام محصولات برمی‌گردد --- ## 🏗️ تغییرات مورد نیاز ### 1. Domain Layer #### 1.1 Enum جدید: `ProductTypeCategory` ```csharp // CMSMicroservice.Domain/Enums/ProductTypeCategory.cs public enum ProductTypeCategory { Simple = 1, // محصول ساده Bundle = 2 // پکیج (بسته محصولات) } ``` #### 1.2 فیلد جدید در `Product` Entity ```csharp // Product.cs - اضافه کردن فیلد public ProductTypeCategory TypeCategory { get; set; } = ProductTypeCategory.Simple; ``` #### 1.3 Entity جدید: `ProductBundleItem` (جدول واسط) ```csharp // CMSMicroservice.Domain/Entities/ProductBundleItem.cs public class ProductBundleItem : BaseAuditableEntity { /// /// شناسه محصول پکیج (والد) /// public long BundleProductId { get; set; } public virtual Product BundleProduct { get; set; } = null!; /// /// شناسه محصول داخل پکیج (فرزند) /// public long ChildProductId { get; set; } public virtual Product ChildProduct { get; set; } = null!; /// /// تعداد این محصول در پکیج /// public int Quantity { get; set; } = 1; } ``` ### 2. Infrastructure Layer #### 2.1 DbContext Configuration ```csharp // ApplicationDbContext.cs public DbSet ProductBundleItems => Set(); // Configuration modelBuilder.Entity(entity => { entity.ToTable("ProductBundleItems", "CMS"); entity.HasOne(x => x.BundleProduct) .WithMany(p => p.BundleItems) .HasForeignKey(x => x.BundleProductId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(x => x.ChildProduct) .WithMany() .HasForeignKey(x => x.ChildProductId) .OnDelete(DeleteBehavior.Restrict); // یک محصول فقط یکبار در یک پکیج entity.HasIndex(x => new { x.BundleProductId, x.ChildProductId }).IsUnique(); }); ``` #### 2.2 آپدیت `InventoryService.ConfirmSaleAsync()` ```csharp public async Task ConfirmSaleAsync( long productId, ProductType productType, int quantity, long? orderId = null, CancellationToken ct = default) { // چک کردن آیا محصول پکیج است var product = await _dbContext.Products .Include(p => p.BundleItems) .ThenInclude(bi => bi.ChildProduct) .FirstOrDefaultAsync(p => p.Id == productId, ct); if (product?.TypeCategory == ProductTypeCategory.Bundle) { // کم کردن موجودی تمام محصولات داخل پکیج foreach (var bundleItem in product.BundleItems) { await ConfirmSaleForSingleProduct( bundleItem.ChildProductId, productType, quantity * bundleItem.Quantity, // ضرب در تعداد خرید شده orderId, ct); } return true; } // محصول ساده - روال عادی return await ConfirmSaleForSingleProduct(productId, productType, quantity, orderId, ct); } ``` ### 3. Application Layer #### 3.1 آپدیت `CreateNewProductsCommand` ```csharp public record CreateNewProductsCommand : IRequest { // ... existing fields ... public ProductTypeCategory TypeCategory { get; init; } = ProductTypeCategory.Simple; /// /// لیست محصولات داخل پکیج (فقط وقتی TypeCategory == Bundle) /// public List? BundleItems { get; init; } } public record BundleItemDto { public long ProductId { get; init; } public int Quantity { get; init; } = 1; } ``` #### 3.2 Repository جدید: `IProductBundleItemRepository` ```csharp public interface IProductBundleItemRepository : IRepository { Task> GetByBundleProductIdAsync(long bundleProductId, CancellationToken ct = default); Task SetBundleItemsAsync(long bundleProductId, List<(long ProductId, int Quantity)> items, CancellationToken ct = default); } ``` ### 4. Proto/gRPC Layer #### 4.1 آپدیت `products.proto` ```protobuf enum ProductTypeCategory { PRODUCT_TYPE_SIMPLE = 0; PRODUCT_TYPE_BUNDLE = 1; } message BundleItemMessage { int64 product_id = 1; int32 quantity = 2; } message CreateNewProductsRequest { // ... existing fields ... ProductTypeCategory type_category = 15; repeated BundleItemMessage bundle_items = 16; } message ProductDto { // ... existing fields ... ProductTypeCategory type_category = 20; repeated BundleItemMessage bundle_items = 21; } ``` --- ## 📊 دیاگرام رابطه‌ها ``` ┌─────────────────┐ │ Products │ ├─────────────────┤ │ Id │◄──────────────────┐ │ Title │ │ │ TypeCategory │ ← Simple/Bundle │ │ ... │ │ └────────┬────────┘ │ │ │ │ 1:N (Bundle → Items) │ ▼ │ ┌─────────────────────┐ │ │ ProductBundleItems │ │ ├─────────────────────┤ │ │ Id │ │ │ BundleProductId (FK)│───────────────┘ │ ChildProductId (FK) │───────────────┐ │ Quantity │ │ └─────────────────────┘ │ │ ┌────────────────────────────┘ ▼ ┌─────────────────┐ │ Products │ │ (Child Item) │ └─────────────────┘ ``` --- ## 🔄 Flow خرید پکیج ``` 1. کاربر پکیج را به سبد اضافه می‌کند └── CartItem { ProductId: 100, Count: 2 } // پکیج شامل 3 محصول 2. سفارش ثبت می‌شود └── PlaceOrderCommandHandler.ReserveStock() ├── Check: Product.TypeCategory == Bundle ├── Get: BundleItems = [ │ { ChildProductId: 10, Quantity: 1 }, │ { ChildProductId: 20, Quantity: 2 }, │ { ChildProductId: 30, Quantity: 1 } │ ] └── Reserve: ├── Product 10: Reserve 2×1 = 2 عدد ├── Product 20: Reserve 2×2 = 4 عدد └── Product 30: Reserve 2×1 = 2 عدد 3. پرداخت موفق └── ConfirmSaleAsync() ├── Product 10: -2 از موجودی ├── Product 20: -4 از موجودی └── Product 30: -2 از موجودی 4. مرجوعی (در صورت نیاز) └── ProcessReturnAsync() ├── Product 10: +2 به موجودی ├── Product 20: +4 به موجودی └── Product 30: +2 به موجودی ``` --- ## ⚠️ محدودیت‌ها و قوانین 1. **محصول پکیج خودش موجودی ندارد** - فقط موجودی محصولات داخلش مهم است 2. **پکیج داخل پکیج ممنوع** - فقط محصولات ساده (`Simple`) می‌توانند داخل پکیج باشند 3. **حذف محصول از پکیج** - اگر محصولی در پکیج استفاده شده، نمی‌تواند حذف شود 4. **موجودی قابل فروش پکیج** = `MIN(موجودی هر محصول داخل / تعداد آن در پکیج)` --- ## 📁 فایل‌های جدید/تغییریافته ### فایل‌های جدید: - `CMSMicroservice.Domain/Enums/ProductTypeCategory.cs` - `CMSMicroservice.Domain/Entities/ProductBundleItem.cs` - `CMSMicroservice.Application/Features/ProductBundleItems/*` - `CMSMicroservice.Infrastructure/Repositories/ProductBundleItemRepository.cs` ### فایل‌های تغییریافته: - `CMSMicroservice.Domain/Entities/Product.cs` - اضافه کردن `TypeCategory` و `BundleItems` - `CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs` - DbSet و Configuration - `CMSMicroservice.Infrastructure/Services/InventoryService.cs` - منطق پکیج - `CMSMicroservice.Application/ProductsCQ/Commands/CreateNewProducts/*` - `CMSMicroservice.Protobuf/Protos/products.proto` - Order Handlers (Reserve, Confirm, Release) --- ## ⏱️ تخمین زمان | تسک | زمان تخمینی | |-----|-------------| | Domain entities & enums | 30 دقیقه | | EF Migration | 15 دقیقه | | Repository | 30 دقیقه | | InventoryService update | 1 ساعت | | CQRS handlers | 1 ساعت | | Proto & gRPC | 45 دقیقه | | تست و دیباگ | 1 ساعت | | **جمع** | **~5 ساعت** | --- ## 📝 یادداشت‌ها - این فیچر با پکیج عضویت (`Package` entity موجود) متفاوت است - نیاز به تست دقیق منطق انبارداری دارد - UI نیاز به multi-select برای انتخاب محصولات داخل پکیج دارد --- *این داکیومنت برای پیاده‌سازی آینده نگهداری می‌شود.*