# Refactoring DiscountProduct Form - Session 2026-01-05

## هدف
یکسان‌سازی فرم ایجاد/ویرایش محصولات تخفیفی (DiscountProduct) با فرم محصولات عادی (Product) برای بهبود تجربه کاربری و افزودن قابلیت‌های حرفه‌ای.

## تغییرات انجام شده

### 1. Frontend - ProductFormDialog.razor

#### قبل:
- استفاده از MudGrid
- TextField ساده برای مسیر تصویر (بدون قابلیت آپلود)
- TextField چند خطی برای توضیحات
- MudSelect ساده برای دسته‌بندی‌ها
- فاقد پیش‌نمایش تصویر

#### بعد:
- **تغییر Layout**: از MudGrid به MudStack
- **آپلود تصویر**: 
  - MudFileUpload برای انتخاب تصویر اصلی و بندانگشتی
  - پیش‌نمایش تصویر قبل از آپلود
  - نمایش تصویر موجود در حالت ویرایش
- **ویرایشگر HTML**: 
  - استفاده از MudHtmlEditor برای FullInformation
  - قابلیت فرمت‌دهی متن (bold, italic, لیست و...)
- **دسته‌بندی**:
  - استفاده از MudSelect با MultiSelection
  - لود خودکار DiscountCategory ها (نه Category های عادی)
  - نمایش تعداد دسته‌بندی‌های انتخاب شده
- **فیلدهای اختصاصی حفظ شده**:
  - MaxDiscountPercent
  - IsActive
  - SortOrder

**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice/Pages/DiscountShop/Components/ProductFormDialog.razor`

### 2. Proto File - discountproduct.proto

#### تغییرات:
```protobuf
message CreateDiscountProductRequest {
    // ... فیلدهای قبلی
    ImageFileModel image_file = 12;      // جدید
    ImageFileModel thumbnail_file = 13;  // جدید
}

message UpdateDiscountProductRequest {
    // ... فیلدهای قبلی  
    ImageFileModel image_file = 12;      // جدید
    ImageFileModel thumbnail_file = 13;  // جدید
}

// تعریف مدل جدید
message ImageFileModel {
    bytes file = 1;
    string mime = 2;
    string file_name = 3;
}
```

**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountProduct.Protobuf/Protos/discountproduct.proto`

### 3. DTOs - Backend

#### CreateDiscountProductDto و UpdateDiscountProductDto:
```csharp
// پراپرتی‌های جدید اضافه شده:
public byte[]? ImageFile { get; set; }
public string? ImageMime { get; set; }
public string? ImageFileName { get; set; }
public byte[]? ThumbnailFile { get; set; }
public string? ThumbnailMime { get; set; }
public string? ThumbnailFileName { get; set; }
```

**فایل‌ها**:
- `/home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice/Services/DiscountProduct/IDiscountProductService.cs`

### 4. Service Layer - DiscountProductService

#### تغییرات CreateAsync و UpdateAsync:
```csharp
// افزودن لاجیک آپلود فایل
if (dto.ImageFile != null && !string.IsNullOrEmpty(dto.ImageMime)) {
    request.ImageFile = new ImageFileModel {
        File = ByteString.CopyFrom(dto.ImageFile),
        Mime = dto.ImageMime,
        FileName = dto.ImageFileName
    };
}
```

**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice/Services/DiscountProduct/DiscountProductService.cs`

### 5. Page Integration - DiscountProductsMainPage

#### تغییرات در OpenCreateDialog و OpenEditDialog:
```csharp
var dto = new CreateDiscountProductDto {
    // ... سایر فیلدها
    InitialCount = 0,  // تغییر: همیشه 0 - موجودی از طریق Inventory مدیریت می‌شود
    ImageFile = formData.ImageFile,
    ImageMime = formData.ImageMime,
    ImageFileName = formData.ImageFileName,
    ThumbnailFile = formData.ThumbnailFile,
    ThumbnailMime = formData.ThumbnailMime,
    ThumbnailFileName = formData.ThumbnailFileName
};
```

**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice/Pages/DiscountShop/DiscountProductsMainPage.razor`

### 6. BFF - Validators

#### اصلاح CreateDiscountProductCommandValidator:
```csharp
// قبل: الزامی بودن ShortInformation و FullInformation
RuleFor(x => x.ShortInformation)
    .NotEmpty().WithMessage("اطلاعات کوتاه محصول الزامی است")

// بعد: اختیاری شدن با چک طول
RuleFor(x => x.ShortInformation)
    .MaximumLength(500)
    .When(x => !string.IsNullOrEmpty(x.ShortInformation));

RuleFor(x => x.FullInformation)
    .MaximumLength(10000)
    .When(x => !string.IsNullOrEmpty(x.FullInformation));
```

**فایل‌ها**:
- `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandValidator.cs`
- `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandValidator.cs`

### 7. BFF - Command Handlers

#### اصلاح CreateDiscountProductCommandHandler:
```csharp
// افزودن null coalescing برای جلوگیری از ArgumentNullException
var createRequest = new CreateDiscountProductRequest {
    ShortInfomation = request.ShortInformation ?? string.Empty,
    FullInformation = request.FullInformation ?? string.Empty,
    // ...
};

// افزودن لاجیک آپلود فایل به File Storage
if (request.ImageFile != null && request.ImageFile.File.Length > 0) {
    var imageFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(...);
    if (imageFileInfo != null)
        createRequest.ImagePath = imageFileInfo.File;
}
```

