304 lines
10 KiB
Markdown
304 lines
10 KiB
Markdown
# 📦 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 برای انتخاب محصولات داخل پکیج دارد
|
||
|
||
---
|
||
|
||
*این داکیومنت برای پیادهسازی آینده نگهداری میشود.*
|