Files
docs/cms/PRODUCT-BUNDLE-FEATURE.md
T

10 KiB
Raw Blame History

📦 Product Bundle Feature (پکیج محصولات)

وضعیت: ⏸️ Postponed - مستند شده برای پیاده‌سازی آینده

تاریخ: ۱۲ دی ۱۴۰۴ (1 January 2026)


📋 خلاصه نیازمندی

امکان ایجاد پکیج محصولات که:

  • یک محصول با نوع "پکیج" ایجاد می‌شود (همه فیلدها مثل محصول عادی)
  • این پکیج شامل چند محصول است
  • هنگام خرید پکیج، موجودی تمام محصولات داخل کم می‌شود
  • هنگام مرجوعی، موجودی تمام محصولات برمی‌گردد

🏗️ تغییرات مورد نیاز

1. Domain Layer

1.1 Enum جدید: ProductTypeCategory

// CMSMicroservice.Domain/Enums/ProductTypeCategory.cs
public enum ProductTypeCategory
{
    Simple = 1,   // محصول ساده
    Bundle = 2    // پکیج (بسته محصولات)
}

1.2 فیلد جدید در Product Entity

// Product.cs - اضافه کردن فیلد
public ProductTypeCategory TypeCategory { get; set; } = ProductTypeCategory.Simple;

1.3 Entity جدید: ProductBundleItem (جدول واسط)

// CMSMicroservice.Domain/Entities/ProductBundleItem.cs
public class ProductBundleItem : BaseAuditableEntity
{
    /// <summary>
    /// شناسه محصول پکیج (والد)
    /// </summary>
    public long BundleProductId { get; set; }
    public virtual Product BundleProduct { get; set; } = null!;
    
    /// <summary>
    /// شناسه محصول داخل پکیج (فرزند)
    /// </summary>
    public long ChildProductId { get; set; }
    public virtual Product ChildProduct { get; set; } = null!;
    
    /// <summary>
    /// تعداد این محصول در پکیج
    /// </summary>
    public int Quantity { get; set; } = 1;
}

2. Infrastructure Layer

2.1 DbContext Configuration

// ApplicationDbContext.cs
public DbSet<ProductBundleItem> ProductBundleItems => Set<ProductBundleItem>();

// Configuration
modelBuilder.Entity<ProductBundleItem>(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()

public async Task<bool> 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

public record CreateNewProductsCommand : IRequest<long>
{
    // ... existing fields ...
    
    public ProductTypeCategory TypeCategory { get; init; } = ProductTypeCategory.Simple;
    
    /// <summary>
    /// لیست محصولات داخل پکیج (فقط وقتی TypeCategory == Bundle)
    /// </summary>
    public List<BundleItemDto>? BundleItems { get; init; }
}

public record BundleItemDto
{
    public long ProductId { get; init; }
    public int Quantity { get; init; } = 1;
}

3.2 Repository جدید: IProductBundleItemRepository

public interface IProductBundleItemRepository : IRepository<ProductBundleItem>
{
    Task<List<ProductBundleItem>> 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

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 برای انتخاب محصولات داخل پکیج دارد

این داکیومنت برای پیاده‌سازی آینده نگهداری می‌شود.