73e1971cc3
- Added DiscountProductImage entity and related configurations for product image gallery. - Created commands and queries for managing product images. - Developed GetAllDiscountOrders API for admin order management with various filters. - Implemented VAT calculation service and integrated it into order processing. - Created Sales Reports API with support for daily, weekly, and monthly reports. - Completed gRPC services for BackOffice.BFF to expose new APIs. - Updated Proto files and project references accordingly.
10 KiB
10 KiB
📦 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 به موجودی
⚠️ محدودیتها و قوانین
- محصول پکیج خودش موجودی ندارد - فقط موجودی محصولات داخلش مهم است
- پکیج داخل پکیج ممنوع - فقط محصولات ساده (
Simple) میتوانند داخل پکیج باشند - حذف محصول از پکیج - اگر محصولی در پکیج استفاده شده، نمیتواند حذف شود
- موجودی قابل فروش پکیج =
MIN(موجودی هر محصول داخل / تعداد آن در پکیج)
📁 فایلهای جدید/تغییریافته
فایلهای جدید:
CMSMicroservice.Domain/Enums/ProductTypeCategory.csCMSMicroservice.Domain/Entities/ProductBundleItem.csCMSMicroservice.Application/Features/ProductBundleItems/*CMSMicroservice.Infrastructure/Repositories/ProductBundleItemRepository.cs
فایلهای تغییریافته:
CMSMicroservice.Domain/Entities/Product.cs- اضافه کردنTypeCategoryوBundleItemsCMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs- DbSet و ConfigurationCMSMicroservice.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 ساعت |
📝 یادداشتها
- این فیچر با پکیج عضویت (
Packageentity موجود) متفاوت است - نیاز به تست دقیق منطق انبارداری دارد
- UI نیاز به multi-select برای انتخاب محصولات داخل پکیج دارد
این داکیومنت برای پیادهسازی آینده نگهداری میشود.