feat: Implement Discount Shop Completion Plan with Product Image Gallery, Admin APIs, VAT Calculation, and Sales Reports
- 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.
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
# 📦 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 برای انتخاب محصولات داخل پکیج دارد
|
||||
|
||||
---
|
||||
|
||||
*این داکیومنت برای پیادهسازی آینده نگهداری میشود.*
|
||||
Reference in New Issue
Block a user