**فایل‌ها**:
- `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandHandler.cs`
- `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandHandler.cs`

### 8. CMS - Validators

#### اصلاح CreateDiscountProductCommandValidator:
```csharp
// حذف الزامی بودن ImagePath و ThumbnailPath
// قبل:
RuleFor(v => v.ImagePath).NotEmpty()
RuleFor(v => v.ThumbnailPath).NotEmpty()

// بعد: حذف شده - فایل‌ها از طریق BFF آپلود می‌شوند

// ShortInformation و FullInformation اختیاری شدن
RuleFor(v => v.ShortInfomation)
    .MaximumLength(500)
    .When(v => !string.IsNullOrEmpty(v.ShortInfomation));
```

**فایل‌ها**:
- `/home/masoud/Apps/project/FourSat/CMS/src/CMSMicroservice.Application/DiscountShopCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandValidator.cs`
- `/home/masoud/Apps/project/FourSat/CMS/src/CMSMicroservice.Application/DiscountShopCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandValidator.cs`

### 9. Bug Fixes

#### 9.1. Foreign Key Constraint Error
**مشکل**: استفاده از CategoryMultiSelectCombo که Category های عادی را لود می‌کرد، در حالی که DiscountProduct نیاز به DiscountCategory دارد.

**راه‌حل**:
- برگشت به MudSelect ساده
- لود DiscountCategory از IDiscountCategoryService
- Flatten کردن tree برای نمایش در dropdown

#### 9.2. GetDiscountProducts NullReferenceException
**مشکل**: `response.MetaData` یا `response.Models` می‌توانست null باشد.

**راه‌حل**:
```csharp
Products = response.Models?.Select(p => new DiscountProductDto {
    Created = p.Created?.ToDateTime() ?? DateTime.MinValue
}).ToList() ?? new List<DiscountProductDto>()
```

**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsQueryHandler.cs`

#### 9.3. GetDiscountCategories Handler Bugs
**مشکل‌ها**:
1. `response.Categories` بدون null check
2. شرط‌های `string.IsNullOrWhiteSpace` معکوس بودند
3. `ParentCategoryId` چک نادرست
4. `node.Children` بدون null check

**راه‌حل**:
```csharp
Categories = response.Categories?.Select(MapCategory).ToList() 
    ?? new List<DiscountCategoryTreeDto>()

// اصلاح شرط‌ها
Description = !string.IsNullOrWhiteSpace(node.Description) ? node.Description : string.Empty,
ParentCategoryId = node.ParentCategoryId != 0 ? node.ParentCategoryId : null,
ChildCategories = node.Children?.Select(MapCategory).ToList() 
    ?? new List<DiscountCategoryTreeDto>()
```

**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountCategoryCQ/Queries/GetDiscountCategories/GetDiscountCategoriesQueryHandler.cs`

## Build و Deploy

### Build Status
- ✅ BackOffice: 0 errors, 248 warnings (MudBlazor style warnings only)
- ✅ BackOffice.BFF: 0 errors
- ✅ CMS: 0 errors

### دستور Build Proto
```bash
bash /home/masoud/Apps/project/FourSat/BackOffice/build-deps.sh
```

### فایل‌های نیاز به Deploy
1. **BackOffice.BFF** (stage)
   - تغییرات: Validators, Command Handlers, Query Handlers
   
2. **CMS** (stage)
   - تغییرات: Validators

3. **BackOffice** (local)
   - در حال اجرا با `dotnet watch`
   - تغییرات بلافاصله اعمال می‌شود

## نتیجه نهایی

### قابلیت‌های جدید:
✅ آپلود تصویر با drag & drop  
✅ پیش‌نمایش تصویر قبل از ذخیره  
✅ ویرایشگر HTML برای محتوای غنی  
✅ انتخاب چندگانه دسته‌بندی با UI بهتر  
✅ مدیریت صحیح DiscountCategory ها  
✅ حذف الزامی بودن فیلدهای اختیاری  
✅ رفع تمام خطاهای NullReference  

### تفاوت با قبل:
| قبل | بعد |
|-----|-----|
| TextField برای مسیر تصویر | آپلود فایل واقعی |
| TextField ساده | HTML Editor |
| CategoryMultiSelectCombo (اشتباه) | MudSelect با DiscountCategory |
| الزامی بودن توضیحات | اختیاری |
| خطای Foreign Key | حل شده |

## نکات مهم

1. **موجودی (InitialCount)**: همیشه 0 قرار می‌گیرد - موجودی باید از صفحه Inventory مدیریت شود
2. **دسته‌بندی**: حتماً DiscountCategory استفاده شود نه Category معمولی
3. **Validators**: فیلدهای اختیاری با `.When()` چک شوند
4. **Null Safety**: همیشه `??` و `?.` استفاده شود در LINQ queries

## مراجع
- Proto Guide: `/home/masoud/Apps/project/FourSat/PROTO-PACKAGING-GUIDE.md`
- Build Script: `/home/masoud/Apps/project/FourSat/BackOffice/build-deps.sh`
