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

304 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 📦 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
{
/// <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
```csharp
// 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()`
```csharp
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`
```csharp
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`
```csharp
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`
```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 برای انتخاب محصولات داخل پکیج دارد
---
*این داکیومنت برای پیاده‌سازی آینده نگهداری می‌شود.*