Implement Inventory Management Service with CRUD operations for warehouses and inventory items, stock operations, and bulk processing capabilities.
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m48s

This commit is contained in:
masoodafar-web
2026-01-02 00:45:37 +03:30
parent f0117eb1d5
commit 1cf501711f
50 changed files with 10699 additions and 101 deletions
+209 -23
View File
@@ -1,38 +1,92 @@
# CMS Microservice - Network & Club Commission System
# CMS Microservice - Network & Club Commission + Inventory Management System
[![Status](https://img.shields.io/badge/Status-Production%20Ready-success)]()
[![Progress](https://img.shields.io/badge/Progress-85%25-blue)]()
[![MVP](https://img.shields.io/badge/MVP-100%25%20Complete-brightgreen)]()
[![Status](https://img.shields.io/badge/Status-Active%20Development-success)]()
[![Progress](https://img.shields.io/badge/Inventory%20System-Phase%202%20Complete-blue)]()
[![Phase](https://img.shields.io/badge/Next-Business%20Services-orange)]()
## 📊 Project Status (2025-12-01)
## 📊 Project Status (January 2026)
**Overall Progress**: 85% Complete (7/10 phases)
**Production Readiness**: 95%
### 🏪 Inventory Management System - NEW!
**Progress**: Phase 2 Complete (50%)
**Architecture**: Clean Architecture + CQRS + Repository Pattern
#### ✅ Completed Phases
1.**Phase 1: Infrastructure & Domain Layer**
- Domain Entities: `InventoryItem`, `StockMovement`, `Warehouse`
- Domain Enums: `StockMovementType`
- EF Core Configurations with proper indexing
- Database migration applied
2.**Phase 2: Repository Pattern & CQRS**
- Repository Interfaces & Implementations
- CQRS Commands (17 commands)
- CQRS Queries (35 queries)
- MediatR Handlers (52 handlers)
#### 🔄 In Progress
3. 🔄 **Phase 3: Business Services Layer**
4.**Phase 4: DTOs & AutoMapper**
5.**Phase 5: API Controllers**
---
### 💼 Commission System - Production Ready
**Progress**: 85% Complete
**MVP Status**: ✅ 100% Complete
### ✅ Completed Phases (7)
1.Domain Layer (Entities, Enums, Value Objects)
2. ✅ Club Membership System
3.Binary Network Tree
4. **Commission Calculation & Background Worker** (MVP)
5.Protobuf gRPC Services
6. ✅ History & Configuration Management
7. ✅ Database Migration & Seed Data
#### ✅ Completed Features
-Binary network tree with automatic placement
- ✅ Club membership (Member/Trial) with commission rates
-Weekly commission calculation (Lesser Leg algorithm)
- ✅ Background worker with Hangfire
-Email + SMS notifications (MailKit + Kavenegar)
- ✅ Health check endpoints (Kubernetes-ready)
### 🟡 Partially Complete (1)
### 🟡 Partially Complete
- Phase 10: Withdrawal & Settlement (40%)
- ✅ Commands & Database
- ❌ Payment Gateway Integration
### ❌ Not Started (1)
### ❌ Not Started
- Phase 9: Club Shop & Product Integration (0%)
### ⏸️ Postponed (1)
- Phase 7: Testing (Unit, Integration, Load tests)
---
## 🚀 Recent Updates (2025-12-01)
## 🚀 Recent Updates (January 2026)
### 🏪 Inventory Management System - NEW! ✅
**Complete CQRS-based inventory management with:**
#### Domain Layer:
-`InventoryItem` - Multi-warehouse product tracking with min/max thresholds
-`StockMovement` - Complete audit trail with 8 movement types
-`Warehouse` - Multi-location support with default warehouse
#### Repository Pattern:
-`IInventoryItemRepository` - 25+ methods for inventory operations
-`IStockMovementRepository` - Movement tracking & analytics
-`IWarehouseRepository` - Warehouse management & statistics
#### CQRS Commands (17 total):
- **Inventory:** Create, Update, Delete, Reserve, Release, Reduce, Increase
- **Movement:** Create, BulkCreate, Delete
- **Warehouse:** Create, Update, Delete, SetDefault, Activate, BulkCreate
#### CQRS Queries (35 total):
- **Inventory:** GetById, Search, LowStock, OutOfStock, CheckAvailability
- **Movement:** GetHistory, GetByOrder, Search, Analytics, DailyVolume, TopMoving
- **Warehouse:** GetById, Search, GetStats, GetLowStock, GetAllStats
#### Business Features:
- ✅ Multi-warehouse inventory management
- ✅ Stock reservation system for orders
- ✅ Automatic movement tracking
- ✅ Low stock & out-of-stock alerts
- ✅ Advanced analytics & reporting
- ✅ Bulk operations support
- ✅ Transaction-safe operations
---
### Email & SMS Notifications - COMPLETED ✅
-**MailKit 4.14.1** for Email (SMTP with HTML templates)
@@ -63,8 +117,38 @@
**Clean Architecture** with 4 layers:
```
CMSMicroservice.Domain/ # Entities, Enums, Interfaces
├── Entities/
│ ├── InventoryItem.cs # NEW: Inventory tracking
│ ├── StockMovement.cs # NEW: Movement audit
│ └── Warehouse.cs # NEW: Multi-warehouse
├── Enums/
│ └── StockMovementType.cs # NEW: Movement types
CMSMicroservice.Application/ # CQRS (Commands, Queries, MediatR)
├── Features/
│ ├── InventoryItems/ # NEW: Inventory CQRS
│ │ ├── Commands/
│ │ ├── Queries/
│ │ └── Handlers/
│ ├── StockMovements/ # NEW: Movement CQRS
│ │ ├── Commands/
│ │ ├── Queries/
│ │ └── Handlers/
│ └── Warehouses/ # NEW: Warehouse CQRS
│ ├── Commands/
│ ├── Queries/
│ └── Handlers/
└── Common/Interfaces/
└── Repositories/ # NEW: Repository interfaces
CMSMicroservice.Infrastructure/ # DbContext, Services, Background Jobs
├── Persistence/
│ ├── Context/
│ ├── Configurations/ # NEW: EF Core configs
│ ├── Repositories/ # NEW: Repository implementations
│ └── Migrations/
└── DependencyInjection.cs # NEW: DI setup
CMSMicroservice.WebApi/ # gRPC Services, Controllers
CMSMicroservice.Protobuf/ # Protocol Buffers definitions
```
@@ -84,6 +168,7 @@ CMSMicroservice.Protobuf/ # Protocol Buffers definitions
## 📖 Documentation
- **[Development Plan](docs/development-plan.md)** - NEW: Inventory system roadmap
- **[Implementation Progress](docs/implementation-progress.md)** - Detailed phase-by-phase progress
- **[Email/SMS Configuration Guide](docs/email-sms-configuration-guide.md)** - Production setup instructions
- **[Balance Calculation Logic](docs/balance-calculation-carryover-logic.md)** - Commission algorithm details
@@ -92,6 +177,74 @@ CMSMicroservice.Protobuf/ # Protocol Buffers definitions
---
## 🏪 Inventory System Usage
### Create Warehouse
```csharp
await mediator.Send(new CreateWarehouseCommand
{
Name = "Main Warehouse",
Code = "WH-001",
IsDefault = true,
IsActive = true
});
```
### Create Inventory Item
```csharp
await mediator.Send(new CreateInventoryItemCommand
{
ProductId = 1,
WarehouseId = 1,
Quantity = 100,
MinQuantity = 10,
MaxQuantity = 1000
});
```
### Reserve Stock for Order
```csharp
await mediator.Send(new ReserveInventoryCommand
{
Id = inventoryId,
Quantity = 5,
OrderId = 12345
});
```
### Check Availability
```csharp
bool available = await mediator.Send(
new CheckInventoryAvailabilityQuery(inventoryId, 10));
```
### Get Low Stock Alerts
```csharp
var lowStock = await mediator.Send(new GetLowStockItemsQuery
{
WarehouseId = 1,
Count = 50
});
```
### Get Movement Analytics
```csharp
var summary = await mediator.Send(new GetMovementSummaryQuery
{
FromDate = DateTime.Now.AddDays(-7),
ToDate = DateTime.Now
});
var topProducts = await mediator.Send(new GetTopMovingProductsQuery
{
FromDate = DateTime.Now.AddDays(-30),
ToDate = DateTime.Now,
Count = 10
});
```
---
## 🚀 Quick Start
### Prerequisites
@@ -197,7 +350,25 @@ curl http://localhost:5133/health/live # Liveness probe (K8s)
## 📊 What's Remaining?
### High Priority
### 🏪 Inventory System (Current Focus)
1. **Phase 3: Business Services** (In Progress)
- `IInventoryManagementService` - High-level operations
- `IStockMovementService` - Movement orchestration
- `IWarehouseService` - Warehouse business logic
- `IInventoryReportingService` - Advanced reporting
2. **Phase 4: DTOs & AutoMapper** (Next)
- Request/Response DTOs
- AutoMapper profiles
- Validation rules
3. **Phase 5: API Controllers** (Planned)
- `InventoryController` - REST API
- `WarehouseController` - Warehouse management
- `StockMovementController` - Movement tracking
- Swagger documentation
### 💼 Commission System
1. **Payment Gateway Integration** (Phase 10 - 1 week)
- Daya or Bank Mellat API integration
- IBAN transfer automation
@@ -230,6 +401,7 @@ curl http://localhost:5133/health/live # Liveness probe (K8s)
## 🎯 MVP Features (100% Complete)
### 💼 Commission System:
✅ Binary network tree with automatic placement
✅ Club membership (Member/Trial) with different commission rates
✅ Weekly commission calculation (Lesser Leg algorithm)
@@ -244,12 +416,26 @@ curl http://localhost:5133/health/live # Liveness probe (K8s)
✅ Structured logging (AlertService for Sentry/Slack)
✅ JWT authentication context (CurrentUserService)
### 🏪 Inventory System (Phase 2 Complete):
✅ Domain entities (InventoryItem, StockMovement, Warehouse)
✅ Multi-warehouse inventory management
✅ Stock reservation system for orders
✅ 8 movement types with complete audit trail
✅ Repository pattern with 25+ methods per repository
✅ CQRS with 17 commands and 35 queries
✅ 52 MediatR handlers with business logic
✅ Low stock and out-of-stock alerts
✅ Advanced analytics (top products, daily volume)
✅ Bulk operations support
✅ Transaction-safe operations with rollback
✅ DI container configuration
---
## 👥 Team
**Development**: FourSat Team
**Last Updated**: 2025-12-01
**Last Updated**: January 2026
---
@@ -61,6 +61,11 @@ public interface IApplicationDbContext
DbSet<State> States { get; }
DbSet<City> Cities { get; }
// ============= Inventory Management =============
DbSet<Warehouse> Warehouses { get; }
DbSet<InventoryItem> InventoryItems { get; }
DbSet<StockMovement> StockMovements { get; }
/// <summary>
/// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures
/// </summary>
@@ -0,0 +1,216 @@
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.Common.Interfaces;
/// <summary>
/// سرویس مدیریت موجودی - لایه بالاتر برای عملیات business
/// این سرویس مسئول همگام‌سازی موجودی بین InventoryItem و Product.RemainingCount است
/// </summary>
public interface IInventoryService
{
#region Initialization
/// <summary>
/// ایجاد رکورد موجودی برای محصول جدید
/// این متد باید در CreateProductCommandHandler و CreateDiscountProductCommandHandler فراخوانی شود
/// </summary>
/// <param name="productId">شناسه محصول (Product.Id یا DiscountProduct.Id)</param>
/// <param name="productType">نوع محصول (RegularProduct یا DiscountProduct)</param>
/// <param name="initialQuantity">موجودی اولیه</param>
/// <param name="warehouseId">شناسه انبار (پیش‌فرض: انبار اصلی)</param>
/// <param name="lowStockThreshold">آستانه هشدار کم‌موجودی</param>
/// <param name="ct">CancellationToken</param>
/// <returns>شناسه InventoryItem ایجاد شده</returns>
Task<long> InitializeInventoryAsync(
long productId,
ProductType productType,
int initialQuantity,
long? warehouseId = null,
int lowStockThreshold = 10,
CancellationToken ct = default);
#endregion
#region Query Operations
/// <summary>
/// دریافت موجودی یک محصول
/// </summary>
Task<InventoryItem?> GetInventoryAsync(
long productId,
ProductType productType,
long? warehouseId = null,
CancellationToken ct = default);
/// <summary>
/// دریافت موجودی قابل فروش (Quantity - ReservedQuantity)
/// </summary>
Task<int> GetAvailableQuantityAsync(
long productId,
ProductType productType,
long? warehouseId = null,
CancellationToken ct = default);
/// <summary>
/// بررسی اینکه آیا موجودی کافی برای فروش وجود دارد
/// </summary>
Task<bool> CheckAvailabilityAsync(
long productId,
ProductType productType,
int requiredQuantity,
long? warehouseId = null,
CancellationToken ct = default);
/// <summary>
/// دریافت لیست محصولات کم‌موجود
/// </summary>
Task<List<InventoryItem>> GetLowStockItemsAsync(
ProductType? productType = null,
long? warehouseId = null,
int count = 50,
CancellationToken ct = default);
/// <summary>
/// دریافت تاریخچه حرکات موجودی
/// </summary>
Task<List<StockMovement>> GetStockMovementsAsync(
long productId,
ProductType productType,
DateTime? fromDate = null,
DateTime? toDate = null,
CancellationToken ct = default);
#endregion
#region Order Flow Operations
/// <summary>
/// رزرو موجودی برای سفارش pending
/// این متد در PlaceOrderCommandHandler فراخوانی می‌شود
/// فقط ReservedQuantity را افزایش می‌دهد، Quantity تغییر نمی‌کند
/// </summary>
/// <param name="productId">شناسه محصول</param>
/// <param name="productType">نوع محصول</param>
/// <param name="quantity">تعداد رزرو</param>
/// <param name="orderId">شناسه سفارش (Order.Id یا DiscountOrder.Id)</param>
/// <param name="ct">CancellationToken</param>
/// <returns>true اگر رزرو موفق بود</returns>
Task<bool> ReserveStockAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
CancellationToken ct = default);
/// <summary>
/// آزادسازی رزرو (لغو سفارش یا timeout)
/// این متد در CancelOrderCommandHandler فراخوانی می‌شود
/// </summary>
Task<bool> ReleaseReservationAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
CancellationToken ct = default);
/// <summary>
/// تایید فروش - کسر واقعی موجودی
/// این متد در CompleteOrderPaymentCommandHandler فراخوانی می‌شود
/// ReservedQuantity کاهش می‌یابد، Quantity کاهش می‌یابد، Product.RemainingCount sync می‌شود
/// </summary>
Task<bool> ConfirmSaleAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
CancellationToken ct = default);
#endregion
#region Stock Management Operations
/// <summary>
/// ورود کالا به انبار (Restock)
/// </summary>
/// <param name="productId">شناسه محصول</param>
/// <param name="productType">نوع محصول</param>
/// <param name="quantity">تعداد ورودی</param>
/// <param name="referenceNumber">شماره مرجع (مثل شماره فاکتور خرید)</param>
/// <param name="note">یادداشت</param>
/// <param name="performedByUserId">شناسه کاربر انجام‌دهنده</param>
/// <param name="ct">CancellationToken</param>
Task<bool> AddStockAsync(
long productId,
ProductType productType,
int quantity,
string? referenceNumber = null,
string? note = null,
long? performedByUserId = null,
CancellationToken ct = default);
/// <summary>
/// تعدیل موجودی (تنظیم به مقدار جدید)
/// </summary>
Task<bool> AdjustStockAsync(
long productId,
ProductType productType,
int newQuantity,
string? note = null,
long? performedByUserId = null,
CancellationToken ct = default);
/// <summary>
/// ثبت برگشت کالا از مشتری
/// </summary>
Task<bool> ProcessReturnAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
string? note = null,
long? performedByUserId = null,
CancellationToken ct = default);
/// <summary>
/// ثبت ضایعات/مفقودی
/// </summary>
Task<bool> RecordLossAsync(
long productId,
ProductType productType,
int quantity,
StockMovementType lossType, // Damaged or Lost
string? note = null,
long? performedByUserId = null,
CancellationToken ct = default);
#endregion
#region Bulk Operations
/// <summary>
/// رزرو موجودی برای چند آیتم (یک سفارش با چند محصول)
/// </summary>
Task<bool> BulkReserveStockAsync(
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
long? orderId = null,
CancellationToken ct = default);
/// <summary>
/// آزادسازی رزرو برای چند آیتم
/// </summary>
Task<bool> BulkReleaseReservationAsync(
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
long? orderId = null,
CancellationToken ct = default);
/// <summary>
/// تایید فروش برای چند آیتم
/// </summary>
Task<bool> BulkConfirmSaleAsync(
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
long? orderId = null,
CancellationToken ct = default);
#endregion
}
@@ -0,0 +1,152 @@
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.Common.Interfaces.Repositories;
/// <summary>
/// Repository interface برای مدیریت موجودی محصولات
/// </summary>
public interface IInventoryItemRepository
{
#region Read Operations
/// <summary>
/// دریافت آیتم موجودی بر اساس شناسه
/// </summary>
Task<InventoryItem?> GetByIdAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// دریافت آیتم موجودی بر اساس محصول معمولی
/// </summary>
Task<InventoryItem?> GetByProductIdAsync(long productId, long warehouseId = 1, CancellationToken cancellationToken = default);
/// <summary>
/// دریافت آیتم موجودی بر اساس محصول تخفیفی
/// </summary>
Task<InventoryItem?> GetByDiscountProductIdAsync(long discountProductId, long warehouseId = 1, CancellationToken cancellationToken = default);
/// <summary>
/// دریافت تمام آیتم‌های موجودی یک انبار
/// </summary>
Task<List<InventoryItem>> GetByWarehouseIdAsync(long warehouseId, CancellationToken cancellationToken = default);
/// <summary>
/// دریافت محصولات کم‌موجود
/// </summary>
Task<List<InventoryItem>> GetLowStockItemsAsync(ProductType? productType = null, long warehouseId = 1, CancellationToken cancellationToken = default);
/// <summary>
/// دریافت محصولات با موجودی صفر
/// </summary>
Task<List<InventoryItem>> GetOutOfStockItemsAsync(ProductType? productType = null, long warehouseId = 1, CancellationToken cancellationToken = default);
/// <summary>
/// جستجوی آیتم‌های موجودی با فیلتر
/// </summary>
Task<List<InventoryItem>> SearchAsync(
string? searchTerm = null,
ProductType? productType = null,
long? warehouseId = null,
int? minQuantity = null,
int? maxQuantity = null,
int skip = 0,
int take = 50,
CancellationToken cancellationToken = default);
/// <summary>
/// شمارش کل آیتم‌های موجودی با فیلتر
/// </summary>
Task<int> CountAsync(
string? searchTerm = null,
ProductType? productType = null,
long? warehouseId = null,
int? minQuantity = null,
int? maxQuantity = null,
CancellationToken cancellationToken = default);
#endregion
#region Write Operations
/// <summary>
/// افزودن آیتم موجودی جدید
/// </summary>
Task<InventoryItem> AddAsync(InventoryItem inventoryItem, CancellationToken cancellationToken = default);
/// <summary>
/// بروزرسانی آیتم موجودی
/// </summary>
Task UpdateAsync(InventoryItem inventoryItem, CancellationToken cancellationToken = default);
/// <summary>
/// حذف آیتم موجودی
/// </summary>
Task DeleteAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// بروزرسانی موجودی (با ثبت حرکت)
/// </summary>
Task<bool> UpdateQuantityAsync(
long inventoryItemId,
int quantityChange,
StockMovementType movementType,
string? note = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default);
/// <summary>
/// رزرو موجودی
/// </summary>
Task<bool> ReserveQuantityAsync(
long inventoryItemId,
int quantity,
string? note = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default);
/// <summary>
/// آزاد کردن موجودی رزرو شده
/// </summary>
Task<bool> ReleaseReservedQuantityAsync(
long inventoryItemId,
int quantity,
string? note = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default);
#endregion
#region Bulk Operations
/// <summary>
/// بروزرسانی انبوه موجودی چندین محصول
/// </summary>
Task<bool> BulkUpdateQuantityAsync(
List<(long InventoryItemId, int QuantityChange, string? Note)> updates,
StockMovementType movementType,
string? referenceNumber = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default);
/// <summary>
/// رزرو انبوه موجودی چندین محصول
/// </summary>
Task<bool> BulkReserveQuantityAsync(
List<(long InventoryItemId, int Quantity, string? Note)> reservations,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default);
#endregion
}
@@ -0,0 +1,146 @@
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.Common.Interfaces.Repositories;
/// <summary>
/// Repository interface برای مدیریت حرکات موجودی
/// </summary>
public interface IStockMovementRepository
{
#region Read Operations
/// <summary>
/// دریافت حرکت موجودی بر اساس شناسه
/// </summary>
Task<StockMovement?> GetByIdAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// دریافت تاریخچه حرکات یک آیتم موجودی
/// </summary>
Task<List<StockMovement>> GetByInventoryItemIdAsync(
long inventoryItemId,
StockMovementType? movementType = null,
DateTime? fromDate = null,
DateTime? toDate = null,
int skip = 0,
int take = 100,
CancellationToken cancellationToken = default);
/// <summary>
/// دریافت حرکات مربوط به یک سفارش
/// </summary>
Task<List<StockMovement>> GetByOrderIdAsync(long orderId, CancellationToken cancellationToken = default);
/// <summary>
/// دریافت حرکات مربوط به یک سفارش تخفیفی
/// </summary>
Task<List<StockMovement>> GetByDiscountOrderIdAsync(long discountOrderId, CancellationToken cancellationToken = default);
/// <summary>
/// دریافت حرکات بر اساس شماره مرجع
/// </summary>
Task<List<StockMovement>> GetByReferenceNumberAsync(string referenceNumber, CancellationToken cancellationToken = default);
/// <summary>
/// دریافت حرکات بر اساس نوع حرکت
/// </summary>
Task<List<StockMovement>> GetByMovementTypeAsync(
StockMovementType movementType,
DateTime? fromDate = null,
DateTime? toDate = null,
int skip = 0,
int take = 100,
CancellationToken cancellationToken = default);
/// <summary>
/// دریافت آخرین حرکات موجودی
/// </summary>
Task<List<StockMovement>> GetRecentMovementsAsync(
int count = 50,
StockMovementType? movementType = null,
CancellationToken cancellationToken = default);
/// <summary>
/// جستجوی حرکات موجودی با فیلتر پیشرفته
/// </summary>
Task<List<StockMovement>> SearchAsync(
long? inventoryItemId = null,
StockMovementType? movementType = null,
DateTime? fromDate = null,
DateTime? toDate = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
int skip = 0,
int take = 100,
CancellationToken cancellationToken = default);
/// <summary>
/// شمارش حرکات موجودی با فیلتر
/// </summary>
Task<int> CountAsync(
long? inventoryItemId = null,
StockMovementType? movementType = null,
DateTime? fromDate = null,
DateTime? toDate = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default);
#endregion
#region Write Operations
/// <summary>
/// افزودن حرکت موجودی جدید
/// </summary>
Task<StockMovement> AddAsync(StockMovement stockMovement, CancellationToken cancellationToken = default);
/// <summary>
/// حذف حرکت موجودی (نرم‌افزاری)
/// </summary>
Task DeleteAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// افزودن انبوه حرکات موجودی
/// </summary>
Task<List<StockMovement>> BulkAddAsync(List<StockMovement> stockMovements, CancellationToken cancellationToken = default);
#endregion
#region Analytics & Reports
/// <summary>
/// گزارش خلاصه حرکات در بازه زمانی
/// </summary>
Task<Dictionary<StockMovementType, int>> GetMovementSummaryAsync(
DateTime fromDate,
DateTime toDate,
long? inventoryItemId = null,
CancellationToken cancellationToken = default);
/// <summary>
/// گزارش حجم ورود و خروج روزانه
/// </summary>
Task<List<(DateTime Date, int InboundQuantity, int OutboundQuantity)>> GetDailyMovementVolumeAsync(
DateTime fromDate,
DateTime toDate,
long? inventoryItemId = null,
CancellationToken cancellationToken = default);
/// <summary>
/// گزارش بیشترین حرکات محصولات
/// </summary>
Task<List<(long InventoryItemId, string ProductName, int MovementCount, int TotalQuantityChange)>> GetTopMovingProductsAsync(
DateTime fromDate,
DateTime toDate,
int count = 10,
StockMovementType? movementType = null,
CancellationToken cancellationToken = default);
#endregion
}
@@ -0,0 +1,111 @@
using CMSMicroservice.Domain.Entities;
namespace CMSMicroservice.Application.Common.Interfaces.Repositories;
/// <summary>
/// Repository interface برای مدیریت انبارها
/// </summary>
public interface IWarehouseRepository
{
#region Read Operations
/// <summary>
/// دریافت انبار بر اساس شناسه
/// </summary>
Task<Warehouse?> GetByIdAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// دریافت انبار بر اساس کد
/// </summary>
Task<Warehouse?> GetByCodeAsync(string code, CancellationToken cancellationToken = default);
/// <summary>
/// دریافت انبار پیش‌فرض
/// </summary>
Task<Warehouse?> GetDefaultWarehouseAsync(CancellationToken cancellationToken = default);
/// <summary>
/// دریافت تمام انبارهای فعال
/// </summary>
Task<List<Warehouse>> GetActiveWarehousesAsync(CancellationToken cancellationToken = default);
/// <summary>
/// دریافت تمام انبارها
/// </summary>
Task<List<Warehouse>> GetAllAsync(bool includeInactive = false, CancellationToken cancellationToken = default);
/// <summary>
/// جستجوی انبارها
/// </summary>
Task<List<Warehouse>> SearchAsync(
string? searchTerm = null,
bool? isActive = null,
int skip = 0,
int take = 50,
CancellationToken cancellationToken = default);
/// <summary>
/// شمارش انبارها
/// </summary>
Task<int> CountAsync(
string? searchTerm = null,
bool? isActive = null,
CancellationToken cancellationToken = default);
/// <summary>
/// بررسی وجود انبار با کد مشخص
/// </summary>
Task<bool> ExistsByCodeAsync(string code, long? excludeId = null, CancellationToken cancellationToken = default);
#endregion
#region Write Operations
/// <summary>
/// افزودن انبار جدید
/// </summary>
Task<Warehouse> AddAsync(Warehouse warehouse, CancellationToken cancellationToken = default);
/// <summary>
/// بروزرسانی انبار
/// </summary>
Task UpdateAsync(Warehouse warehouse, CancellationToken cancellationToken = default);
/// <summary>
/// حذف انبار (نرم‌افزاری)
/// </summary>
Task DeleteAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// فعال/غیرفعال کردن انبار
/// </summary>
Task SetActiveStatusAsync(long id, bool isActive, CancellationToken cancellationToken = default);
/// <summary>
/// تنظیم انبار پیش‌فرض
/// </summary>
Task SetAsDefaultAsync(long id, CancellationToken cancellationToken = default);
#endregion
#region Analytics
/// <summary>
/// گزارش آمار کلی انبار
/// </summary>
Task<(int TotalProducts, int LowStockProducts, int OutOfStockProducts, decimal TotalValue)> GetWarehouseStatisticsAsync(
long warehouseId,
CancellationToken cancellationToken = default);
/// <summary>
/// گزارش محصولات پرفروش انبار
/// </summary>
Task<List<(long ProductId, string ProductName, int TotalSold, int CurrentStock)>> GetTopSellingProductsAsync(
long warehouseId,
DateTime fromDate,
DateTime toDate,
int count = 10,
CancellationToken cancellationToken = default);
#endregion
}
@@ -8,10 +8,14 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayme
public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderPaymentCommand, CompleteOrderPaymentResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IInventoryService _inventoryService;
public CompleteOrderPaymentCommandHandler(IApplicationDbContext context)
public CompleteOrderPaymentCommandHandler(
IApplicationDbContext context,
IInventoryService inventoryService)
{
_context = context;
_inventoryService = inventoryService;
}
public async Task<CompleteOrderPaymentResponseDto> Handle(CompleteOrderPaymentCommand request, CancellationToken cancellationToken)
@@ -63,12 +67,18 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
userWallet.DiscountBalance -= order.DiscountBalanceUsed;
}
// Update product stock and sale count
// تایید فروش و کسر موجودی از طریق InventoryService
foreach (var orderDetail in order.OrderDetails)
{
var product = orderDetail.Product;
product.RemainingCount -= orderDetail.Count;
product.SaleCount += orderDetail.Count;
await _inventoryService.ConfirmSaleAsync(
orderDetail.ProductId,
ProductType.DiscountProduct,
orderDetail.Count,
order.Id,
cancellationToken);
// افزایش تعداد فروش
orderDetail.Product.SaleCount += orderDetail.Count;
}
await _context.SaveChangesAsync(cancellationToken);
@@ -82,7 +92,17 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
}
else
{
// Payment failed
// Payment failed - آزادسازی رزرو
foreach (var orderDetail in order.OrderDetails)
{
await _inventoryService.ReleaseReservationAsync(
orderDetail.ProductId,
ProductType.DiscountProduct,
orderDetail.Count,
order.Id,
cancellationToken);
}
transaction.PaymentStatus = PaymentStatus.Reject;
order.PaymentStatus = PaymentStatus.Reject;
@@ -1,5 +1,6 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.DiscountShop;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
@@ -8,10 +9,14 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.CreateDiscountProd
public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscountProductCommand, long>
{
private readonly IApplicationDbContext _context;
private readonly IInventoryService _inventoryService;
public CreateDiscountProductCommandHandler(IApplicationDbContext context)
public CreateDiscountProductCommandHandler(
IApplicationDbContext context,
IInventoryService inventoryService)
{
_context = context;
_inventoryService = inventoryService;
}
public async Task<long> Handle(CreateDiscountProductCommand request, CancellationToken cancellationToken)
@@ -35,6 +40,13 @@ public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscoun
_context.DiscountProducts.Add(product);
await _context.SaveChangesAsync(cancellationToken);
// ایجاد رکورد موجودی در سیستم انبارداری
await _inventoryService.InitializeInventoryAsync(
product.Id,
ProductType.DiscountProduct,
request.RemainingCount,
ct: cancellationToken);
// Add product categories
if (request.CategoryIds.Any())
{
@@ -11,10 +11,14 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.PlaceOrder;
public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, PlaceOrderResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IInventoryService _inventoryService;
public PlaceOrderCommandHandler(IApplicationDbContext context)
public PlaceOrderCommandHandler(
IApplicationDbContext context,
IInventoryService inventoryService)
{
_context = context;
_inventoryService = inventoryService;
}
public async Task<PlaceOrderResponseDto> Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
@@ -152,6 +156,17 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
_context.DiscountOrderDetails.AddRange(orderDetails);
// رزرو موجودی برای سفارش pending
foreach (var cartItem in cartItems)
{
await _inventoryService.ReserveStockAsync(
cartItem.ProductId,
ProductType.DiscountProduct,
cartItem.Count,
order.Id,
cancellationToken);
}
// Clear cart
_context.DiscountShoppingCarts.RemoveRange(cartItems);
@@ -0,0 +1,102 @@
using MediatR;
namespace CMSMicroservice.Application.Features.InventoryItems.Commands;
/// <summary>
/// Command برای ایجاد آیتم موجودی جدید
/// </summary>
public record CreateInventoryItemCommand : IRequest<long>
{
public long? ProductId { get; init; }
public long? DiscountProductId { get; init; }
public long WarehouseId { get; init; }
public int Quantity { get; init; }
public int MinQuantity { get; init; }
public int MaxQuantity { get; init; }
public bool IsActive { get; init; } = true;
}
/// <summary>
/// Command برای آپدیت آیتم موجودی
/// </summary>
public record UpdateInventoryItemCommand : IRequest<bool>
{
public long Id { get; init; }
public int? Quantity { get; init; }
public int? MinQuantity { get; init; }
public int? MaxQuantity { get; init; }
public long? WarehouseId { get; init; }
public bool? IsActive { get; init; }
}
/// <summary>
/// Command برای آپدیت کردن موجودی یک آیتم
/// </summary>
public record UpdateInventoryQuantityCommand : IRequest<bool>
{
public long Id { get; init; }
public int NewQuantity { get; init; }
public string? ReferenceNumber { get; init; }
public long? PerformedByUserId { get; init; }
public string? Note { get; init; }
}
/// <summary>
/// Command برای رزرو کردن موجودی
/// </summary>
public record ReserveInventoryCommand : IRequest<bool>
{
public long Id { get; init; }
public int Quantity { get; init; }
public long? OrderId { get; init; }
public long? DiscountOrderId { get; init; }
public string? ReferenceNumber { get; init; }
public long? PerformedByUserId { get; init; }
}
/// <summary>
/// Command برای آزاد کردن موجودی رزرو شده
/// </summary>
public record ReleaseReservedInventoryCommand : IRequest<bool>
{
public long Id { get; init; }
public int Quantity { get; init; }
public long? OrderId { get; init; }
public long? DiscountOrderId { get; init; }
public string? ReferenceNumber { get; init; }
public long? PerformedByUserId { get; init; }
}
/// <summary>
/// Command برای کم کردن موجودی (فروش)
/// </summary>
public record ReduceInventoryCommand : IRequest<bool>
{
public long Id { get; init; }
public int Quantity { get; init; }
public long? OrderId { get; init; }
public long? DiscountOrderId { get; init; }
public string? ReferenceNumber { get; init; }
public long? PerformedByUserId { get; init; }
public bool FromReserved { get; init; } = true; // آیا از موجودی رزرو شده کم شود؟
}
/// <summary>
/// Command برای اضافه کردن موجودی (خرید)
/// </summary>
public record IncreaseInventoryCommand : IRequest<bool>
{
public long Id { get; init; }
public int Quantity { get; init; }
public string? ReferenceNumber { get; init; }
public long? PerformedByUserId { get; init; }
public string? Note { get; init; }
}
/// <summary>
/// Command برای حذف آیتم موجودی
/// </summary>
public record DeleteInventoryItemCommand : IRequest<bool>
{
public long Id { get; init; }
}
@@ -0,0 +1,289 @@
using MediatR;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Application.Features.InventoryItems.Commands;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.Features.InventoryItems.Handlers;
/// <summary>
/// Handler برای ایجاد آیتم موجودی جدید
/// </summary>
public class CreateInventoryItemCommandHandler : IRequestHandler<CreateInventoryItemCommand, long>
{
private readonly IInventoryItemRepository _repository;
private readonly IStockMovementRepository _stockMovementRepository;
public CreateInventoryItemCommandHandler(
IInventoryItemRepository repository,
IStockMovementRepository stockMovementRepository)
{
_repository = repository;
_stockMovementRepository = stockMovementRepository;
}
public async Task<long> Handle(CreateInventoryItemCommand request, CancellationToken cancellationToken)
{
// بررسی اینکه حداقل یکی از Product یا DiscountProduct تعریف شده باشد
if (request.ProductId == null && request.DiscountProductId == null)
{
throw new ArgumentException("Either ProductId or DiscountProductId must be provided");
}
// بررسی اینکه هر دو ProductId و DiscountProductId تعریف نشده باشند
if (request.ProductId != null && request.DiscountProductId != null)
{
throw new ArgumentException("Only one of ProductId or DiscountProductId can be provided");
}
// بررسی وجود آیتم موجودی قبلی برای همین محصول در همین انبار
InventoryItem? existingItem = null;
if (request.ProductId.HasValue)
{
existingItem = await _repository.GetByProductIdAsync(request.ProductId.Value, request.WarehouseId, cancellationToken);
}
else if (request.DiscountProductId.HasValue)
{
existingItem = await _repository.GetByDiscountProductIdAsync(request.DiscountProductId.Value, request.WarehouseId, cancellationToken);
}
if (existingItem != null)
{
throw new InvalidOperationException("Inventory item already exists for this product in this warehouse");
}
var productType = request.ProductId.HasValue ? ProductType.RegularProduct : ProductType.DiscountProduct;
var inventoryItem = new InventoryItem
{
ProductId = request.ProductId,
DiscountProductId = request.DiscountProductId,
ProductType = productType,
WarehouseId = request.WarehouseId,
Quantity = request.Quantity,
LowStockThreshold = request.MinQuantity,
MaxStockLevel = request.MaxQuantity,
ReservedQuantity = 0
};
var createdItem = await _repository.AddAsync(inventoryItem, cancellationToken);
// ثبت حرکت موجودی اولیه اگر موجودی اولیه بیشتر از صفر باشد
if (request.Quantity > 0)
{
var stockMovement = new StockMovement
{
InventoryItemId = createdItem.Id,
MovementType = StockMovementType.InitialStock,
Quantity = request.Quantity,
Note = "Initial stock creation",
ReferenceNumber = $"INIT-{createdItem.Id}"
};
await _stockMovementRepository.AddAsync(stockMovement, cancellationToken);
}
return createdItem.Id;
}
}
/// <summary>
/// Handler برای آپدیت آیتم موجودی
/// </summary>
public class UpdateInventoryItemCommandHandler : IRequestHandler<UpdateInventoryItemCommand, bool>
{
private readonly IInventoryItemRepository _repository;
public UpdateInventoryItemCommandHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(UpdateInventoryItemCommand request, CancellationToken cancellationToken)
{
var inventoryItem = await _repository.GetByIdAsync(request.Id, cancellationToken);
if (inventoryItem == null)
{
return false;
}
if (request.WarehouseId.HasValue && request.WarehouseId.Value != inventoryItem.WarehouseId)
{
inventoryItem.WarehouseId = request.WarehouseId.Value;
}
if (request.Quantity.HasValue)
{
inventoryItem.Quantity = request.Quantity.Value;
}
if (request.MinQuantity.HasValue)
{
inventoryItem.LowStockThreshold = request.MinQuantity.Value;
}
if (request.MaxQuantity.HasValue)
{
inventoryItem.MaxStockLevel = request.MaxQuantity.Value;
}
await _repository.UpdateAsync(inventoryItem, cancellationToken);
return true;
}
}
/// <summary>
/// Handler برای آپدیت موجودی
/// </summary>
public class UpdateInventoryQuantityCommandHandler : IRequestHandler<UpdateInventoryQuantityCommand, bool>
{
private readonly IInventoryItemRepository _repository;
public UpdateInventoryQuantityCommandHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(UpdateInventoryQuantityCommand request, CancellationToken cancellationToken)
{
var inventoryItem = await _repository.GetByIdAsync(request.Id, cancellationToken);
if (inventoryItem == null)
{
return false;
}
var quantityChange = request.NewQuantity - inventoryItem.Quantity;
var movementType = quantityChange >= 0 ? StockMovementType.AdjustmentPlus : StockMovementType.AdjustmentMinus;
var result = await _repository.UpdateQuantityAsync(
request.Id,
quantityChange,
movementType,
note: request.Note,
referenceNumber: request.ReferenceNumber,
performedByUserId: request.PerformedByUserId,
cancellationToken: cancellationToken);
return result;
}
}
/// <summary>
/// Handler برای رزرو موجودی
/// </summary>
public class ReserveInventoryCommandHandler : IRequestHandler<ReserveInventoryCommand, bool>
{
private readonly IInventoryItemRepository _repository;
public ReserveInventoryCommandHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(ReserveInventoryCommand request, CancellationToken cancellationToken)
{
return await _repository.ReserveQuantityAsync(
request.Id,
request.Quantity,
referenceNumber: request.ReferenceNumber,
orderId: request.OrderId,
discountOrderId: request.DiscountOrderId,
performedByUserId: request.PerformedByUserId,
cancellationToken: cancellationToken);
}
}
/// <summary>
/// Handler برای آزاد کردن موجودی رزرو شده
/// </summary>
public class ReleaseReservedInventoryCommandHandler : IRequestHandler<ReleaseReservedInventoryCommand, bool>
{
private readonly IInventoryItemRepository _repository;
public ReleaseReservedInventoryCommandHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(ReleaseReservedInventoryCommand request, CancellationToken cancellationToken)
{
return await _repository.ReleaseReservedQuantityAsync(
request.Id,
request.Quantity,
referenceNumber: request.ReferenceNumber,
orderId: request.OrderId,
discountOrderId: request.DiscountOrderId,
performedByUserId: request.PerformedByUserId,
cancellationToken: cancellationToken);
}
}
/// <summary>
/// Handler برای کم کردن موجودی
/// </summary>
public class ReduceInventoryCommandHandler : IRequestHandler<ReduceInventoryCommand, bool>
{
private readonly IInventoryItemRepository _repository;
public ReduceInventoryCommandHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(ReduceInventoryCommand request, CancellationToken cancellationToken)
{
return await _repository.UpdateQuantityAsync(
request.Id,
-request.Quantity,
StockMovementType.Sale,
referenceNumber: request.ReferenceNumber,
orderId: request.OrderId,
discountOrderId: request.DiscountOrderId,
performedByUserId: request.PerformedByUserId,
cancellationToken: cancellationToken);
}
}
/// <summary>
/// Handler برای اضافه کردن موجودی
/// </summary>
public class IncreaseInventoryCommandHandler : IRequestHandler<IncreaseInventoryCommand, bool>
{
private readonly IInventoryItemRepository _repository;
public IncreaseInventoryCommandHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(IncreaseInventoryCommand request, CancellationToken cancellationToken)
{
return await _repository.UpdateQuantityAsync(
request.Id,
request.Quantity,
StockMovementType.Restock,
note: request.Note,
referenceNumber: request.ReferenceNumber,
performedByUserId: request.PerformedByUserId,
cancellationToken: cancellationToken);
}
}
/// <summary>
/// Handler برای حذف آیتم موجودی
/// </summary>
public class DeleteInventoryItemCommandHandler : IRequestHandler<DeleteInventoryItemCommand, bool>
{
private readonly IInventoryItemRepository _repository;
public DeleteInventoryItemCommandHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(DeleteInventoryItemCommand request, CancellationToken cancellationToken)
{
await _repository.DeleteAsync(request.Id, cancellationToken);
return true;
}
}
@@ -0,0 +1,197 @@
using MediatR;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Application.Features.InventoryItems.Queries;
using CMSMicroservice.Domain.Entities;
namespace CMSMicroservice.Application.Features.InventoryItems.Handlers;
/// <summary>
/// Handler برای دریافت آیتم موجودی به ID
/// </summary>
public class GetInventoryItemByIdQueryHandler : IRequestHandler<GetInventoryItemByIdQuery, InventoryItem?>
{
private readonly IInventoryItemRepository _repository;
public GetInventoryItemByIdQueryHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<InventoryItem?> Handle(GetInventoryItemByIdQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByIdAsync(request.Id, cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت آیتم موجودی به Product ID
/// </summary>
public class GetInventoryItemByProductIdQueryHandler : IRequestHandler<GetInventoryItemByProductIdQuery, InventoryItem?>
{
private readonly IInventoryItemRepository _repository;
public GetInventoryItemByProductIdQueryHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<InventoryItem?> Handle(GetInventoryItemByProductIdQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByProductIdAsync(request.ProductId, request.WarehouseId ?? 1, cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت آیتم موجودی به DiscountProduct ID
/// </summary>
public class GetInventoryItemByDiscountProductIdQueryHandler : IRequestHandler<GetInventoryItemByDiscountProductIdQuery, InventoryItem?>
{
private readonly IInventoryItemRepository _repository;
public GetInventoryItemByDiscountProductIdQueryHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<InventoryItem?> Handle(GetInventoryItemByDiscountProductIdQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByDiscountProductIdAsync(request.DiscountProductId, request.WarehouseId ?? 1, cancellationToken);
}
}
/// <summary>
/// Handler برای جستجوی آیتم های موجودی
/// </summary>
public class SearchInventoryItemsQueryHandler : IRequestHandler<SearchInventoryItemsQuery, List<InventoryItem>>
{
private readonly IInventoryItemRepository _repository;
public SearchInventoryItemsQueryHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<List<InventoryItem>> Handle(SearchInventoryItemsQuery request, CancellationToken cancellationToken)
{
return await _repository.SearchAsync(
searchTerm: request.ProductName,
warehouseId: request.WarehouseId,
skip: request.Skip,
take: request.Take,
cancellationToken: cancellationToken);
}
}
/// <summary>
/// Handler برای شمارش آیتم های موجودی
/// </summary>
public class GetInventoryItemsCountQueryHandler : IRequestHandler<GetInventoryItemsCountQuery, int>
{
private readonly IInventoryItemRepository _repository;
public GetInventoryItemsCountQueryHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<int> Handle(GetInventoryItemsCountQuery request, CancellationToken cancellationToken)
{
return await _repository.CountAsync(
searchTerm: request.ProductName,
warehouseId: request.WarehouseId,
cancellationToken: cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت آیتم های کم موجود
/// </summary>
public class GetLowStockItemsQueryHandler : IRequestHandler<GetLowStockItemsQuery, List<InventoryItem>>
{
private readonly IInventoryItemRepository _repository;
public GetLowStockItemsQueryHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<List<InventoryItem>> Handle(GetLowStockItemsQuery request, CancellationToken cancellationToken)
{
return await _repository.GetLowStockItemsAsync(warehouseId: request.WarehouseId ?? 1, cancellationToken: cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت آیتم های ناموجود
/// </summary>
public class GetOutOfStockItemsQueryHandler : IRequestHandler<GetOutOfStockItemsQuery, List<InventoryItem>>
{
private readonly IInventoryItemRepository _repository;
public GetOutOfStockItemsQueryHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<List<InventoryItem>> Handle(GetOutOfStockItemsQuery request, CancellationToken cancellationToken)
{
return await _repository.GetOutOfStockItemsAsync(warehouseId: request.WarehouseId ?? 1, cancellationToken: cancellationToken);
}
}
/// <summary>
/// Handler برای چک کردن دسترسی موجودی
/// </summary>
public class CheckInventoryAvailabilityQueryHandler : IRequestHandler<CheckInventoryAvailabilityQuery, bool>
{
private readonly IInventoryItemRepository _repository;
public CheckInventoryAvailabilityQueryHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(CheckInventoryAvailabilityQuery request, CancellationToken cancellationToken)
{
var item = await _repository.GetByIdAsync(request.InventoryItemId, cancellationToken);
if (item == null) return false;
return item.AvailableQuantity >= request.RequiredQuantity;
}
}
/// <summary>
/// Handler برای دریافت موجودی قابل دسترس
/// </summary>
public class GetAvailableQuantityQueryHandler : IRequestHandler<GetAvailableQuantityQuery, int>
{
private readonly IInventoryItemRepository _repository;
public GetAvailableQuantityQueryHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<int> Handle(GetAvailableQuantityQuery request, CancellationToken cancellationToken)
{
var item = await _repository.GetByIdAsync(request.InventoryItemId, cancellationToken);
return item?.AvailableQuantity ?? 0;
}
}
/// <summary>
/// Handler برای دریافت آیتم های موجودی در انبار
/// </summary>
public class GetWarehouseInventoryItemsQueryHandler : IRequestHandler<GetWarehouseInventoryItemsQuery, List<InventoryItem>>
{
private readonly IInventoryItemRepository _repository;
public GetWarehouseInventoryItemsQueryHandler(IInventoryItemRepository repository)
{
_repository = repository;
}
public async Task<List<InventoryItem>> Handle(GetWarehouseInventoryItemsQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByWarehouseIdAsync(request.WarehouseId, cancellationToken);
}
}
@@ -0,0 +1,93 @@
using MediatR;
using CMSMicroservice.Domain.Entities;
namespace CMSMicroservice.Application.Features.InventoryItems.Queries;
/// <summary>
/// Query برای دریافت آیتم موجودی به ID
/// </summary>
public record GetInventoryItemByIdQuery(long Id) : IRequest<InventoryItem?>;
/// <summary>
/// Query برای دریافت آیتم موجودی به Product ID
/// </summary>
public record GetInventoryItemByProductIdQuery(long ProductId, long? WarehouseId = null) : IRequest<InventoryItem?>;
/// <summary>
/// Query برای دریافت آیتم موجودی به DiscountProduct ID
/// </summary>
public record GetInventoryItemByDiscountProductIdQuery(long DiscountProductId, long? WarehouseId = null) : IRequest<InventoryItem?>;
/// <summary>
/// Query برای جستجوی آیتم های موجودی
/// </summary>
public record SearchInventoryItemsQuery : IRequest<List<InventoryItem>>
{
public long? WarehouseId { get; init; }
public long? ProductId { get; init; }
public long? DiscountProductId { get; init; }
public string? ProductName { get; init; }
public bool? IsActive { get; init; }
public bool? IsLowStock { get; init; }
public bool? IsOutOfStock { get; init; }
public int Skip { get; init; } = 0;
public int Take { get; init; } = 100;
}
/// <summary>
/// Query برای دریافت تعداد آیتم های موجودی
/// </summary>
public record GetInventoryItemsCountQuery : IRequest<int>
{
public long? WarehouseId { get; init; }
public long? ProductId { get; init; }
public long? DiscountProductId { get; init; }
public string? ProductName { get; init; }
public bool? IsActive { get; init; }
public bool? IsLowStock { get; init; }
public bool? IsOutOfStock { get; init; }
}
/// <summary>
/// Query برای دریافت آیتم های کم موجود
/// </summary>
public record GetLowStockItemsQuery : IRequest<List<InventoryItem>>
{
public long? WarehouseId { get; init; }
public int Count { get; init; } = 50;
}
/// <summary>
/// Query برای دریافت آیتم های ناموجود
/// </summary>
public record GetOutOfStockItemsQuery : IRequest<List<InventoryItem>>
{
public long? WarehouseId { get; init; }
public int Count { get; init; } = 50;
}
/// <summary>
/// Query برای چک کردن دسترسی موجودی
/// </summary>
public record CheckInventoryAvailabilityQuery : IRequest<bool>
{
public long InventoryItemId { get; init; }
public int RequiredQuantity { get; init; }
}
/// <summary>
/// Query برای دریافت موجودی قابل دسترس
/// </summary>
public record GetAvailableQuantityQuery(long InventoryItemId) : IRequest<int>;
/// <summary>
/// Query برای دریافت آیتم های موجودی در انبار
/// </summary>
public record GetWarehouseInventoryItemsQuery : IRequest<List<InventoryItem>>
{
public long WarehouseId { get; init; }
public bool? IsActive { get; init; }
public bool? IsLowStock { get; init; }
public int Skip { get; init; } = 0;
public int Take { get; init; } = 100;
}
@@ -0,0 +1,44 @@
using MediatR;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.Features.StockMovements.Commands;
/// <summary>
/// Command برای ثبت حرکت موجودی
/// </summary>
public record CreateStockMovementCommand : IRequest<long>
{
public long InventoryItemId { get; init; }
public StockMovementType MovementType { get; init; }
public int Quantity { get; init; }
public long? OrderId { get; init; }
public long? DiscountOrderId { get; init; }
public string? ReferenceNumber { get; init; }
public string? Note { get; init; }
public long? PerformedByUserId { get; init; }
}
/// <summary>
/// Command برای ثبت چندین حرکت موجودی به صورت bulk
/// </summary>
public record BulkCreateStockMovementCommand : IRequest<List<long>>
{
public List<StockMovementItem> Movements { get; init; } = new();
public record StockMovementItem
{
public long InventoryItemId { get; init; }
public StockMovementType MovementType { get; init; }
public int Quantity { get; init; }
public long? OrderId { get; init; }
public long? DiscountOrderId { get; init; }
public string? ReferenceNumber { get; init; }
public string? Note { get; init; }
public long? PerformedByUserId { get; init; }
}
}
/// <summary>
/// Command برای حذف حرکت موجودی
/// </summary>
public record DeleteStockMovementCommand(long Id) : IRequest<bool>;
@@ -0,0 +1,119 @@
using MediatR;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Application.Features.StockMovements.Commands;
using CMSMicroservice.Domain.Entities;
namespace CMSMicroservice.Application.Features.StockMovements.Handlers;
/// <summary>
/// Handler برای ثبت حرکت موجودی
/// </summary>
public class CreateStockMovementCommandHandler : IRequestHandler<CreateStockMovementCommand, long>
{
private readonly IStockMovementRepository _repository;
private readonly IInventoryItemRepository _inventoryRepository;
public CreateStockMovementCommandHandler(
IStockMovementRepository repository,
IInventoryItemRepository inventoryRepository)
{
_repository = repository;
_inventoryRepository = inventoryRepository;
}
public async Task<long> Handle(CreateStockMovementCommand request, CancellationToken cancellationToken)
{
// بررسی وجود آیتم موجودی
var inventoryItem = await _inventoryRepository.GetByIdAsync(request.InventoryItemId, cancellationToken);
if (inventoryItem == null)
{
throw new ArgumentException("Inventory item not found");
}
var stockMovement = new StockMovement
{
InventoryItemId = request.InventoryItemId,
MovementType = request.MovementType,
Quantity = request.Quantity,
OrderId = request.OrderId,
DiscountOrderId = request.DiscountOrderId,
ReferenceNumber = request.ReferenceNumber,
Note = request.Note,
PerformedByUserId = request.PerformedByUserId
};
var createdMovement = await _repository.AddAsync(stockMovement, cancellationToken);
return createdMovement.Id;
}
}
/// <summary>
/// Handler برای ثبت چندین حرکت موجودی bulk
/// </summary>
public class BulkCreateStockMovementCommandHandler : IRequestHandler<BulkCreateStockMovementCommand, List<long>>
{
private readonly IStockMovementRepository _repository;
private readonly IInventoryItemRepository _inventoryRepository;
public BulkCreateStockMovementCommandHandler(
IStockMovementRepository repository,
IInventoryItemRepository inventoryRepository)
{
_repository = repository;
_inventoryRepository = inventoryRepository;
}
public async Task<List<long>> Handle(BulkCreateStockMovementCommand request, CancellationToken cancellationToken)
{
var stockMovements = new List<StockMovement>();
// بررسی وجود تمام آیتم های موجودی
var inventoryItemIds = request.Movements.Select(m => m.InventoryItemId).Distinct().ToList();
foreach (var inventoryItemId in inventoryItemIds)
{
var item = await _inventoryRepository.GetByIdAsync(inventoryItemId, cancellationToken);
if (item == null)
{
throw new ArgumentException($"Inventory item with ID {inventoryItemId} not found");
}
}
foreach (var movement in request.Movements)
{
var stockMovement = new StockMovement
{
InventoryItemId = movement.InventoryItemId,
MovementType = movement.MovementType,
Quantity = movement.Quantity,
OrderId = movement.OrderId,
DiscountOrderId = movement.DiscountOrderId,
ReferenceNumber = movement.ReferenceNumber,
Note = movement.Note,
PerformedByUserId = movement.PerformedByUserId
};
stockMovements.Add(stockMovement);
}
var createdMovements = await _repository.BulkAddAsync(stockMovements, cancellationToken);
return createdMovements.Select(m => m.Id).ToList();
}
}
/// <summary>
/// Handler برای حذف حرکت موجودی
/// </summary>
public class DeleteStockMovementCommandHandler : IRequestHandler<DeleteStockMovementCommand, bool>
{
private readonly IStockMovementRepository _repository;
public DeleteStockMovementCommandHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(DeleteStockMovementCommand request, CancellationToken cancellationToken)
{
await _repository.DeleteAsync(request.Id, cancellationToken);
return true;
}
}
@@ -0,0 +1,269 @@
using MediatR;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Application.Features.StockMovements.Queries;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.Features.StockMovements.Handlers;
/// <summary>
/// Handler برای دریافت حرکت موجودی به ID
/// </summary>
public class GetStockMovementByIdQueryHandler : IRequestHandler<GetStockMovementByIdQuery, StockMovement?>
{
private readonly IStockMovementRepository _repository;
public GetStockMovementByIdQueryHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<StockMovement?> Handle(GetStockMovementByIdQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByIdAsync(request.Id, cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت تاریخچه حرکت موجودی یک آیتم
/// </summary>
public class GetInventoryItemMovementHistoryQueryHandler : IRequestHandler<GetInventoryItemMovementHistoryQuery, List<StockMovement>>
{
private readonly IStockMovementRepository _repository;
public GetInventoryItemMovementHistoryQueryHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<List<StockMovement>> Handle(GetInventoryItemMovementHistoryQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByInventoryItemIdAsync(
request.InventoryItemId,
request.MovementType,
request.FromDate,
request.ToDate,
request.Skip,
request.Take,
cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت حرکات موجودی بر اساس سفارش
/// </summary>
public class GetStockMovementsByOrderQueryHandler : IRequestHandler<GetStockMovementsByOrderQuery, List<StockMovement>>
{
private readonly IStockMovementRepository _repository;
public GetStockMovementsByOrderQueryHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<List<StockMovement>> Handle(GetStockMovementsByOrderQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByOrderIdAsync(request.OrderId, cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت حرکات موجودی بر اساس سفارش تخفیف
/// </summary>
public class GetStockMovementsByDiscountOrderQueryHandler : IRequestHandler<GetStockMovementsByDiscountOrderQuery, List<StockMovement>>
{
private readonly IStockMovementRepository _repository;
public GetStockMovementsByDiscountOrderQueryHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<List<StockMovement>> Handle(GetStockMovementsByDiscountOrderQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByDiscountOrderIdAsync(request.DiscountOrderId, cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت حرکات موجودی بر اساس شماره مرجع
/// </summary>
public class GetStockMovementsByReferenceQueryHandler : IRequestHandler<GetStockMovementsByReferenceQuery, List<StockMovement>>
{
private readonly IStockMovementRepository _repository;
public GetStockMovementsByReferenceQueryHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<List<StockMovement>> Handle(GetStockMovementsByReferenceQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByReferenceNumberAsync(request.ReferenceNumber, cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت حرکات موجودی بر اساس نوع
/// </summary>
public class GetStockMovementsByTypeQueryHandler : IRequestHandler<GetStockMovementsByTypeQuery, List<StockMovement>>
{
private readonly IStockMovementRepository _repository;
public GetStockMovementsByTypeQueryHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<List<StockMovement>> Handle(GetStockMovementsByTypeQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByMovementTypeAsync(
request.MovementType,
request.FromDate,
request.ToDate,
request.Skip,
request.Take,
cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت آخرین حرکات موجودی
/// </summary>
public class GetRecentStockMovementsQueryHandler : IRequestHandler<GetRecentStockMovementsQuery, List<StockMovement>>
{
private readonly IStockMovementRepository _repository;
public GetRecentStockMovementsQueryHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<List<StockMovement>> Handle(GetRecentStockMovementsQuery request, CancellationToken cancellationToken)
{
return await _repository.GetRecentMovementsAsync(request.Count, request.MovementType, cancellationToken);
}
}
/// <summary>
/// Handler برای جستجوی حرکات موجودی
/// </summary>
public class SearchStockMovementsQueryHandler : IRequestHandler<SearchStockMovementsQuery, List<StockMovement>>
{
private readonly IStockMovementRepository _repository;
public SearchStockMovementsQueryHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<List<StockMovement>> Handle(SearchStockMovementsQuery request, CancellationToken cancellationToken)
{
return await _repository.SearchAsync(
request.InventoryItemId,
request.MovementType,
request.FromDate,
request.ToDate,
request.ReferenceNumber,
request.OrderId,
request.DiscountOrderId,
request.PerformedByUserId,
request.Skip,
request.Take,
cancellationToken);
}
}
/// <summary>
/// Handler برای شمارش حرکات موجودی
/// </summary>
public class GetStockMovementsCountQueryHandler : IRequestHandler<GetStockMovementsCountQuery, int>
{
private readonly IStockMovementRepository _repository;
public GetStockMovementsCountQueryHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<int> Handle(GetStockMovementsCountQuery request, CancellationToken cancellationToken)
{
return await _repository.CountAsync(
request.InventoryItemId,
request.MovementType,
request.FromDate,
request.ToDate,
request.ReferenceNumber,
request.OrderId,
request.DiscountOrderId,
request.PerformedByUserId,
cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت خلاصه حرکات موجودی
/// </summary>
public class GetMovementSummaryQueryHandler : IRequestHandler<GetMovementSummaryQuery, Dictionary<StockMovementType, int>>
{
private readonly IStockMovementRepository _repository;
public GetMovementSummaryQueryHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<Dictionary<StockMovementType, int>> Handle(GetMovementSummaryQuery request, CancellationToken cancellationToken)
{
return await _repository.GetMovementSummaryAsync(
request.FromDate,
request.ToDate,
request.InventoryItemId,
cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت حجم حرکات روزانه
/// </summary>
public class GetDailyMovementVolumeQueryHandler : IRequestHandler<GetDailyMovementVolumeQuery, List<(DateTime Date, int InboundQuantity, int OutboundQuantity)>>
{
private readonly IStockMovementRepository _repository;
public GetDailyMovementVolumeQueryHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<List<(DateTime Date, int InboundQuantity, int OutboundQuantity)>> Handle(GetDailyMovementVolumeQuery request, CancellationToken cancellationToken)
{
return await _repository.GetDailyMovementVolumeAsync(
request.FromDate,
request.ToDate,
request.InventoryItemId,
cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت محصولات پر حرکت
/// </summary>
public class GetTopMovingProductsQueryHandler : IRequestHandler<GetTopMovingProductsQuery, List<(long InventoryItemId, string ProductName, int MovementCount, int TotalQuantityChange)>>
{
private readonly IStockMovementRepository _repository;
public GetTopMovingProductsQueryHandler(IStockMovementRepository repository)
{
_repository = repository;
}
public async Task<List<(long InventoryItemId, string ProductName, int MovementCount, int TotalQuantityChange)>> Handle(GetTopMovingProductsQuery request, CancellationToken cancellationToken)
{
return await _repository.GetTopMovingProductsAsync(
request.FromDate,
request.ToDate,
request.Count,
request.MovementType,
cancellationToken);
}
}
@@ -0,0 +1,122 @@
using MediatR;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.Features.StockMovements.Queries;
/// <summary>
/// Query برای دریافت حرکت موجودی به ID
/// </summary>
public record GetStockMovementByIdQuery(long Id) : IRequest<StockMovement?>;
/// <summary>
/// Query برای دریافت تاریخچه حرکت موجودی یک آیتم
/// </summary>
public record GetInventoryItemMovementHistoryQuery : IRequest<List<StockMovement>>
{
public long InventoryItemId { get; init; }
public StockMovementType? MovementType { get; init; }
public DateTime? FromDate { get; init; }
public DateTime? ToDate { get; init; }
public int Skip { get; init; } = 0;
public int Take { get; init; } = 100;
}
/// <summary>
/// Query برای دریافت حرکات موجودی بر اساس سفارش
/// </summary>
public record GetStockMovementsByOrderQuery(long OrderId) : IRequest<List<StockMovement>>;
/// <summary>
/// Query برای دریافت حرکات موجودی بر اساس سفارش تخفیف
/// </summary>
public record GetStockMovementsByDiscountOrderQuery(long DiscountOrderId) : IRequest<List<StockMovement>>;
/// <summary>
/// Query برای دریافت حرکات موجودی بر اساس شماره مرجع
/// </summary>
public record GetStockMovementsByReferenceQuery(string ReferenceNumber) : IRequest<List<StockMovement>>;
/// <summary>
/// Query برای دریافت حرکات موجودی بر اساس نوع
/// </summary>
public record GetStockMovementsByTypeQuery : IRequest<List<StockMovement>>
{
public StockMovementType MovementType { get; init; }
public DateTime? FromDate { get; init; }
public DateTime? ToDate { get; init; }
public int Skip { get; init; } = 0;
public int Take { get; init; } = 100;
}
/// <summary>
/// Query برای دریافت آخرین حرکات موجودی
/// </summary>
public record GetRecentStockMovementsQuery : IRequest<List<StockMovement>>
{
public int Count { get; init; } = 50;
public StockMovementType? MovementType { get; init; }
}
/// <summary>
/// Query برای جستجوی حرکات موجودی
/// </summary>
public record SearchStockMovementsQuery : IRequest<List<StockMovement>>
{
public long? InventoryItemId { get; init; }
public StockMovementType? MovementType { get; init; }
public DateTime? FromDate { get; init; }
public DateTime? ToDate { get; init; }
public string? ReferenceNumber { get; init; }
public long? OrderId { get; init; }
public long? DiscountOrderId { get; init; }
public long? PerformedByUserId { get; init; }
public int Skip { get; init; } = 0;
public int Take { get; init; } = 100;
}
/// <summary>
/// Query برای شمارش حرکات موجودی
/// </summary>
public record GetStockMovementsCountQuery : IRequest<int>
{
public long? InventoryItemId { get; init; }
public StockMovementType? MovementType { get; init; }
public DateTime? FromDate { get; init; }
public DateTime? ToDate { get; init; }
public string? ReferenceNumber { get; init; }
public long? OrderId { get; init; }
public long? DiscountOrderId { get; init; }
public long? PerformedByUserId { get; init; }
}
/// <summary>
/// Query برای دریافت خلاصه حرکات موجودی
/// </summary>
public record GetMovementSummaryQuery : IRequest<Dictionary<StockMovementType, int>>
{
public DateTime FromDate { get; init; }
public DateTime ToDate { get; init; }
public long? InventoryItemId { get; init; }
}
/// <summary>
/// Query برای دریافت حجم حرکات روزانه
/// </summary>
public record GetDailyMovementVolumeQuery : IRequest<List<(DateTime Date, int InboundQuantity, int OutboundQuantity)>>
{
public DateTime FromDate { get; init; }
public DateTime ToDate { get; init; }
public long? InventoryItemId { get; init; }
}
/// <summary>
/// Query برای دریافت محصولات پر حرکت
/// </summary>
public record GetTopMovingProductsQuery : IRequest<List<(long InventoryItemId, string ProductName, int MovementCount, int TotalQuantityChange)>>
{
public DateTime FromDate { get; init; }
public DateTime ToDate { get; init; }
public int Count { get; init; } = 10;
public StockMovementType? MovementType { get; init; }
}
@@ -0,0 +1,66 @@
using MediatR;
namespace CMSMicroservice.Application.Features.Warehouses.Commands;
/// <summary>
/// Command برای ایجاد انبار جدید
/// </summary>
public record CreateWarehouseCommand : IRequest<long>
{
public string Name { get; init; } = string.Empty;
public string Code { get; init; } = string.Empty;
public string? Description { get; init; }
public string? Address { get; init; }
public string? CityName { get; init; }
public bool IsActive { get; init; } = true;
public bool IsDefault { get; init; } = false;
}
/// <summary>
/// Command برای آپدیت انبار
/// </summary>
public record UpdateWarehouseCommand : IRequest<bool>
{
public long Id { get; init; }
public string? Name { get; init; }
public string? Code { get; init; }
public string? Description { get; init; }
public string? Address { get; init; }
public string? CityName { get; init; }
public bool? IsActive { get; init; }
public bool? IsDefault { get; init; }
}
/// <summary>
/// Command برای حذف انبار
/// </summary>
public record DeleteWarehouseCommand(long Id) : IRequest<bool>;
/// <summary>
/// Command برای تعیین انبار پیش‌فرض
/// </summary>
public record SetDefaultWarehouseCommand(long Id) : IRequest<bool>;
/// <summary>
/// Command برای فعال/غیرفعال کردن انبار
/// </summary>
public record ActivateWarehouseCommand(long Id, bool IsActive) : IRequest<bool>;
/// <summary>
/// Command برای ایجاد چندین انبار به صورت bulk
/// </summary>
public record BulkCreateWarehousesCommand : IRequest<List<long>>
{
public List<WarehouseItem> Warehouses { get; init; } = new();
public record WarehouseItem
{
public string Name { get; init; } = string.Empty;
public string Code { get; init; } = string.Empty;
public string? Description { get; init; }
public string? Address { get; init; }
public string? CityName { get; init; }
public bool IsActive { get; init; } = true;
public bool IsDefault { get; init; } = false;
}
}
@@ -0,0 +1,208 @@
using MediatR;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Application.Features.Warehouses.Commands;
using CMSMicroservice.Domain.Entities;
namespace CMSMicroservice.Application.Features.Warehouses.Handlers;
/// <summary>
/// Handler برای ایجاد انبار جدید
/// </summary>
public class CreateWarehouseCommandHandler : IRequestHandler<CreateWarehouseCommand, long>
{
private readonly IWarehouseRepository _repository;
public CreateWarehouseCommandHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<long> Handle(CreateWarehouseCommand request, CancellationToken cancellationToken)
{
// بررسی تکراری نبودن کد
var codeExists = await _repository.ExistsByCodeAsync(request.Code, cancellationToken: cancellationToken);
if (codeExists)
{
throw new InvalidOperationException($"Warehouse with code '{request.Code}' already exists");
}
var warehouse = new Warehouse
{
Name = request.Name,
Code = request.Code,
Address = request.Address,
IsActive = request.IsActive,
IsDefault = request.IsDefault
};
var createdWarehouse = await _repository.AddAsync(warehouse, cancellationToken);
return createdWarehouse.Id;
}
}
/// <summary>
/// Handler برای آپدیت انبار
/// </summary>
public class UpdateWarehouseCommandHandler : IRequestHandler<UpdateWarehouseCommand, bool>
{
private readonly IWarehouseRepository _repository;
public UpdateWarehouseCommandHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(UpdateWarehouseCommand request, CancellationToken cancellationToken)
{
var warehouse = await _repository.GetByIdAsync(request.Id, cancellationToken);
if (warehouse == null)
{
return false;
}
// بررسی تکراری نبودن کد جدید
if (!string.IsNullOrEmpty(request.Code) && request.Code != warehouse.Code)
{
var codeExists = await _repository.ExistsByCodeAsync(request.Code, request.Id, cancellationToken);
if (codeExists)
{
throw new InvalidOperationException($"Warehouse with code '{request.Code}' already exists");
}
warehouse.Code = request.Code;
}
if (!string.IsNullOrEmpty(request.Name))
{
warehouse.Name = request.Name;
}
if (request.Address != null)
{
warehouse.Address = request.Address;
}
if (request.IsActive.HasValue)
{
warehouse.IsActive = request.IsActive.Value;
}
if (request.IsDefault.HasValue)
{
warehouse.IsDefault = request.IsDefault.Value;
}
await _repository.UpdateAsync(warehouse, cancellationToken);
return true;
}
}
/// <summary>
/// Handler برای حذف انبار
/// </summary>
public class DeleteWarehouseCommandHandler : IRequestHandler<DeleteWarehouseCommand, bool>
{
private readonly IWarehouseRepository _repository;
public DeleteWarehouseCommandHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(DeleteWarehouseCommand request, CancellationToken cancellationToken)
{
try
{
await _repository.DeleteAsync(request.Id, cancellationToken);
return true;
}
catch (InvalidOperationException)
{
// انبار دارای موجودی است
return false;
}
}
}
/// <summary>
/// Handler برای تعیین انبار پیش‌فرض
/// </summary>
public class SetDefaultWarehouseCommandHandler : IRequestHandler<SetDefaultWarehouseCommand, bool>
{
private readonly IWarehouseRepository _repository;
public SetDefaultWarehouseCommandHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(SetDefaultWarehouseCommand request, CancellationToken cancellationToken)
{
await _repository.SetAsDefaultAsync(request.Id, cancellationToken);
return true;
}
}
/// <summary>
/// Handler برای فعال/غیرفعال کردن انبار
/// </summary>
public class ActivateWarehouseCommandHandler : IRequestHandler<ActivateWarehouseCommand, bool>
{
private readonly IWarehouseRepository _repository;
public ActivateWarehouseCommandHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(ActivateWarehouseCommand request, CancellationToken cancellationToken)
{
await _repository.SetActiveStatusAsync(request.Id, request.IsActive, cancellationToken);
return true;
}
}
/// <summary>
/// Handler برای ایجاد چندین انبار bulk
/// </summary>
public class BulkCreateWarehousesCommandHandler : IRequestHandler<BulkCreateWarehousesCommand, List<long>>
{
private readonly IWarehouseRepository _repository;
public BulkCreateWarehousesCommandHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<List<long>> Handle(BulkCreateWarehousesCommand request, CancellationToken cancellationToken)
{
var ids = new List<long>();
// بررسی تکراری نبودن کدها
var codes = request.Warehouses.Select(w => w.Code).ToList();
foreach (var code in codes.Distinct())
{
var codeExists = await _repository.ExistsByCodeAsync(code, cancellationToken: cancellationToken);
if (codeExists)
{
throw new InvalidOperationException($"Warehouse with code '{code}' already exists");
}
}
foreach (var warehouseItem in request.Warehouses)
{
var warehouse = new Warehouse
{
Name = warehouseItem.Name,
Code = warehouseItem.Code,
Address = warehouseItem.Address,
IsActive = warehouseItem.IsActive,
IsDefault = warehouseItem.IsDefault
};
var created = await _repository.AddAsync(warehouse, cancellationToken);
ids.Add(created.Id);
}
return ids;
}
}
@@ -0,0 +1,202 @@
using MediatR;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Application.Features.Warehouses.Queries;
using CMSMicroservice.Domain.Entities;
namespace CMSMicroservice.Application.Features.Warehouses.Handlers;
/// <summary>
/// Handler برای دریافت انبار به ID
/// </summary>
public class GetWarehouseByIdQueryHandler : IRequestHandler<GetWarehouseByIdQuery, Warehouse?>
{
private readonly IWarehouseRepository _repository;
public GetWarehouseByIdQueryHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<Warehouse?> Handle(GetWarehouseByIdQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByIdAsync(request.Id, cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت انبار به کد
/// </summary>
public class GetWarehouseByCodeQueryHandler : IRequestHandler<GetWarehouseByCodeQuery, Warehouse?>
{
private readonly IWarehouseRepository _repository;
public GetWarehouseByCodeQueryHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<Warehouse?> Handle(GetWarehouseByCodeQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByCodeAsync(request.Code, cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت انبار پیش‌فرض
/// </summary>
public class GetDefaultWarehouseQueryHandler : IRequestHandler<GetDefaultWarehouseQuery, Warehouse?>
{
private readonly IWarehouseRepository _repository;
public GetDefaultWarehouseQueryHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<Warehouse?> Handle(GetDefaultWarehouseQuery request, CancellationToken cancellationToken)
{
return await _repository.GetDefaultWarehouseAsync(cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت انبارهای فعال
/// </summary>
public class GetActiveWarehousesQueryHandler : IRequestHandler<GetActiveWarehousesQuery, List<Warehouse>>
{
private readonly IWarehouseRepository _repository;
public GetActiveWarehousesQueryHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<List<Warehouse>> Handle(GetActiveWarehousesQuery request, CancellationToken cancellationToken)
{
return await _repository.GetActiveWarehousesAsync(cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت تمام انبارها
/// </summary>
public class GetAllWarehousesQueryHandler : IRequestHandler<GetAllWarehousesQuery, List<Warehouse>>
{
private readonly IWarehouseRepository _repository;
public GetAllWarehousesQueryHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<List<Warehouse>> Handle(GetAllWarehousesQuery request, CancellationToken cancellationToken)
{
return await _repository.GetAllAsync(includeInactive: true, cancellationToken);
}
}
/// <summary>
/// Handler برای جستجوی انبارها
/// </summary>
public class SearchWarehousesQueryHandler : IRequestHandler<SearchWarehousesQuery, List<Warehouse>>
{
private readonly IWarehouseRepository _repository;
public SearchWarehousesQueryHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<List<Warehouse>> Handle(SearchWarehousesQuery request, CancellationToken cancellationToken)
{
return await _repository.SearchAsync(
request.SearchTerm,
request.IsActive,
request.Skip,
request.Take,
cancellationToken);
}
}
/// <summary>
/// Handler برای شمارش انبارها
/// </summary>
public class GetWarehousesCountQueryHandler : IRequestHandler<GetWarehousesCountQuery, int>
{
private readonly IWarehouseRepository _repository;
public GetWarehousesCountQueryHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<int> Handle(GetWarehousesCountQuery request, CancellationToken cancellationToken)
{
return await _repository.CountAsync(
request.SearchTerm,
request.IsActive,
cancellationToken);
}
}
/// <summary>
/// Handler برای بررسی وجود انبار
/// </summary>
public class WarehouseExistsQueryHandler : IRequestHandler<WarehouseExistsQuery, bool>
{
private readonly IWarehouseRepository _repository;
public WarehouseExistsQueryHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(WarehouseExistsQuery request, CancellationToken cancellationToken)
{
var warehouse = await _repository.GetByIdAsync(request.Id, cancellationToken);
return warehouse != null;
}
}
/// <summary>
/// Handler برای بررسی وجود انبار با کد
/// </summary>
public class WarehouseExistsByCodeQueryHandler : IRequestHandler<WarehouseExistsByCodeQuery, bool>
{
private readonly IWarehouseRepository _repository;
public WarehouseExistsByCodeQueryHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<bool> Handle(WarehouseExistsByCodeQuery request, CancellationToken cancellationToken)
{
return await _repository.ExistsByCodeAsync(request.Code, request.ExcludeId, cancellationToken);
}
}
/// <summary>
/// Handler برای دریافت آمار انبار
/// </summary>
public class GetWarehouseStatisticsQueryHandler : IRequestHandler<GetWarehouseStatisticsQuery, Dictionary<string, object>>
{
private readonly IWarehouseRepository _repository;
public GetWarehouseStatisticsQueryHandler(IWarehouseRepository repository)
{
_repository = repository;
}
public async Task<Dictionary<string, object>> Handle(GetWarehouseStatisticsQuery request, CancellationToken cancellationToken)
{
var stats = await _repository.GetWarehouseStatisticsAsync(request.Id, cancellationToken);
return new Dictionary<string, object>
{
["TotalProducts"] = stats.TotalProducts,
["LowStockProducts"] = stats.LowStockProducts,
["OutOfStockProducts"] = stats.OutOfStockProducts,
["TotalValue"] = stats.TotalValue
};
}
}
@@ -0,0 +1,68 @@
using MediatR;
using CMSMicroservice.Domain.Entities;
namespace CMSMicroservice.Application.Features.Warehouses.Queries;
/// <summary>
/// Query برای دریافت انبار به ID
/// </summary>
public record GetWarehouseByIdQuery(long Id) : IRequest<Warehouse?>;
/// <summary>
/// Query برای دریافت انبار به کد
/// </summary>
public record GetWarehouseByCodeQuery(string Code) : IRequest<Warehouse?>;
/// <summary>
/// Query برای دریافت انبار پیش‌فرض
/// </summary>
public record GetDefaultWarehouseQuery : IRequest<Warehouse?>;
/// <summary>
/// Query برای دریافت انبارهای فعال
/// </summary>
public record GetActiveWarehousesQuery : IRequest<List<Warehouse>>;
/// <summary>
/// Query برای دریافت تمام انبارها
/// </summary>
public record GetAllWarehousesQuery : IRequest<List<Warehouse>>;
/// <summary>
/// Query برای جستجوی انبارها
/// </summary>
public record SearchWarehousesQuery : IRequest<List<Warehouse>>
{
public string? SearchTerm { get; init; }
public bool? IsActive { get; init; }
public int Skip { get; init; } = 0;
public int Take { get; init; } = 100;
}
/// <summary>
/// Query برای شمارش انبارها
/// </summary>
public record GetWarehousesCountQuery : IRequest<int>
{
public string? SearchTerm { get; init; }
public bool? IsActive { get; init; }
}
/// <summary>
/// Query برای بررسی وجود انبار
/// </summary>
public record WarehouseExistsQuery(long Id) : IRequest<bool>;
/// <summary>
/// Query برای بررسی وجود انبار با کد
/// </summary>
public record WarehouseExistsByCodeQuery : IRequest<bool>
{
public string Code { get; init; } = string.Empty;
public long? ExcludeId { get; init; }
}
/// <summary>
/// Query برای دریافت آمار انبار
/// </summary>
public record GetWarehouseStatisticsQuery(long Id) : IRequest<Dictionary<string, object>>;
@@ -32,4 +32,9 @@ public class CreateManualPaymentCommand : IRequest<long>
/// شماره مرجع یا شماره فیش (اختیاری)
/// </summary>
public string? ReferenceNumber { get; set; }
/// <summary>
/// مسیر تصویر فیش واریزی (اختیاری)
/// </summary>
public string? ImagePath { get; set; }
}
@@ -1,5 +1,6 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Entities.Payment;
using CMSMicroservice.Domain.Enums;
@@ -32,13 +33,24 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
try
{
_logger.LogInformation(
"Creating manual payment for UserId: {UserId}, Amount: {Amount}, Type: {Type}",
"Creating manual membership payment for UserId: {UserId}, Type: {Type}",
request.UserId,
request.Amount,
request.Type
);
// 1. بررسی وجود کاربر
// 1. بررسی Admin فعلی
var currentUserId = _currentUser.UserId;
if (string.IsNullOrEmpty(currentUserId))
{
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
}
if (!long.TryParse(currentUserId, out var adminUserId))
{
throw new UnauthorizedAccessException("شناسه کاربر نامعتبر است");
}
// 2. بررسی وجود کاربر
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
@@ -48,47 +60,102 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
throw new NotFoundException(nameof(User), request.UserId);
}
// 2. بررسی Admin فعلی
var currentUserId = _currentUser.UserId;
if (string.IsNullOrEmpty(currentUserId))
// 3. پیدا کردن کیف پول
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == request.UserId, cancellationToken);
if (wallet == null)
{
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
_logger.LogError("Wallet not found for UserId: {UserId}", request.UserId);
throw new NotFoundException($"کیف پول کاربر {request.UserId} یافت نشد");
}
if (!long.TryParse(currentUserId, out var requestedById))
{
throw new UnauthorizedAccessException("شناسه کاربر نامعتبر است");
}
// 4. محاسبه مبالغ
var balanceAmount = SystemConstants.BasePackageAmount; // 56M
var discountBalanceAmount = SystemConstants.BasePackageAmount * 2; // 112M
var totalAmount = balanceAmount + discountBalanceAmount; // 168M
// 3. ایجاد ManualPayment
// 5. ثبت تراکنش
var transaction = new Transaction
{
Amount = totalAmount,
Description = $"عضویت دستی باشگاه مشتریان - {request.Description} - مرجع: {request.ReferenceNumber}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.Now,
RefId = request.ReferenceNumber,
Type = TransactionType.DepositExternal1
};
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(cancellationToken);
// 6. ایجاد ManualPayment با وضعیت Approved (بدون نیاز به تایید دو مرحله‌ای)
var manualPayment = new ManualPayment
{
UserId = request.UserId,
Amount = request.Amount,
Amount = totalAmount,
Type = request.Type,
Description = request.Description,
ReferenceNumber = request.ReferenceNumber,
Status = ManualPaymentStatus.Pending,
RequestedBy = requestedById
ImagePath = request.ImagePath,
Status = ManualPaymentStatus.Approved,
RequestedBy = adminUserId,
ApprovedBy = adminUserId,
ApprovedAt = DateTime.Now,
TransactionId = transaction.Id
};
_context.ManualPayments.Add(manualPayment);
// 7. اعمال تغییرات بر کیف پول
var oldBalance = wallet.Balance;
var oldDiscountBalance = wallet.DiscountBalance;
wallet.Balance += balanceAmount; // +56M
wallet.DiscountBalance += discountBalanceAmount; // +112M
// 8. ثبت لاگ کیف پول
var walletLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = balanceAmount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = discountBalanceAmount,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(walletLog, cancellationToken);
// 9. تنظیم روش خرید پکیج
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
// 10. ذخیره همه تغییرات
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Manual payment created successfully. Id: {Id}, UserId: {UserId}, RequestedBy: {RequestedBy}",
"Manual membership payment created successfully. " +
"ManualPaymentId: {Id}, UserId: {UserId}, TransactionId: {TransactionId}, " +
"Balance: {OldBalance} -> {NewBalance}, DiscountBalance: {OldDiscount} -> {NewDiscount}",
manualPayment.Id,
request.UserId,
requestedById
transaction.Id,
oldBalance,
wallet.Balance,
oldDiscountBalance,
wallet.DiscountBalance
);
return manualPayment.Id;
}
catch (Exception ex)
catch (Exception ex) when (ex is not NotFoundException && ex is not UnauthorizedAccessException)
{
_logger.LogError(
ex,
"Error creating manual payment for UserId: {UserId}",
"Error creating manual membership payment for UserId: {UserId}",
request.UserId
);
throw;
@@ -118,58 +118,19 @@ public class ProcessManualMembershipPaymentCommandHandler : IRequestHandler<Proc
};
await _context.UserWalletChangeLogs.AddAsync(balanceLog, cancellationToken);
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
// 10. به‌روزرسانی ManualPayment با TransactionId
manualPayment.TransactionId = transaction.Id;
await _context.SaveChangesAsync(cancellationToken);
// 11. پیدا کردن یا ایجاد آدرس پیشفرض کاربر
var userAddress = await _context.UserAddresses
.Where(a => a.UserId == request.UserId)
.OrderByDescending(a => a.IsDefault)
.ThenBy(a => a.Id)
.FirstOrDefaultAsync(cancellationToken);
if (userAddress == null)
{
userAddress = new UserAddress
{
UserId = request.UserId,
Title = "آدرس پیشفرض",
Address = "پرداخت دستی عضویت - آدرس موقت",
PostalCode = "0000000000",
IsDefault = true,
CityId = 1
};
await _context.UserAddresses.AddAsync(userAddress, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
// 12. ثبت سفارش
var order = new UserOrder
{
UserId = request.UserId,
Amount = request.Amount,
TransactionId = transaction.Id,
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.Now,
PaymentMethod = PaymentMethod.Deposit,
DeliveryStatus = DeliveryStatus.None,
UserAddressId = userAddress.Id,
DeliveryDescription = $"پرداخت دستی عضویت - مرجع: {request.ReferenceNumber}"
};
_context.UserOrders.Add(order);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Manual membership payment processed successfully. UserId: {UserId}, Amount: {Amount}, ManualPaymentId: {ManualPaymentId}, TransactionId: {TransactionId}, OrderId: {OrderId}, AdminUserId: {AdminUserId}",
request.UserId, request.Amount, manualPayment.Id, transaction.Id, order.Id, adminUserId);
"Manual membership payment processed successfully. UserId: {UserId}, Amount: {Amount}, ManualPaymentId: {ManualPaymentId}, TransactionId: {TransactionId}, AdminUserId: {AdminUserId}",
request.UserId, request.Amount, manualPayment.Id, transaction.Id, adminUserId);
return new ProcessManualMembershipPaymentResponseDto
{
TransactionId = transaction.Id,
OrderId = order.Id,
NewWalletBalance = wallet.Balance,
Message = "پرداخت دستی با موفقیت ثبت شد"
};
@@ -1,13 +1,21 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProductsCommand, CreateNewProductsResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IInventoryService _inventoryService;
public CreateNewProductsCommandHandler(IApplicationDbContext context)
public CreateNewProductsCommandHandler(
IApplicationDbContext context,
IInventoryService inventoryService)
{
_context = context;
_inventoryService = inventoryService;
}
public async Task<CreateNewProductsResponseDto> Handle(CreateNewProductsCommand request,
@@ -17,6 +25,13 @@ public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProducts
await _context.Products.AddAsync(entity, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
// ایجاد رکورد موجودی در سیستم انبارداری
await _inventoryService.InitializeInventoryAsync(
entity.Id,
ProductType.RegularProduct,
request.RemainingCount,
ct: cancellationToken);
// ثبت دسته‌بندی‌های محصول (در صورت ارسال)
if (request.CategoryIds is { Count: > 0 })
{
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Domain.Enums;
@@ -6,17 +7,22 @@ namespace CMSMicroservice.Application.UserOrderCQ.Commands.CancelOrder;
public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, CancelOrderResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IInventoryService _inventoryService;
public CancelOrderCommandHandler(IApplicationDbContext context)
public CancelOrderCommandHandler(
IApplicationDbContext context,
IInventoryService inventoryService)
{
_context = context;
_inventoryService = inventoryService;
}
public async Task<CancelOrderResponseDto> Handle(CancelOrderCommand request, CancellationToken cancellationToken)
{
// پیدا کردن سفارش
// پیدا کردن سفارش با جزئیات
var order = await _context.UserOrders
.Include(o => o.Transaction)
.Include(o => o.FactorDetails)
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
if (order == null)
@@ -35,6 +41,19 @@ public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, Can
throw new InvalidOperationException("این سفارش قبلاً لغو شده است");
}
// برگشت موجودی محصولات به انبار
foreach (var factorDetail in order.FactorDetails)
{
await _inventoryService.ProcessReturnAsync(
factorDetail.ProductId,
ProductType.RegularProduct,
factorDetail.Count,
order.Id,
$"لغو سفارش: {request.CancelReason}",
null,
cancellationToken);
}
// تغییر وضعیت سفارش
order.DeliveryStatus = DeliveryStatus.Cancelled;
order.DeliveryDescription = $"لغو شده: {request.CancelReason}";
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Events;
@@ -10,14 +11,17 @@ public class
SubmitShopBuyOrderCommandHandler : IRequestHandler<SubmitShopBuyOrderCommand, SubmitShopBuyOrderResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IInventoryService _inventoryService;
private readonly ILogger<SubmitShopBuyOrderCommandHandler> _logger;
private float _vatRate;
public SubmitShopBuyOrderCommandHandler(
IApplicationDbContext context,
IInventoryService inventoryService,
ILogger<SubmitShopBuyOrderCommandHandler> logger)
{
_context = context;
_inventoryService = inventoryService;
_logger = logger;
}
@@ -148,10 +152,18 @@ public class
});
await _context.FactorDetails.AddRangeAsync(factorDetailsList, cancellationToken);
// کاهش موجودی محصولات و افزایش تعداد فروش
// کاهش موجودی محصولات و افزایش تعداد فروش از طریق InventoryService
foreach (var cartItem in user.UserCarts)
{
cartItem.Product.RemainingCount -= cartItem.Count;
// استفاده از سرویس انبارداری برای کسر موجودی
await _inventoryService.ConfirmSaleAsync(
cartItem.ProductId,
ProductType.RegularProduct,
cartItem.Count,
newOrder.Id,
cancellationToken);
// افزایش تعداد فروش
cartItem.Product.SaleCount += cartItem.Count;
}
@@ -0,0 +1,105 @@
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Entities.DiscountShop;
namespace CMSMicroservice.Domain.Entities;
/// <summary>
/// موجودی کالا در انبار
/// </summary>
public class InventoryItem : BaseAuditableEntity
{
// ========== شناسه محصول (یکی از دو فیلد زیر پر است) ==========
/// <summary>
/// شناسه محصول فروشگاه معمولی
/// </summary>
public long? ProductId { get; set; }
/// <summary>
/// محصول فروشگاه معمولی
/// </summary>
public Product? Product { get; set; }
/// <summary>
/// شناسه محصول فروشگاه تخفیفی
/// </summary>
public long? DiscountProductId { get; set; }
/// <summary>
/// محصول فروشگاه تخفیفی
/// </summary>
public DiscountProduct? DiscountProduct { get; set; }
// ========== نوع محصول ==========
/// <summary>
/// نوع محصول (معمولی یا تخفیفی)
/// </summary>
public ProductType ProductType { get; set; }
// ========== موجودی ==========
/// <summary>
/// موجودی فعلی (منبع اصلی)
/// </summary>
public int Quantity { get; set; }
/// <summary>
/// مقدار رزرو شده برای سفارشات pending
/// </summary>
public int ReservedQuantity { get; set; }
/// <summary>
/// موجودی قابل فروش (محاسباتی)
/// </summary>
public int AvailableQuantity => Quantity - ReservedQuantity;
// ========== تنظیمات انبار ==========
/// <summary>
/// آستانه هشدار کم‌موجودی
/// </summary>
public int LowStockThreshold { get; set; } = 10;
/// <summary>
/// نقطه سفارش مجدد
/// </summary>
public int ReorderPoint { get; set; } = 5;
/// <summary>
/// حداکثر موجودی مجاز
/// </summary>
public int MaxStockLevel { get; set; } = 1000;
// ========== آمار ==========
/// <summary>
/// تاریخ آخرین ورود کالا
/// </summary>
public DateTime? LastRestockedAt { get; set; }
/// <summary>
/// تاریخ آخرین فروش
/// </summary>
public DateTime? LastSoldAt { get; set; }
// ========== انبار (برای آینده) ==========
/// <summary>
/// شناسه انبار (پیش‌فرض: انبار اصلی)
/// </summary>
public long WarehouseId { get; set; } = 1;
/// <summary>
/// انبار
/// </summary>
public Warehouse? Warehouse { get; set; }
// ========== Navigation Properties ==========
/// <summary>
/// حرکات انبار مرتبط با این آیتم
/// </summary>
public ICollection<StockMovement> StockMovements { get; set; } = new List<StockMovement>();
}
@@ -38,6 +38,11 @@ public class ManualPayment : BaseAuditableEntity
/// </summary>
public string? ReferenceNumber { get; set; }
/// <summary>
/// مسیر تصویر فیش واریزی (اختیاری)
/// </summary>
public string? ImagePath { get; set; }
/// <summary>
/// وضعیت تایید
/// </summary>
@@ -0,0 +1,77 @@
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Domain.Entities;
/// <summary>
/// حرکات انبار (تاریخچه تغییرات موجودی)
/// </summary>
public class StockMovement : BaseAuditableEntity
{
// ========== ارتباط با InventoryItem ==========
/// <summary>
/// شناسه آیتم انبار
/// </summary>
public long InventoryItemId { get; set; }
/// <summary>
/// آیتم انبار
/// </summary>
public InventoryItem InventoryItem { get; set; } = null!;
// ========== نوع حرکت ==========
/// <summary>
/// نوع حرکت انبار
/// </summary>
public StockMovementType MovementType { get; set; }
// ========== مقادیر ==========
/// <summary>
/// مقدار تغییر (مثبت برای ورود، منفی برای خروج)
/// </summary>
public int Quantity { get; set; }
/// <summary>
/// موجودی قبل از این حرکت
/// </summary>
public int QuantityBefore { get; set; }
/// <summary>
/// موجودی بعد از این حرکت
/// </summary>
public int QuantityAfter { get; set; }
// ========== مراجع ==========
/// <summary>
/// شناسه سفارش (برای فروش/برگشت در فروشگاه معمولی)
/// </summary>
public long? OrderId { get; set; }
/// <summary>
/// شناسه سفارش تخفیفی (برای فروش/برگشت در فروشگاه تخفیفی)
/// </summary>
public long? DiscountOrderId { get; set; }
/// <summary>
/// شماره مرجع (مثل شماره فاکتور ورود کالا)
/// </summary>
public string? ReferenceNumber { get; set; }
// ========== توضیحات ==========
/// <summary>
/// یادداشت و توضیحات اضافی
/// </summary>
public string? Note { get; set; }
// ========== کاربر ==========
/// <summary>
/// شناسه کاربری که این عملیات را انجام داده
/// </summary>
public long? PerformedByUserId { get; set; }
}
@@ -0,0 +1,45 @@
using CMSMicroservice.Domain.Common;
namespace CMSMicroservice.Domain.Entities;
/// <summary>
/// انبار (برای آینده - چند انبار)
/// </summary>
public class Warehouse : BaseAuditableEntity
{
// ========== اطلاعات اصلی ==========
/// <summary>
/// نام انبار
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// کد انبار
/// </summary>
public string Code { get; set; } = null!;
/// <summary>
/// آدرس انبار
/// </summary>
public string? Address { get; set; }
// ========== تنظیمات ==========
/// <summary>
/// انبار پیش‌فرض سیستم
/// </summary>
public bool IsDefault { get; set; }
/// <summary>
/// وضعیت فعال/غیرفعال
/// </summary>
public bool IsActive { get; set; } = true;
// ========== Navigation Properties ==========
/// <summary>
/// آیتم‌های موجودی در این انبار
/// </summary>
public ICollection<InventoryItem> InventoryItems { get; set; } = new List<InventoryItem>();
}
@@ -0,0 +1,17 @@
namespace CMSMicroservice.Domain.Enums;
/// <summary>
/// نوع محصول در سیستم
/// </summary>
public enum ProductType
{
/// <summary>
/// محصول فروشگاه معمولی
/// </summary>
RegularProduct = 1,
/// <summary>
/// محصول فروشگاه تخفیفی
/// </summary>
DiscountProduct = 2
}
@@ -0,0 +1,75 @@
namespace CMSMicroservice.Domain.Enums;
/// <summary>
/// نوع حرکت انبار
/// </summary>
public enum StockMovementType
{
// ========== ورودی ==========
/// <summary>
/// موجودی اولیه محصول
/// </summary>
InitialStock = 1,
/// <summary>
/// ورود کالا به انبار (خرید از تامین‌کننده)
/// </summary>
Restock = 2,
/// <summary>
/// برگشت کالا از مشتری
/// </summary>
Return = 3,
/// <summary>
/// انتقال کالا از انبار دیگر
/// </summary>
TransferIn = 4,
// ========== خروجی ==========
/// <summary>
/// فروش کالا به مشتری
/// </summary>
Sale = 10,
/// <summary>
/// ضایعات (کالای خراب شده)
/// </summary>
Damaged = 11,
/// <summary>
/// مفقودی انبار
/// </summary>
Lost = 12,
/// <summary>
/// انتقال کالا به انبار دیگر
/// </summary>
TransferOut = 13,
// ========== تعدیل ==========
/// <summary>
/// تعدیل افزایشی موجودی (انبارگردانی مثبت)
/// </summary>
AdjustmentPlus = 20,
/// <summary>
/// تعدیل کاهشی موجودی (انبارگردانی منفی)
/// </summary>
AdjustmentMinus = 21,
// ========== رزرو ==========
/// <summary>
/// رزرو موجودی برای سفارش pending
/// </summary>
Reserved = 30,
/// <summary>
/// آزادسازی رزرو (لغو سفارش)
/// </summary>
Released = 31
}
@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Infrastructure.Persistence;
using CMSMicroservice.Infrastructure.Persistence.Repositories;
using CMSMicroservice.Infrastructure.Services;
namespace CMSMicroservice.Infrastructure;
/// <summary>
/// کلاس تزریق وابستگی برای لایه Infrastructure
/// </summary>
public static class DependencyInjection
{
/// <summary>
/// افزودن سرویس های Infrastructure به DI Container
/// </summary>
/// <param name="services">IServiceCollection</param>
/// <param name="configuration">IConfiguration</param>
/// <returns>IServiceCollection</returns>
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
// Database Configuration
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
// Application Context Interface
services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
// Repository Pattern Registration
services.AddScoped<IInventoryItemRepository, InventoryItemRepository>();
services.AddScoped<IStockMovementRepository, StockMovementRepository>();
services.AddScoped<IWarehouseRepository, WarehouseRepository>();
// Business Services
services.AddScoped<IInventoryService, InventoryService>();
return services;
}
}
@@ -14,9 +14,20 @@ namespace CMSMicroservice.Infrastructure.Persistence;
public class ApplicationDbContext : DbContext, IApplicationDbContext
{
private readonly IMediator _mediator;
private readonly AuditableEntitySaveChangesInterceptor _auditableEntitySaveChangesInterceptor;
private readonly IMediator? _mediator;
private readonly AuditableEntitySaveChangesInterceptor? _auditableEntitySaveChangesInterceptor;
/// <summary>
/// Constructor برای design-time (migrations)
/// </summary>
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
/// <summary>
/// Constructor اصلی برای runtime
/// </summary>
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options,
IMediator mediator,
@@ -39,7 +50,10 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(_auditableEntitySaveChangesInterceptor);
if (_auditableEntitySaveChangesInterceptor != null)
{
optionsBuilder.AddInterceptors(_auditableEntitySaveChangesInterceptor);
}
// Suppress PendingModelChangesWarning in EF Core 9
optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning));
@@ -119,4 +133,9 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
public DbSet<Country> Countries => Set<Country>();
public DbSet<State> States => Set<State>();
public DbSet<City> Cities => Set<City>();
// ============= Inventory Management DbSets =============
public DbSet<Warehouse> Warehouses => Set<Warehouse>();
public DbSet<InventoryItem> InventoryItems => Set<InventoryItem>();
public DbSet<StockMovement> StockMovements => Set<StockMovement>();
}
@@ -0,0 +1,46 @@
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using CMSMicroservice.Infrastructure.Persistence.Interceptors;
namespace CMSMicroservice.Infrastructure.Persistence;
/// <summary>
/// Factory برای ایجاد DbContext در زمان طراحی (migrations, scaffolding)
/// </summary>
public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext(string[] args)
{
// سعی در خواندن connection string از appsettings.json
var basePath = Directory.GetCurrentDirectory();
var webApiPath = Path.Combine(basePath, "../CMSMicroservice.WebApi");
if (Directory.Exists(webApiPath))
{
basePath = webApiPath;
}
var configuration = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("appsettings.Development.json", optional: true)
.AddEnvironmentVariables()
.Build();
var connectionString = configuration.GetConnectionString("DefaultConnection");
// اگر connection string پیدا نشد، از یک مقدار پیش‌فرض استفاده کن
if (string.IsNullOrEmpty(connectionString))
{
connectionString = "Server=localhost;Database=CMS;Trusted_Connection=True;TrustServerCertificate=True;";
}
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
optionsBuilder.UseSqlServer(connectionString,
b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName));
return new ApplicationDbContext(optionsBuilder.Options);
}
}
@@ -0,0 +1,122 @@
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Entities.DiscountShop;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
/// <summary>
/// تنظیمات Entity Framework برای موجودی کالا
/// </summary>
public class InventoryItemConfiguration : IEntityTypeConfiguration<InventoryItem>
{
public void Configure(EntityTypeBuilder<InventoryItem> builder)
{
// ========== تنظیمات پایه ==========
builder.HasQueryFilter(i => !i.IsDeleted);
builder.Ignore(entity => entity.DomainEvents);
builder.HasKey(entity => entity.Id);
builder.Property(entity => entity.Id).UseIdentityColumn();
// ========== فیلدهای اصلی ==========
builder.Property(e => e.ProductType)
.IsRequired()
.HasConversion<int>();
builder.Property(e => e.Quantity)
.IsRequired()
.HasDefaultValue(0);
builder.Property(e => e.ReservedQuantity)
.IsRequired()
.HasDefaultValue(0);
// ========== تنظیمات انبار ==========
builder.Property(e => e.LowStockThreshold)
.IsRequired()
.HasDefaultValue(10);
builder.Property(e => e.ReorderPoint)
.IsRequired()
.HasDefaultValue(5);
builder.Property(e => e.MaxStockLevel)
.IsRequired()
.HasDefaultValue(1000);
// ========== تاریخ‌ها ==========
builder.Property(e => e.LastRestockedAt)
.IsRequired(false);
builder.Property(e => e.LastSoldAt)
.IsRequired(false);
// ========== انبار ==========
builder.Property(e => e.WarehouseId)
.IsRequired()
.HasDefaultValue(1);
// ========== روابط ==========
// رابطه با Product (اختیاری)
builder.HasOne(e => e.Product)
.WithMany()
.HasForeignKey(e => e.ProductId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
// رابطه با DiscountProduct (اختیاری)
builder.HasOne(e => e.DiscountProduct)
.WithMany()
.HasForeignKey(e => e.DiscountProductId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
// رابطه با Warehouse
builder.HasOne(e => e.Warehouse)
.WithMany(w => w.InventoryItems)
.HasForeignKey(e => e.WarehouseId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
// ========== Index ها ==========
// Index روی ProductId برای جستجوی سریع محصولات معمولی
builder.HasIndex(e => e.ProductId)
.HasDatabaseName("IX_InventoryItems_ProductId");
// Index روی DiscountProductId برای جستجوی سریع محصولات تخفیفی
builder.HasIndex(e => e.DiscountProductId)
.HasDatabaseName("IX_InventoryItems_DiscountProductId");
// Index ترکیبی روی ProductType و Quantity برای گزارش‌گیری
builder.HasIndex(e => new { e.ProductType, e.Quantity })
.HasDatabaseName("IX_InventoryItems_ProductType_Quantity");
// Index ساده روی WarehouseId برای انبار
builder.HasIndex(e => e.WarehouseId)
.HasDatabaseName("IX_InventoryItems_WarehouseId");
// ========== محدودیت‌ها ==========
// محدودیت: یا ProductId یا DiscountProductId باید پر باشد (نه هر دو، نه هیچ‌کدام)
builder.HasCheckConstraint("CK_InventoryItem_ProductReference",
"(ProductId IS NOT NULL AND DiscountProductId IS NULL) OR (ProductId IS NULL AND DiscountProductId IS NOT NULL)");
// محدودیت: ProductType باید با نوع محصول مطابقت داشته باشد
builder.HasCheckConstraint("CK_InventoryItem_ProductType_Match",
"(ProductType = 1 AND ProductId IS NOT NULL) OR (ProductType = 2 AND DiscountProductId IS NOT NULL)");
// محدودیت: موجودی نمی‌تواند منفی باشد
builder.HasCheckConstraint("CK_InventoryItem_Quantity_NonNegative",
"Quantity >= 0");
// محدودیت: موجودی رزرو شده نمی‌تواند منفی باشد
builder.HasCheckConstraint("CK_InventoryItem_ReservedQuantity_NonNegative",
"ReservedQuantity >= 0");
// محدودیت: موجودی رزرو شده نمی‌تواند بیشتر از موجودی کل باشد
builder.HasCheckConstraint("CK_InventoryItem_ReservedQuantity_LessOrEqualQuantity",
"ReservedQuantity <= Quantity");
}
}
@@ -0,0 +1,117 @@
using CMSMicroservice.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
/// <summary>
/// تنظیمات Entity Framework برای حرکات انبار
/// </summary>
public class StockMovementConfiguration : IEntityTypeConfiguration<StockMovement>
{
public void Configure(EntityTypeBuilder<StockMovement> builder)
{
// ========== تنظیمات پایه ==========
builder.HasQueryFilter(s => !s.IsDeleted);
builder.Ignore(entity => entity.DomainEvents);
builder.HasKey(entity => entity.Id);
builder.Property(entity => entity.Id).UseIdentityColumn();
// ========== فیلدهای اصلی ==========
// نوع حرکت (enum به int)
builder.Property(e => e.MovementType)
.IsRequired()
.HasConversion<int>();
// مقدار تغییر (می‌تواند منفی باشد)
builder.Property(e => e.Quantity)
.IsRequired();
// موجودی قبل و بعد
builder.Property(e => e.QuantityBefore)
.IsRequired();
builder.Property(e => e.QuantityAfter)
.IsRequired();
// ========== فیلدهای اختیاری ==========
// شناسه سفارش معمولی
builder.Property(e => e.OrderId)
.IsRequired(false);
// شناسه سفارش تخفیفی
builder.Property(e => e.DiscountOrderId)
.IsRequired(false);
// شماره مرجع
builder.Property(e => e.ReferenceNumber)
.IsRequired(false)
.HasMaxLength(100);
// یادداشت
builder.Property(e => e.Note)
.IsRequired(false)
.HasMaxLength(500);
// کاربر انجام‌دهنده
builder.Property(e => e.PerformedByUserId)
.IsRequired(false);
// ========== روابط ==========
// رابطه با InventoryItem (اجباری)
builder.HasOne(e => e.InventoryItem)
.WithMany(i => i.StockMovements)
.HasForeignKey(e => e.InventoryItemId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
// ========== Index ها ==========
// Index روی InventoryItemId برای جستجوی حرکات یک آیتم
builder.HasIndex(e => e.InventoryItemId)
.HasDatabaseName("IX_StockMovements_InventoryItemId");
// Index روی MovementType برای فیلتر بر اساس نوع حرکت
builder.HasIndex(e => e.MovementType)
.HasDatabaseName("IX_StockMovements_MovementType");
// Index روی Created برای مرتب‌سازی زمانی
builder.HasIndex(e => e.Created)
.HasDatabaseName("IX_StockMovements_Created");
// Index ترکیبی برای گزارش‌گیری
builder.HasIndex(e => new { e.InventoryItemId, e.MovementType, e.Created })
.HasDatabaseName("IX_StockMovements_Item_Type_Date");
// Index روی OrderId برای ردیابی حرکات مرتبط با سفارش
builder.HasIndex(e => e.OrderId)
.HasDatabaseName("IX_StockMovements_OrderId")
.HasFilter("[OrderId] IS NOT NULL");
// Index روی DiscountOrderId برای ردیابی حرکات مرتبط با سفارش تخفیفی
builder.HasIndex(e => e.DiscountOrderId)
.HasDatabaseName("IX_StockMovements_DiscountOrderId")
.HasFilter("[DiscountOrderId] IS NOT NULL");
// Index روی ReferenceNumber برای جستجوی سریع با شماره مرجع
builder.HasIndex(e => e.ReferenceNumber)
.HasDatabaseName("IX_StockMovements_ReferenceNumber")
.HasFilter("[ReferenceNumber] IS NOT NULL");
// ========== محدودیت‌ها ==========
// محدودیت: QuantityAfter باید برابر QuantityBefore + Quantity باشد
builder.HasCheckConstraint("CK_StockMovement_QuantityAfter_Calculation",
"QuantityAfter = QuantityBefore + Quantity");
// محدودیت: QuantityBefore و QuantityAfter نمی‌توانند منفی باشند
builder.HasCheckConstraint("CK_StockMovement_QuantityBefore_NonNegative",
"QuantityBefore >= 0");
builder.HasCheckConstraint("CK_StockMovement_QuantityAfter_NonNegative",
"QuantityAfter >= 0");
}
}
@@ -0,0 +1,89 @@
using CMSMicroservice.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
/// <summary>
/// تنظیمات Entity Framework برای انبار
/// </summary>
public class WarehouseConfiguration : IEntityTypeConfiguration<Warehouse>
{
public void Configure(EntityTypeBuilder<Warehouse> builder)
{
// ========== تنظیمات پایه ==========
builder.HasQueryFilter(w => !w.IsDeleted);
builder.Ignore(entity => entity.DomainEvents);
builder.HasKey(entity => entity.Id);
builder.Property(entity => entity.Id).UseIdentityColumn();
// ========== فیلدهای اصلی ==========
// نام انبار (اجباری)
builder.Property(e => e.Name)
.IsRequired()
.HasMaxLength(200);
// کد انبار (اجباری و یکتا)
builder.Property(e => e.Code)
.IsRequired()
.HasMaxLength(50);
// آدرس انبار (اختیاری)
builder.Property(e => e.Address)
.IsRequired(false)
.HasMaxLength(1000);
// ========== تنظیمات ==========
// انبار پیش‌فرض
builder.Property(e => e.IsDefault)
.IsRequired()
.HasDefaultValue(false);
// وضعیت فعال/غیرفعال
builder.Property(e => e.IsActive)
.IsRequired()
.HasDefaultValue(true);
// ========== Index ها ==========
// Index یکتا روی کد انبار
builder.HasIndex(e => e.Code)
.IsUnique()
.HasDatabaseName("IX_Warehouses_Code_Unique");
// Index روی IsDefault برای پیدا کردن سریع انبار پیش‌فرض
builder.HasIndex(e => e.IsDefault)
.HasDatabaseName("IX_Warehouses_IsDefault")
.HasFilter("[IsDefault] = 1");
// Index روی IsActive برای فیلتر انبارهای فعال
builder.HasIndex(e => e.IsActive)
.HasDatabaseName("IX_Warehouses_IsActive");
// ========== روابط ==========
// رابطه یک-به-چند با InventoryItems
builder.HasMany(w => w.InventoryItems)
.WithOne(i => i.Warehouse)
.HasForeignKey(i => i.WarehouseId)
.OnDelete(DeleteBehavior.Restrict); // جلوگیری از حذف انبار در صورت وجود موجودی
// ========== داده‌های اولیه ==========
// انبار پیش‌فرض
builder.HasData(new Warehouse
{
Id = 1,
Name = "انبار اصلی",
Code = "WH-001",
Address = "تهران - انبار مرکزی فروشگاه",
IsDefault = true,
IsActive = true,
Created = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
CreatedBy = "System",
IsDeleted = false
});
}
}
@@ -0,0 +1,242 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddInventorySystem : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Warehouses",
schema: "CMS",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Code = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Address = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
IsDefault = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Warehouses", x => x.Id);
});
migrationBuilder.CreateTable(
name: "InventoryItems",
schema: "CMS",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ProductId = table.Column<long>(type: "bigint", nullable: true),
DiscountProductId = table.Column<long>(type: "bigint", nullable: true),
ProductType = table.Column<int>(type: "int", nullable: false),
Quantity = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
ReservedQuantity = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
LowStockThreshold = table.Column<int>(type: "int", nullable: false, defaultValue: 10),
ReorderPoint = table.Column<int>(type: "int", nullable: false, defaultValue: 5),
MaxStockLevel = table.Column<int>(type: "int", nullable: false, defaultValue: 1000),
LastRestockedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
LastSoldAt = table.Column<DateTime>(type: "datetime2", nullable: true),
WarehouseId = table.Column<long>(type: "bigint", nullable: false, defaultValue: 1L),
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_InventoryItems", x => x.Id);
table.CheckConstraint("CK_InventoryItem_ProductReference", "(ProductId IS NOT NULL AND DiscountProductId IS NULL) OR (ProductId IS NULL AND DiscountProductId IS NOT NULL)");
table.CheckConstraint("CK_InventoryItem_ProductType_Match", "(ProductType = 1 AND ProductId IS NOT NULL) OR (ProductType = 2 AND DiscountProductId IS NOT NULL)");
table.CheckConstraint("CK_InventoryItem_Quantity_NonNegative", "Quantity >= 0");
table.CheckConstraint("CK_InventoryItem_ReservedQuantity_LessOrEqualQuantity", "ReservedQuantity <= Quantity");
table.CheckConstraint("CK_InventoryItem_ReservedQuantity_NonNegative", "ReservedQuantity >= 0");
table.ForeignKey(
name: "FK_InventoryItems_DiscountProducts_DiscountProductId",
column: x => x.DiscountProductId,
principalSchema: "CMS",
principalTable: "DiscountProducts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_InventoryItems_Products_ProductId",
column: x => x.ProductId,
principalSchema: "CMS",
principalTable: "Products",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_InventoryItems_Warehouses_WarehouseId",
column: x => x.WarehouseId,
principalSchema: "CMS",
principalTable: "Warehouses",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "StockMovements",
schema: "CMS",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
InventoryItemId = table.Column<long>(type: "bigint", nullable: false),
MovementType = table.Column<int>(type: "int", nullable: false),
Quantity = table.Column<int>(type: "int", nullable: false),
QuantityBefore = table.Column<int>(type: "int", nullable: false),
QuantityAfter = table.Column<int>(type: "int", nullable: false),
OrderId = table.Column<long>(type: "bigint", nullable: true),
DiscountOrderId = table.Column<long>(type: "bigint", nullable: true),
ReferenceNumber = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Note = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
PerformedByUserId = table.Column<long>(type: "bigint", nullable: true),
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StockMovements", x => x.Id);
table.CheckConstraint("CK_StockMovement_QuantityAfter_Calculation", "QuantityAfter = QuantityBefore + Quantity");
table.CheckConstraint("CK_StockMovement_QuantityAfter_NonNegative", "QuantityAfter >= 0");
table.CheckConstraint("CK_StockMovement_QuantityBefore_NonNegative", "QuantityBefore >= 0");
table.ForeignKey(
name: "FK_StockMovements_InventoryItems_InventoryItemId",
column: x => x.InventoryItemId,
principalSchema: "CMS",
principalTable: "InventoryItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.InsertData(
schema: "CMS",
table: "Warehouses",
columns: new[] { "Id", "Address", "Code", "Created", "CreatedBy", "IsActive", "IsDefault", "IsDeleted", "LastModified", "LastModifiedBy", "Name" },
values: new object[] { 1L, "تهران - انبار مرکزی فروشگاه", "WH-001", new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), "System", true, true, false, null, null, "انبار اصلی" });
migrationBuilder.CreateIndex(
name: "IX_InventoryItems_DiscountProductId",
schema: "CMS",
table: "InventoryItems",
column: "DiscountProductId");
migrationBuilder.CreateIndex(
name: "IX_InventoryItems_ProductId",
schema: "CMS",
table: "InventoryItems",
column: "ProductId");
migrationBuilder.CreateIndex(
name: "IX_InventoryItems_ProductType_Quantity",
schema: "CMS",
table: "InventoryItems",
columns: new[] { "ProductType", "Quantity" });
migrationBuilder.CreateIndex(
name: "IX_InventoryItems_WarehouseId",
schema: "CMS",
table: "InventoryItems",
column: "WarehouseId");
migrationBuilder.CreateIndex(
name: "IX_StockMovements_Created",
schema: "CMS",
table: "StockMovements",
column: "Created");
migrationBuilder.CreateIndex(
name: "IX_StockMovements_DiscountOrderId",
schema: "CMS",
table: "StockMovements",
column: "DiscountOrderId",
filter: "[DiscountOrderId] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_StockMovements_InventoryItemId",
schema: "CMS",
table: "StockMovements",
column: "InventoryItemId");
migrationBuilder.CreateIndex(
name: "IX_StockMovements_Item_Type_Date",
schema: "CMS",
table: "StockMovements",
columns: new[] { "InventoryItemId", "MovementType", "Created" });
migrationBuilder.CreateIndex(
name: "IX_StockMovements_MovementType",
schema: "CMS",
table: "StockMovements",
column: "MovementType");
migrationBuilder.CreateIndex(
name: "IX_StockMovements_OrderId",
schema: "CMS",
table: "StockMovements",
column: "OrderId",
filter: "[OrderId] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_StockMovements_ReferenceNumber",
schema: "CMS",
table: "StockMovements",
column: "ReferenceNumber",
filter: "[ReferenceNumber] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_Warehouses_Code_Unique",
schema: "CMS",
table: "Warehouses",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Warehouses_IsActive",
schema: "CMS",
table: "Warehouses",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_Warehouses_IsDefault",
schema: "CMS",
table: "Warehouses",
column: "IsDefault",
filter: "[IsDefault] = 1");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StockMovements",
schema: "CMS");
migrationBuilder.DropTable(
name: "InventoryItems",
schema: "CMS");
migrationBuilder.DropTable(
name: "Warehouses",
schema: "CMS");
}
}
}
@@ -1503,6 +1503,102 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.ToTable("NetworkMembershipHistories", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.InventoryItem", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<long?>("DiscountProductId")
.HasColumnType("bigint");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTime?>("LastModified")
.HasColumnType("datetime2");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("LastRestockedAt")
.HasColumnType("datetime2");
b.Property<DateTime?>("LastSoldAt")
.HasColumnType("datetime2");
b.Property<int>("LowStockThreshold")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(10);
b.Property<int>("MaxStockLevel")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(1000);
b.Property<long?>("ProductId")
.HasColumnType("bigint");
b.Property<int>("ProductType")
.HasColumnType("int");
b.Property<int>("Quantity")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<int>("ReorderPoint")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(5);
b.Property<int>("ReservedQuantity")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<long>("WarehouseId")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasDefaultValue(1L);
b.HasKey("Id");
b.HasIndex("DiscountProductId")
.HasDatabaseName("IX_InventoryItems_DiscountProductId");
b.HasIndex("ProductId")
.HasDatabaseName("IX_InventoryItems_ProductId");
b.HasIndex("WarehouseId")
.HasDatabaseName("IX_InventoryItems_WarehouseId");
b.HasIndex("ProductType", "Quantity")
.HasDatabaseName("IX_InventoryItems_ProductType_Quantity");
b.ToTable("InventoryItems", "CMS", t =>
{
t.HasCheckConstraint("CK_InventoryItem_ProductReference", "(ProductId IS NOT NULL AND DiscountProductId IS NULL) OR (ProductId IS NULL AND DiscountProductId IS NOT NULL)");
t.HasCheckConstraint("CK_InventoryItem_ProductType_Match", "(ProductType = 1 AND ProductId IS NOT NULL) OR (ProductType = 2 AND DiscountProductId IS NOT NULL)");
t.HasCheckConstraint("CK_InventoryItem_Quantity_NonNegative", "Quantity >= 0");
t.HasCheckConstraint("CK_InventoryItem_ReservedQuantity_LessOrEqualQuantity", "ReservedQuantity <= Quantity");
t.HasCheckConstraint("CK_InventoryItem_ReservedQuantity_NonNegative", "ReservedQuantity >= 0");
});
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Network.NetworkWeeklyBalance", b =>
{
b.Property<long>("Id")
@@ -2210,6 +2306,97 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.ToTable("Roles", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.StockMovement", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<long?>("DiscountOrderId")
.HasColumnType("bigint");
b.Property<long>("InventoryItemId")
.HasColumnType("bigint");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTime?>("LastModified")
.HasColumnType("datetime2");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<int>("MovementType")
.HasColumnType("int");
b.Property<string>("Note")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<long?>("OrderId")
.HasColumnType("bigint");
b.Property<long?>("PerformedByUserId")
.HasColumnType("bigint");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("QuantityAfter")
.HasColumnType("int");
b.Property<int>("QuantityBefore")
.HasColumnType("int");
b.Property<string>("ReferenceNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.HasKey("Id");
b.HasIndex("Created")
.HasDatabaseName("IX_StockMovements_Created");
b.HasIndex("DiscountOrderId")
.HasDatabaseName("IX_StockMovements_DiscountOrderId")
.HasFilter("[DiscountOrderId] IS NOT NULL");
b.HasIndex("InventoryItemId")
.HasDatabaseName("IX_StockMovements_InventoryItemId");
b.HasIndex("MovementType")
.HasDatabaseName("IX_StockMovements_MovementType");
b.HasIndex("OrderId")
.HasDatabaseName("IX_StockMovements_OrderId")
.HasFilter("[OrderId] IS NOT NULL");
b.HasIndex("ReferenceNumber")
.HasDatabaseName("IX_StockMovements_ReferenceNumber")
.HasFilter("[ReferenceNumber] IS NOT NULL");
b.HasIndex("InventoryItemId", "MovementType", "Created")
.HasDatabaseName("IX_StockMovements_Item_Type_Date");
b.ToTable("StockMovements", "CMS", t =>
{
t.HasCheckConstraint("CK_StockMovement_QuantityAfter_Calculation", "QuantityAfter = QuantityBefore + Quantity");
t.HasCheckConstraint("CK_StockMovement_QuantityAfter_NonNegative", "QuantityAfter >= 0");
t.HasCheckConstraint("CK_StockMovement_QuantityBefore_NonNegative", "QuantityBefore >= 0");
});
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Tag", b =>
{
b.Property<long>("Id")
@@ -2818,6 +3005,83 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.ToTable("UserWalletChangeLogs", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Warehouse", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Address")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.Property<bool>("IsDefault")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTime?>("LastModified")
.HasColumnType("datetime2");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique()
.HasDatabaseName("IX_Warehouses_Code_Unique");
b.HasIndex("IsActive")
.HasDatabaseName("IX_Warehouses_IsActive");
b.HasIndex("IsDefault")
.HasDatabaseName("IX_Warehouses_IsDefault")
.HasFilter("[IsDefault] = 1");
b.ToTable("Warehouses", "CMS");
b.HasData(
new
{
Id = 1L,
Address = "تهران - انبار مرکزی فروشگاه",
Code = "WH-001",
Created = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
CreatedBy = "System",
IsActive = true,
IsDefault = true,
IsDeleted = false,
Name = "انبار اصلی"
});
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.WeekDefinition", b =>
{
b.Property<long>("Id")
@@ -3185,6 +3449,31 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("WeekDefinition");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.InventoryItem", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.DiscountShop.DiscountProduct", "DiscountProduct")
.WithMany()
.HasForeignKey("DiscountProductId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("CMSMicroservice.Domain.Entities.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("CMSMicroservice.Domain.Entities.Warehouse", "Warehouse")
.WithMany("InventoryItems")
.HasForeignKey("WarehouseId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("DiscountProduct");
b.Navigation("Product");
b.Navigation("Warehouse");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Network.NetworkWeeklyBalance", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.User", "User")
@@ -3290,6 +3579,17 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("Tag");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.StockMovement", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.InventoryItem", "InventoryItem")
.WithMany("StockMovements")
.HasForeignKey("InventoryItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("InventoryItem");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.User", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.User", "NetworkParent")
@@ -3527,6 +3827,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("Cities");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.InventoryItem", b =>
{
b.Navigation("StockMovements");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Package", b =>
{
b.Navigation("UserOrders");
@@ -3611,6 +3916,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("UserWalletChangeLogs");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Warehouse", b =>
{
b.Navigation("InventoryItems");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.WeekDefinition", b =>
{
b.Navigation("CommissionPayoutHistories");
@@ -0,0 +1,454 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Infrastructure.Persistence.Repositories;
/// <summary>
/// Repository implementation برای مدیریت موجودی محصولات
/// </summary>
public class InventoryItemRepository : IInventoryItemRepository
{
private readonly IApplicationDbContext _context;
public InventoryItemRepository(IApplicationDbContext context)
{
_context = context;
}
#region Read Operations
public async Task<InventoryItem?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
{
return await _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Include(i => i.Warehouse)
.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
}
public async Task<InventoryItem?> GetByProductIdAsync(long productId, long warehouseId = 1, CancellationToken cancellationToken = default)
{
return await _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.Warehouse)
.FirstOrDefaultAsync(i => i.ProductId == productId && i.WarehouseId == warehouseId, cancellationToken);
}
public async Task<InventoryItem?> GetByDiscountProductIdAsync(long discountProductId, long warehouseId = 1, CancellationToken cancellationToken = default)
{
return await _context.InventoryItems
.Include(i => i.DiscountProduct)
.Include(i => i.Warehouse)
.FirstOrDefaultAsync(i => i.DiscountProductId == discountProductId && i.WarehouseId == warehouseId, cancellationToken);
}
public async Task<List<InventoryItem>> GetByWarehouseIdAsync(long warehouseId, CancellationToken cancellationToken = default)
{
return await _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Where(i => i.WarehouseId == warehouseId)
.OrderBy(i => i.Product != null ? i.Product.Title : i.DiscountProduct != null ? i.DiscountProduct.Title : "")
.ToListAsync(cancellationToken);
}
public async Task<List<InventoryItem>> GetLowStockItemsAsync(ProductType? productType = null, long warehouseId = 1, CancellationToken cancellationToken = default)
{
var query = _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Where(i => i.WarehouseId == warehouseId && i.Quantity <= i.LowStockThreshold && i.Quantity > 0);
if (productType.HasValue)
{
query = query.Where(i => i.ProductType == productType.Value);
}
return await query
.OrderBy(i => i.Quantity)
.ToListAsync(cancellationToken);
}
public async Task<List<InventoryItem>> GetOutOfStockItemsAsync(ProductType? productType = null, long warehouseId = 1, CancellationToken cancellationToken = default)
{
var query = _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Where(i => i.WarehouseId == warehouseId && i.Quantity == 0);
if (productType.HasValue)
{
query = query.Where(i => i.ProductType == productType.Value);
}
return await query
.OrderBy(i => i.Product != null ? i.Product.Title : i.DiscountProduct != null ? i.DiscountProduct.Title : "")
.ToListAsync(cancellationToken);
}
public async Task<List<InventoryItem>> SearchAsync(
string? searchTerm = null,
ProductType? productType = null,
long? warehouseId = null,
int? minQuantity = null,
int? maxQuantity = null,
int skip = 0,
int take = 50,
CancellationToken cancellationToken = default)
{
var query = _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Include(i => i.Warehouse)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var term = searchTerm.ToLower();
query = query.Where(i =>
(i.Product != null && i.Product.Title.ToLower().Contains(term)) ||
(i.DiscountProduct != null && i.DiscountProduct.Title.ToLower().Contains(term)));
}
if (productType.HasValue)
{
query = query.Where(i => i.ProductType == productType.Value);
}
if (warehouseId.HasValue)
{
query = query.Where(i => i.WarehouseId == warehouseId.Value);
}
if (minQuantity.HasValue)
{
query = query.Where(i => i.Quantity >= minQuantity.Value);
}
if (maxQuantity.HasValue)
{
query = query.Where(i => i.Quantity <= maxQuantity.Value);
}
return await query
.OrderBy(i => i.Product != null ? i.Product.Title : i.DiscountProduct != null ? i.DiscountProduct.Title : "")
.Skip(skip)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<int> CountAsync(
string? searchTerm = null,
ProductType? productType = null,
long? warehouseId = null,
int? minQuantity = null,
int? maxQuantity = null,
CancellationToken cancellationToken = default)
{
var query = _context.InventoryItems.AsQueryable();
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var term = searchTerm.ToLower();
query = query.Where(i =>
(i.Product != null && i.Product.Title.ToLower().Contains(term)) ||
(i.DiscountProduct != null && i.DiscountProduct.Title.ToLower().Contains(term)));
}
if (productType.HasValue)
{
query = query.Where(i => i.ProductType == productType.Value);
}
if (warehouseId.HasValue)
{
query = query.Where(i => i.WarehouseId == warehouseId.Value);
}
if (minQuantity.HasValue)
{
query = query.Where(i => i.Quantity >= minQuantity.Value);
}
if (maxQuantity.HasValue)
{
query = query.Where(i => i.Quantity <= maxQuantity.Value);
}
return await query.CountAsync(cancellationToken);
}
#endregion
#region Write Operations
public async Task<InventoryItem> AddAsync(InventoryItem inventoryItem, CancellationToken cancellationToken = default)
{
_context.InventoryItems.Add(inventoryItem);
await _context.SaveChangesAsync(cancellationToken);
return inventoryItem;
}
public async Task UpdateAsync(InventoryItem inventoryItem, CancellationToken cancellationToken = default)
{
_context.InventoryItems.Update(inventoryItem);
await _context.SaveChangesAsync(cancellationToken);
}
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
{
var item = await _context.InventoryItems.FindAsync(new object[] { id }, cancellationToken);
if (item != null)
{
_context.InventoryItems.Remove(item);
await _context.SaveChangesAsync(cancellationToken);
}
}
public async Task<bool> UpdateQuantityAsync(
long inventoryItemId,
int quantityChange,
StockMovementType movementType,
string? note = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default)
{
var item = await _context.InventoryItems.FindAsync(new object[] { inventoryItemId }, cancellationToken);
if (item == null) return false;
// بررسی اینکه موجودی کافی برای کاهش موجود باشد
if (quantityChange < 0 && item.Quantity + quantityChange < 0)
{
return false;
}
// بروزرسانی موجودی
item.Quantity += quantityChange;
// بروزرسانی تاریخ آخرین فعالیت
if (movementType == StockMovementType.Sale)
{
item.LastSoldAt = DateTime.UtcNow;
}
else if (movementType == StockMovementType.Restock || movementType == StockMovementType.InitialStock)
{
item.LastRestockedAt = DateTime.UtcNow;
}
// ثبت حرکت موجودی
var stockMovement = new StockMovement
{
InventoryItemId = inventoryItemId,
MovementType = movementType,
Quantity = Math.Abs(quantityChange),
Note = note,
ReferenceNumber = referenceNumber,
OrderId = orderId,
DiscountOrderId = discountOrderId,
PerformedByUserId = performedByUserId
};
_context.StockMovements.Add(stockMovement);
await _context.SaveChangesAsync(cancellationToken);
return true;
}
public async Task<bool> ReserveQuantityAsync(
long inventoryItemId,
int quantity,
string? note = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default)
{
var item = await _context.InventoryItems.FindAsync(new object[] { inventoryItemId }, cancellationToken);
if (item == null) return false;
// بررسی موجودی قابل دسترس
if (item.AvailableQuantity < quantity)
{
return false;
}
// رزرو موجودی
item.ReservedQuantity += quantity;
// ثبت حرکت رزرو
var stockMovement = new StockMovement
{
InventoryItemId = inventoryItemId,
MovementType = StockMovementType.Reserved,
Quantity = quantity,
Note = note ?? "Quantity reserved",
ReferenceNumber = referenceNumber,
OrderId = orderId,
DiscountOrderId = discountOrderId,
PerformedByUserId = performedByUserId
};
_context.StockMovements.Add(stockMovement);
await _context.SaveChangesAsync(cancellationToken);
return true;
}
public async Task<bool> ReleaseReservedQuantityAsync(
long inventoryItemId,
int quantity,
string? note = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default)
{
var item = await _context.InventoryItems.FindAsync(new object[] { inventoryItemId }, cancellationToken);
if (item == null) return false;
// بررسی اینکه مقدار رزرو شده کافی باشد
if (item.ReservedQuantity < quantity)
{
return false;
}
// آزاد کردن رزرو
item.ReservedQuantity -= quantity;
// ثبت حرکت آزادسازی
var stockMovement = new StockMovement
{
InventoryItemId = inventoryItemId,
MovementType = StockMovementType.Released,
Quantity = quantity,
Note = note ?? "Reserved quantity released",
ReferenceNumber = referenceNumber,
OrderId = orderId,
DiscountOrderId = discountOrderId,
PerformedByUserId = performedByUserId
};
_context.StockMovements.Add(stockMovement);
await _context.SaveChangesAsync(cancellationToken);
return true;
}
#endregion
#region Bulk Operations
public async Task<bool> BulkUpdateQuantityAsync(
List<(long InventoryItemId, int QuantityChange, string? Note)> updates,
StockMovementType movementType,
string? referenceNumber = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default)
{
var inventoryItemIds = updates.Select(u => u.InventoryItemId).ToList();
var items = await _context.InventoryItems
.Where(i => inventoryItemIds.Contains(i.Id))
.ToListAsync(cancellationToken);
if (items.Count != updates.Count)
{
return false; // برخی آیتم‌ها پیدا نشدند
}
var stockMovements = new List<StockMovement>();
foreach (var update in updates)
{
var item = items.First(i => i.Id == update.InventoryItemId);
// بررسی موجودی کافی
if (update.QuantityChange < 0 && item.Quantity + update.QuantityChange < 0)
{
return false;
}
item.Quantity += update.QuantityChange;
if (movementType == StockMovementType.Sale)
{
item.LastSoldAt = DateTime.UtcNow;
}
else if (movementType == StockMovementType.Restock || movementType == StockMovementType.InitialStock)
{
item.LastRestockedAt = DateTime.UtcNow;
}
stockMovements.Add(new StockMovement
{
InventoryItemId = update.InventoryItemId,
MovementType = movementType,
Quantity = Math.Abs(update.QuantityChange),
Note = update.Note,
ReferenceNumber = referenceNumber,
PerformedByUserId = performedByUserId
});
}
_context.StockMovements.AddRange(stockMovements);
await _context.SaveChangesAsync(cancellationToken);
return true;
}
public async Task<bool> BulkReserveQuantityAsync(
List<(long InventoryItemId, int Quantity, string? Note)> reservations,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default)
{
var inventoryItemIds = reservations.Select(r => r.InventoryItemId).ToList();
var items = await _context.InventoryItems
.Where(i => inventoryItemIds.Contains(i.Id))
.ToListAsync(cancellationToken);
if (items.Count != reservations.Count)
{
return false;
}
var stockMovements = new List<StockMovement>();
foreach (var reservation in reservations)
{
var item = items.First(i => i.Id == reservation.InventoryItemId);
// بررسی موجودی قابل دسترس
if (item.AvailableQuantity < reservation.Quantity)
{
return false;
}
item.ReservedQuantity += reservation.Quantity;
stockMovements.Add(new StockMovement
{
InventoryItemId = reservation.InventoryItemId,
MovementType = StockMovementType.Reserved,
Quantity = reservation.Quantity,
Note = reservation.Note ?? "Bulk reservation",
ReferenceNumber = referenceNumber,
OrderId = orderId,
DiscountOrderId = discountOrderId,
PerformedByUserId = performedByUserId
});
}
_context.StockMovements.AddRange(stockMovements);
await _context.SaveChangesAsync(cancellationToken);
return true;
}
#endregion
}
@@ -0,0 +1,430 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Infrastructure.Persistence.Repositories;
/// <summary>
/// Repository implementation برای مدیریت حرکات موجودی
/// </summary>
public class StockMovementRepository : IStockMovementRepository
{
private readonly IApplicationDbContext _context;
public StockMovementRepository(IApplicationDbContext context)
{
_context = context;
}
#region Read Operations
public async Task<StockMovement?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
{
return await _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.FirstOrDefaultAsync(m => m.Id == id, cancellationToken);
}
public async Task<List<StockMovement>> GetByInventoryItemIdAsync(
long inventoryItemId,
StockMovementType? movementType = null,
DateTime? fromDate = null,
DateTime? toDate = null,
int skip = 0,
int take = 100,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Include(m => m.InventoryItem)
.Where(m => m.InventoryItemId == inventoryItemId);
if (movementType.HasValue)
{
query = query.Where(m => m.MovementType == movementType.Value);
}
if (fromDate.HasValue)
{
query = query.Where(m => m.Created >= fromDate.Value);
}
if (toDate.HasValue)
{
query = query.Where(m => m.Created <= toDate.Value);
}
return await query
.OrderByDescending(m => m.Created)
.Skip(skip)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<List<StockMovement>> GetByOrderIdAsync(long orderId, CancellationToken cancellationToken = default)
{
return await _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.Where(m => m.OrderId == orderId)
.OrderByDescending(m => m.Created)
.ToListAsync(cancellationToken);
}
public async Task<List<StockMovement>> GetByDiscountOrderIdAsync(long discountOrderId, CancellationToken cancellationToken = default)
{
return await _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.Where(m => m.DiscountOrderId == discountOrderId)
.OrderByDescending(m => m.Created)
.ToListAsync(cancellationToken);
}
public async Task<List<StockMovement>> GetByReferenceNumberAsync(string referenceNumber, CancellationToken cancellationToken = default)
{
return await _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.Where(m => m.ReferenceNumber == referenceNumber)
.OrderByDescending(m => m.Created)
.ToListAsync(cancellationToken);
}
public async Task<List<StockMovement>> GetByMovementTypeAsync(
StockMovementType movementType,
DateTime? fromDate = null,
DateTime? toDate = null,
int skip = 0,
int take = 100,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.Where(m => m.MovementType == movementType);
if (fromDate.HasValue)
{
query = query.Where(m => m.Created >= fromDate.Value);
}
if (toDate.HasValue)
{
query = query.Where(m => m.Created <= toDate.Value);
}
return await query
.OrderByDescending(m => m.Created)
.Skip(skip)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<List<StockMovement>> GetRecentMovementsAsync(
int count = 50,
StockMovementType? movementType = null,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.AsQueryable();
if (movementType.HasValue)
{
query = query.Where(m => m.MovementType == movementType.Value);
}
return await query
.OrderByDescending(m => m.Created)
.Take(count)
.ToListAsync(cancellationToken);
}
public async Task<List<StockMovement>> SearchAsync(
long? inventoryItemId = null,
StockMovementType? movementType = null,
DateTime? fromDate = null,
DateTime? toDate = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
int skip = 0,
int take = 100,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.AsQueryable();
if (inventoryItemId.HasValue)
{
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
}
if (movementType.HasValue)
{
query = query.Where(m => m.MovementType == movementType.Value);
}
if (fromDate.HasValue)
{
query = query.Where(m => m.Created >= fromDate.Value);
}
if (toDate.HasValue)
{
query = query.Where(m => m.Created <= toDate.Value);
}
if (!string.IsNullOrWhiteSpace(referenceNumber))
{
query = query.Where(m => m.ReferenceNumber != null && m.ReferenceNumber.Contains(referenceNumber));
}
if (orderId.HasValue)
{
query = query.Where(m => m.OrderId == orderId.Value);
}
if (discountOrderId.HasValue)
{
query = query.Where(m => m.DiscountOrderId == discountOrderId.Value);
}
if (performedByUserId.HasValue)
{
query = query.Where(m => m.PerformedByUserId == performedByUserId.Value);
}
return await query
.OrderByDescending(m => m.Created)
.Skip(skip)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<int> CountAsync(
long? inventoryItemId = null,
StockMovementType? movementType = null,
DateTime? fromDate = null,
DateTime? toDate = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements.AsQueryable();
if (inventoryItemId.HasValue)
{
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
}
if (movementType.HasValue)
{
query = query.Where(m => m.MovementType == movementType.Value);
}
if (fromDate.HasValue)
{
query = query.Where(m => m.Created >= fromDate.Value);
}
if (toDate.HasValue)
{
query = query.Where(m => m.Created <= toDate.Value);
}
if (!string.IsNullOrWhiteSpace(referenceNumber))
{
query = query.Where(m => m.ReferenceNumber != null && m.ReferenceNumber.Contains(referenceNumber));
}
if (orderId.HasValue)
{
query = query.Where(m => m.OrderId == orderId.Value);
}
if (discountOrderId.HasValue)
{
query = query.Where(m => m.DiscountOrderId == discountOrderId.Value);
}
if (performedByUserId.HasValue)
{
query = query.Where(m => m.PerformedByUserId == performedByUserId.Value);
}
return await query.CountAsync(cancellationToken);
}
#endregion
#region Write Operations
public async Task<StockMovement> AddAsync(StockMovement stockMovement, CancellationToken cancellationToken = default)
{
_context.StockMovements.Add(stockMovement);
await _context.SaveChangesAsync(cancellationToken);
return stockMovement;
}
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
{
var stockMovement = await _context.StockMovements.FindAsync(new object[] { id }, cancellationToken);
if (stockMovement != null)
{
_context.StockMovements.Remove(stockMovement);
await _context.SaveChangesAsync(cancellationToken);
}
}
public async Task<List<StockMovement>> BulkAddAsync(List<StockMovement> stockMovements, CancellationToken cancellationToken = default)
{
_context.StockMovements.AddRange(stockMovements);
await _context.SaveChangesAsync(cancellationToken);
return stockMovements;
}
#endregion
#region Analytics & Reports
public async Task<Dictionary<StockMovementType, int>> GetMovementSummaryAsync(
DateTime fromDate,
DateTime toDate,
long? inventoryItemId = null,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Where(m => m.Created >= fromDate && m.Created <= toDate);
if (inventoryItemId.HasValue)
{
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
}
var movements = await query
.GroupBy(m => m.MovementType)
.Select(g => new { MovementType = g.Key, TotalQuantity = g.Sum(m => m.Quantity) })
.ToListAsync(cancellationToken);
return movements.ToDictionary(x => x.MovementType, x => x.TotalQuantity);
}
public async Task<List<(DateTime Date, int InboundQuantity, int OutboundQuantity)>> GetDailyMovementVolumeAsync(
DateTime fromDate,
DateTime toDate,
long? inventoryItemId = null,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Where(m => m.Created >= fromDate && m.Created <= toDate);
if (inventoryItemId.HasValue)
{
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
}
var movements = await query.ToListAsync(cancellationToken);
// نوع‌های ورودی (افزایش موجودی)
var inboundTypes = new[]
{
StockMovementType.InitialStock,
StockMovementType.Restock,
StockMovementType.Return,
StockMovementType.TransferIn,
StockMovementType.AdjustmentPlus,
StockMovementType.Released
};
// نوع‌های خروجی (کاهش موجودی)
var outboundTypes = new[]
{
StockMovementType.Sale,
StockMovementType.Damaged,
StockMovementType.Lost,
StockMovementType.TransferOut,
StockMovementType.AdjustmentMinus,
StockMovementType.Reserved
};
var dailyVolumes = movements
.GroupBy(m => m.Created.Date)
.Select(g => (
Date: g.Key,
InboundQuantity: g.Where(m => inboundTypes.Contains(m.MovementType)).Sum(m => m.Quantity),
OutboundQuantity: g.Where(m => outboundTypes.Contains(m.MovementType)).Sum(m => m.Quantity)
))
.OrderBy(x => x.Date)
.ToList();
return dailyVolumes;
}
public async Task<List<(long InventoryItemId, string ProductName, int MovementCount, int TotalQuantityChange)>> GetTopMovingProductsAsync(
DateTime fromDate,
DateTime toDate,
int count = 10,
StockMovementType? movementType = null,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.Where(m => m.Created >= fromDate && m.Created <= toDate);
if (movementType.HasValue)
{
query = query.Where(m => m.MovementType == movementType.Value);
}
var movements = await query.ToListAsync(cancellationToken);
var topProducts = movements
.GroupBy(m => m.InventoryItemId)
.Select(g =>
{
var firstItem = g.First().InventoryItem;
var productName = firstItem.Product?.Title ?? firstItem.DiscountProduct?.Title ?? "Unknown";
return (
InventoryItemId: g.Key,
ProductName: productName,
MovementCount: g.Count(),
TotalQuantityChange: g.Sum(m => m.Quantity)
);
})
.OrderByDescending(x => x.MovementCount)
.Take(count)
.ToList();
return topProducts;
}
#endregion
}
@@ -0,0 +1,309 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Infrastructure.Persistence.Repositories;
/// <summary>
/// Repository implementation برای مدیریت انبارها
/// </summary>
public class WarehouseRepository : IWarehouseRepository
{
private readonly IApplicationDbContext _context;
public WarehouseRepository(IApplicationDbContext context)
{
_context = context;
}
#region Read Operations
public async Task<Warehouse?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
{
return await _context.Warehouses
.Include(w => w.InventoryItems)
.ThenInclude(i => i.Product)
.Include(w => w.InventoryItems)
.ThenInclude(i => i.DiscountProduct)
.FirstOrDefaultAsync(w => w.Id == id, cancellationToken);
}
public async Task<Warehouse?> GetByCodeAsync(string code, CancellationToken cancellationToken = default)
{
return await _context.Warehouses
.Include(w => w.InventoryItems)
.ThenInclude(i => i.Product)
.Include(w => w.InventoryItems)
.ThenInclude(i => i.DiscountProduct)
.FirstOrDefaultAsync(w => w.Code == code, cancellationToken);
}
public async Task<Warehouse?> GetDefaultWarehouseAsync(CancellationToken cancellationToken = default)
{
return await _context.Warehouses
.Include(w => w.InventoryItems)
.ThenInclude(i => i.Product)
.Include(w => w.InventoryItems)
.ThenInclude(i => i.DiscountProduct)
.FirstOrDefaultAsync(w => w.IsDefault, cancellationToken);
}
public async Task<List<Warehouse>> GetActiveWarehousesAsync(CancellationToken cancellationToken = default)
{
return await _context.Warehouses
.Where(w => w.IsActive)
.OrderBy(w => w.Name)
.ToListAsync(cancellationToken);
}
public async Task<List<Warehouse>> GetAllAsync(
bool includeInactive = false,
CancellationToken cancellationToken = default)
{
var query = _context.Warehouses.AsQueryable();
if (!includeInactive)
{
query = query.Where(w => w.IsActive);
}
return await query
.OrderBy(w => w.Name)
.ToListAsync(cancellationToken);
}
public async Task<List<Warehouse>> SearchAsync(
string? searchTerm = null,
bool? isActive = null,
int skip = 0,
int take = 50,
CancellationToken cancellationToken = default)
{
var query = _context.Warehouses.AsQueryable();
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var term = searchTerm.ToLower();
query = query.Where(w =>
w.Name.ToLower().Contains(term) ||
w.Code.ToLower().Contains(term) ||
(w.Address != null && w.Address.ToLower().Contains(term)));
}
if (isActive.HasValue)
{
query = query.Where(w => w.IsActive == isActive.Value);
}
return await query
.OrderBy(w => w.Name)
.Skip(skip)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<int> CountAsync(
string? searchTerm = null,
bool? isActive = null,
CancellationToken cancellationToken = default)
{
var query = _context.Warehouses.AsQueryable();
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var term = searchTerm.ToLower();
query = query.Where(w =>
w.Name.ToLower().Contains(term) ||
w.Code.ToLower().Contains(term) ||
(w.Address != null && w.Address.ToLower().Contains(term)));
}
if (isActive.HasValue)
{
query = query.Where(w => w.IsActive == isActive.Value);
}
return await query.CountAsync(cancellationToken);
}
public async Task<bool> ExistsByCodeAsync(string code, long? excludeId = null, CancellationToken cancellationToken = default)
{
var query = _context.Warehouses.Where(w => w.Code == code);
if (excludeId.HasValue)
{
query = query.Where(w => w.Id != excludeId.Value);
}
return await query.AnyAsync(cancellationToken);
}
#endregion
#region Write Operations
public async Task<Warehouse> AddAsync(Warehouse warehouse, CancellationToken cancellationToken = default)
{
// اگر این انبار پیش‌فرض است، سایر انبارها را غیرپیش‌فرض کن
if (warehouse.IsDefault)
{
await RemoveDefaultFromAllWarehousesAsync(cancellationToken);
}
_context.Warehouses.Add(warehouse);
await _context.SaveChangesAsync(cancellationToken);
return warehouse;
}
public async Task UpdateAsync(Warehouse warehouse, CancellationToken cancellationToken = default)
{
// اگر این انبار پیش‌فرض شده، سایر انبارها را غیرپیش‌فرض کن
if (warehouse.IsDefault)
{
await RemoveDefaultFromAllWarehousesAsync(warehouse.Id, cancellationToken);
}
_context.Warehouses.Update(warehouse);
await _context.SaveChangesAsync(cancellationToken);
}
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
{
var warehouse = await _context.Warehouses.FindAsync(new object[] { id }, cancellationToken);
if (warehouse != null)
{
warehouse.IsActive = false;
await _context.SaveChangesAsync(cancellationToken);
}
}
public async Task SetActiveStatusAsync(long id, bool isActive, CancellationToken cancellationToken = default)
{
var warehouse = await _context.Warehouses.FindAsync(new object[] { id }, cancellationToken);
if (warehouse != null)
{
warehouse.IsActive = isActive;
await _context.SaveChangesAsync(cancellationToken);
}
}
public async Task SetAsDefaultAsync(long id, CancellationToken cancellationToken = default)
{
// ابتدا همه انبارها را غیرپیش‌فرض کن
await RemoveDefaultFromAllWarehousesAsync(cancellationToken);
// سپس انبار مورد نظر را پیش‌فرض کن
var warehouse = await _context.Warehouses.FindAsync(new object[] { id }, cancellationToken);
if (warehouse != null)
{
warehouse.IsDefault = true;
await _context.SaveChangesAsync(cancellationToken);
}
}
#endregion
#region Analytics
public async Task<(int TotalProducts, int LowStockProducts, int OutOfStockProducts, decimal TotalValue)> GetWarehouseStatisticsAsync(
long warehouseId,
CancellationToken cancellationToken = default)
{
var inventoryItems = await _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Where(i => i.WarehouseId == warehouseId)
.ToListAsync(cancellationToken);
var totalProducts = inventoryItems.Count;
var lowStockProducts = inventoryItems.Count(i => i.Quantity <= i.LowStockThreshold && i.Quantity > 0);
var outOfStockProducts = inventoryItems.Count(i => i.Quantity == 0);
// محاسبه ارزش کل بر اساس قیمت محصولات
decimal totalValue = 0;
foreach (var item in inventoryItems)
{
if (item.Product != null)
{
totalValue += item.Quantity * item.Product.Price;
}
else if (item.DiscountProduct != null)
{
totalValue += item.Quantity * item.DiscountProduct.Price;
}
}
return (totalProducts, lowStockProducts, outOfStockProducts, totalValue);
}
public async Task<List<(long ProductId, string ProductName, int TotalSold, int CurrentStock)>> GetTopSellingProductsAsync(
long warehouseId,
DateTime fromDate,
DateTime toDate,
int count = 10,
CancellationToken cancellationToken = default)
{
// دریافت حرکات فروش برای این انبار در بازه زمانی مشخص
var salesMovements = await _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.Where(m => m.InventoryItem.WarehouseId == warehouseId &&
m.MovementType == StockMovementType.Sale &&
m.Created >= fromDate &&
m.Created <= toDate)
.ToListAsync(cancellationToken);
// گروه‌بندی بر اساس محصول و محاسبه تعداد فروش
var topProducts = salesMovements
.GroupBy(m => m.InventoryItemId)
.Select(g =>
{
var firstItem = g.First().InventoryItem;
var productId = firstItem.ProductId ?? firstItem.DiscountProductId ?? 0;
var productName = firstItem.Product?.Title ?? firstItem.DiscountProduct?.Title ?? "Unknown";
var totalSold = g.Sum(m => m.Quantity);
var currentStock = firstItem.Quantity;
return (ProductId: productId, ProductName: productName, TotalSold: totalSold, CurrentStock: currentStock);
})
.OrderByDescending(x => x.TotalSold)
.Take(count)
.ToList();
return topProducts;
}
#endregion
#region Private Methods
private async Task RemoveDefaultFromAllWarehousesAsync(CancellationToken cancellationToken = default)
{
var defaultWarehouses = await _context.Warehouses
.Where(w => w.IsDefault)
.ToListAsync(cancellationToken);
foreach (var warehouse in defaultWarehouses)
{
warehouse.IsDefault = false;
}
}
private async Task RemoveDefaultFromAllWarehousesAsync(long excludeId, CancellationToken cancellationToken = default)
{
var defaultWarehouses = await _context.Warehouses
.Where(w => w.IsDefault && w.Id != excludeId)
.ToListAsync(cancellationToken);
foreach (var warehouse in defaultWarehouses)
{
warehouse.IsDefault = false;
}
}
#endregion
}
@@ -0,0 +1,662 @@
using System.Collections.Generic;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Infrastructure.Services;
/// <summary>
/// پیاده‌سازی سرویس مدیریت موجودی
/// این سرویس Source of Truth برای موجودی است و مسئول همگام‌سازی با Product.RemainingCount
/// </summary>
public class InventoryService : IInventoryService
{
private readonly ApplicationDbContext _context;
private readonly ILogger<InventoryService> _logger;
private const long DefaultWarehouseId = 1; // انبار پیش‌فرض
public InventoryService(ApplicationDbContext context, ILogger<InventoryService> logger)
{
_context = context;
_logger = logger;
}
#region Initialization
public async Task<long> InitializeInventoryAsync(
long productId,
ProductType productType,
int initialQuantity,
long? warehouseId = null,
int lowStockThreshold = 10,
CancellationToken ct = default)
{
var effectiveWarehouseId = warehouseId ?? DefaultWarehouseId;
// چک کردن اینکه آیا قبلاً InventoryItem برای این محصول وجود دارد
var existingItem = productType == ProductType.RegularProduct
? await _context.InventoryItems.FirstOrDefaultAsync(
x => x.ProductId == productId && x.WarehouseId == effectiveWarehouseId && !x.IsDeleted, ct)
: await _context.InventoryItems.FirstOrDefaultAsync(
x => x.DiscountProductId == productId && x.WarehouseId == effectiveWarehouseId && !x.IsDeleted, ct);
if (existingItem != null)
{
_logger.LogWarning("InventoryItem already exists for {ProductType} with Id {ProductId}",
productType, productId);
return existingItem.Id;
}
// ایجاد InventoryItem جدید
var inventoryItem = new InventoryItem
{
ProductId = productType == ProductType.RegularProduct ? productId : null,
DiscountProductId = productType == ProductType.DiscountProduct ? productId : null,
ProductType = productType,
Quantity = initialQuantity,
ReservedQuantity = 0,
LowStockThreshold = lowStockThreshold,
WarehouseId = effectiveWarehouseId
};
_context.InventoryItems.Add(inventoryItem);
await _context.SaveChangesAsync(ct);
// ثبت StockMovement اولیه
if (initialQuantity > 0)
{
await LogMovementAsync(
inventoryItem.Id,
StockMovementType.InitialStock,
initialQuantity,
0,
initialQuantity,
null,
null,
null,
"موجودی اولیه",
null,
ct);
}
// همگام‌سازی با Product.RemainingCount
await SyncRemainingCountAsync(inventoryItem, ct);
_logger.LogInformation(
"Initialized inventory for {ProductType} Id={ProductId}, Quantity={Quantity}",
productType, productId, initialQuantity);
return inventoryItem.Id;
}
#endregion
#region Query Operations
public async Task<InventoryItem?> GetInventoryAsync(
long productId,
ProductType productType,
long? warehouseId = null,
CancellationToken ct = default)
{
var effectiveWarehouseId = warehouseId ?? DefaultWarehouseId;
return productType == ProductType.RegularProduct
? await _context.InventoryItems.FirstOrDefaultAsync(
x => x.ProductId == productId && x.WarehouseId == effectiveWarehouseId && !x.IsDeleted, ct)
: await _context.InventoryItems.FirstOrDefaultAsync(
x => x.DiscountProductId == productId && x.WarehouseId == effectiveWarehouseId && !x.IsDeleted, ct);
}
public async Task<int> GetAvailableQuantityAsync(
long productId,
ProductType productType,
long? warehouseId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, warehouseId, ct);
return item?.AvailableQuantity ?? 0;
}
public async Task<bool> CheckAvailabilityAsync(
long productId,
ProductType productType,
int requiredQuantity,
long? warehouseId = null,
CancellationToken ct = default)
{
var available = await GetAvailableQuantityAsync(productId, productType, warehouseId, ct);
return available >= requiredQuantity;
}
public async Task<List<InventoryItem>> GetLowStockItemsAsync(
ProductType? productType = null,
long? warehouseId = null,
int count = 50,
CancellationToken ct = default)
{
var query = _context.InventoryItems
.Where(x => !x.IsDeleted)
.Where(x => x.Quantity <= x.LowStockThreshold);
if (productType.HasValue)
query = query.Where(x => x.ProductType == productType.Value);
if (warehouseId.HasValue)
query = query.Where(x => x.WarehouseId == warehouseId.Value);
return await query
.OrderBy(x => x.Quantity)
.Take(count)
.ToListAsync(ct);
}
public async Task<List<StockMovement>> GetStockMovementsAsync(
long productId,
ProductType productType,
DateTime? fromDate = null,
DateTime? toDate = null,
CancellationToken ct = default)
{
var inventoryItem = await GetInventoryAsync(productId, productType, null, ct);
if (inventoryItem == null)
return new List<StockMovement>();
var query = _context.StockMovements
.Where(x => x.InventoryItemId == inventoryItem.Id && !x.IsDeleted);
if (fromDate.HasValue)
query = query.Where(x => x.Created >= fromDate.Value);
if (toDate.HasValue)
query = query.Where(x => x.Created <= toDate.Value);
return await query
.OrderByDescending(x => x.Created)
.ToListAsync(ct);
}
#endregion
#region Order Flow Operations
public async Task<bool> ReserveStockAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot reserve: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
if (item.AvailableQuantity < quantity)
{
_logger.LogWarning(
"Cannot reserve: Insufficient stock. Available={Available}, Requested={Requested}",
item.AvailableQuantity, quantity);
return false;
}
var quantityBefore = item.ReservedQuantity;
item.ReservedQuantity += quantity;
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
await LogMovementAsync(
item.Id,
StockMovementType.Reserved,
quantity,
quantityBefore,
item.ReservedQuantity,
orderId,
productType == ProductType.DiscountProduct ? orderId : null,
null,
"رزرو برای سفارش",
null,
ct);
_logger.LogInformation(
"Reserved {Quantity} units for {ProductType} Id={ProductId}, OrderId={OrderId}",
quantity, productType, productId, orderId);
return true;
}
public async Task<bool> ReleaseReservationAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot release: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
var quantityBefore = item.ReservedQuantity;
item.ReservedQuantity = Math.Max(0, item.ReservedQuantity - quantity);
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
await LogMovementAsync(
item.Id,
StockMovementType.Released,
quantity,
quantityBefore,
item.ReservedQuantity,
orderId,
productType == ProductType.DiscountProduct ? orderId : null,
null,
"آزادسازی رزرو",
null,
ct);
_logger.LogInformation(
"Released {Quantity} reserved units for {ProductType} Id={ProductId}, OrderId={OrderId}",
quantity, productType, productId, orderId);
return true;
}
public async Task<bool> ConfirmSaleAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot confirm sale: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
var quantityBefore = item.Quantity;
// کاهش موجودی واقعی
item.Quantity -= quantity;
// کاهش رزرو (اگر رزرو شده بود)
item.ReservedQuantity = Math.Max(0, item.ReservedQuantity - quantity);
// آپدیت آخرین فروش
item.LastSoldAt = DateTime.UtcNow;
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
await LogMovementAsync(
item.Id,
StockMovementType.Sale,
-quantity, // منفی برای خروج
quantityBefore,
item.Quantity,
orderId,
productType == ProductType.DiscountProduct ? orderId : null,
null,
"فروش",
null,
ct);
// همگام‌سازی با Product.RemainingCount
await SyncRemainingCountAsync(item, ct);
_logger.LogInformation(
"Confirmed sale of {Quantity} units for {ProductType} Id={ProductId}, OrderId={OrderId}. New Quantity={NewQuantity}",
quantity, productType, productId, orderId, item.Quantity);
return true;
}
#endregion
#region Stock Management Operations
public async Task<bool> AddStockAsync(
long productId,
ProductType productType,
int quantity,
string? referenceNumber = null,
string? note = null,
long? performedByUserId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot add stock: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
var quantityBefore = item.Quantity;
item.Quantity += quantity;
item.LastRestockedAt = DateTime.UtcNow;
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
await LogMovementAsync(
item.Id,
StockMovementType.Restock,
quantity,
quantityBefore,
item.Quantity,
null,
null,
referenceNumber,
note ?? "ورود کالا",
performedByUserId,
ct);
// همگام‌سازی با Product.RemainingCount
await SyncRemainingCountAsync(item, ct);
_logger.LogInformation(
"Added {Quantity} units to {ProductType} Id={ProductId}. New Quantity={NewQuantity}",
quantity, productType, productId, item.Quantity);
return true;
}
public async Task<bool> AdjustStockAsync(
long productId,
ProductType productType,
int newQuantity,
string? note = null,
long? performedByUserId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot adjust stock: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
var quantityBefore = item.Quantity;
var difference = newQuantity - quantityBefore;
item.Quantity = newQuantity;
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
var movementType = difference >= 0
? StockMovementType.AdjustmentPlus
: StockMovementType.AdjustmentMinus;
await LogMovementAsync(
item.Id,
movementType,
difference,
quantityBefore,
newQuantity,
null,
null,
null,
note ?? "تعدیل موجودی",
performedByUserId,
ct);
// همگام‌سازی با Product.RemainingCount
await SyncRemainingCountAsync(item, ct);
_logger.LogInformation(
"Adjusted stock for {ProductType} Id={ProductId}. Before={Before}, After={After}",
productType, productId, quantityBefore, newQuantity);
return true;
}
public async Task<bool> ProcessReturnAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
string? note = null,
long? performedByUserId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot process return: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
var quantityBefore = item.Quantity;
item.Quantity += quantity;
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
await LogMovementAsync(
item.Id,
StockMovementType.Return,
quantity,
quantityBefore,
item.Quantity,
orderId,
productType == ProductType.DiscountProduct ? orderId : null,
null,
note ?? "برگشت از مشتری",
performedByUserId,
ct);
// همگام‌سازی با Product.RemainingCount
await SyncRemainingCountAsync(item, ct);
_logger.LogInformation(
"Processed return of {Quantity} units for {ProductType} Id={ProductId}. New Quantity={NewQuantity}",
quantity, productType, productId, item.Quantity);
return true;
}
public async Task<bool> RecordLossAsync(
long productId,
ProductType productType,
int quantity,
StockMovementType lossType,
string? note = null,
long? performedByUserId = null,
CancellationToken ct = default)
{
if (lossType != StockMovementType.Damaged && lossType != StockMovementType.Lost)
{
_logger.LogWarning("Invalid loss type: {LossType}. Must be Damaged or Lost.", lossType);
return false;
}
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot record loss: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
var quantityBefore = item.Quantity;
item.Quantity = Math.Max(0, item.Quantity - quantity);
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
await LogMovementAsync(
item.Id,
lossType,
-quantity,
quantityBefore,
item.Quantity,
null,
null,
null,
note ?? (lossType == StockMovementType.Damaged ? "ضایعات" : "مفقودی"),
performedByUserId,
ct);
// همگام‌سازی با Product.RemainingCount
await SyncRemainingCountAsync(item, ct);
_logger.LogInformation(
"Recorded {LossType} of {Quantity} units for {ProductType} Id={ProductId}. New Quantity={NewQuantity}",
lossType, quantity, productType, productId, item.Quantity);
return true;
}
#endregion
#region Bulk Operations
public async Task<bool> BulkReserveStockAsync(
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
long? orderId = null,
CancellationToken ct = default)
{
foreach (var (productId, productType, quantity) in items)
{
var result = await ReserveStockAsync(productId, productType, quantity, orderId, ct);
if (!result)
{
// در صورت خطا، رزروهای قبلی را آزاد کنید
_logger.LogError(
"Bulk reserve failed at {ProductType} Id={ProductId}. Rolling back...",
productType, productId);
// TODO: Implement rollback logic
return false;
}
}
return true;
}
public async Task<bool> BulkReleaseReservationAsync(
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
long? orderId = null,
CancellationToken ct = default)
{
foreach (var (productId, productType, quantity) in items)
{
await ReleaseReservationAsync(productId, productType, quantity, orderId, ct);
}
return true;
}
public async Task<bool> BulkConfirmSaleAsync(
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
long? orderId = null,
CancellationToken ct = default)
{
foreach (var (productId, productType, quantity) in items)
{
var result = await ConfirmSaleAsync(productId, productType, quantity, orderId, ct);
if (!result)
{
_logger.LogError(
"Bulk confirm sale failed at {ProductType} Id={ProductId}",
productType, productId);
return false;
}
}
return true;
}
#endregion
#region Private Helper Methods
/// <summary>
/// همگام‌سازی موجودی InventoryItem با Product.RemainingCount
/// این متد اطمینان می‌دهد که داده‌های قدیمی (RemainingCount) همیشه با سیستم جدید sync است
/// </summary>
private async Task SyncRemainingCountAsync(InventoryItem item, CancellationToken ct)
{
try
{
if (item.ProductType == ProductType.RegularProduct && item.ProductId.HasValue)
{
var product = await _context.Products.FindAsync(new object[] { item.ProductId.Value }, ct);
if (product != null)
{
product.RemainingCount = item.Quantity;
await _context.SaveChangesAsync(ct);
_logger.LogDebug("Synced RemainingCount for Product Id={ProductId} to {Quantity}",
item.ProductId, item.Quantity);
}
}
else if (item.ProductType == ProductType.DiscountProduct && item.DiscountProductId.HasValue)
{
var discountProduct = await _context.DiscountProducts.FindAsync(
new object[] { item.DiscountProductId.Value }, ct);
if (discountProduct != null)
{
discountProduct.RemainingCount = item.Quantity;
await _context.SaveChangesAsync(ct);
_logger.LogDebug("Synced RemainingCount for DiscountProduct Id={ProductId} to {Quantity}",
item.DiscountProductId, item.Quantity);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to sync RemainingCount for InventoryItem Id={ItemId}", item.Id);
// Don't throw - sync failure shouldn't break the main operation
}
}
/// <summary>
/// ثبت حرکت موجودی در StockMovements
/// </summary>
private async Task LogMovementAsync(
long inventoryItemId,
StockMovementType movementType,
int quantity,
int quantityBefore,
int quantityAfter,
long? orderId,
long? discountOrderId,
string? referenceNumber,
string? note,
long? performedByUserId,
CancellationToken ct)
{
var movement = new StockMovement
{
InventoryItemId = inventoryItemId,
MovementType = movementType,
Quantity = quantity,
QuantityBefore = quantityBefore,
QuantityAfter = quantityAfter,
OrderId = orderId,
DiscountOrderId = discountOrderId,
ReferenceNumber = referenceNumber,
Note = note,
PerformedByUserId = performedByUserId
};
_context.StockMovements.Add(movement);
await _context.SaveChangesAsync(ct);
}
#endregion
}
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.164</Version>
<Version>0.0.165</Version>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
@@ -59,6 +59,8 @@
<Protobuf Include="Protos\city.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<!-- App Version Tracking System -->
<Protobuf Include="Protos\appversion.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<!-- Inventory Management System -->
<Protobuf Include="Protos\inventory.proto" ProtoRoot="Protos\" GrpcServices="Both" />
</ItemGroup>
<Target Name="PushToFoursatNuget" AfterTargets="Pack">
@@ -0,0 +1,529 @@
syntax = "proto3";
package inventory;
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
option csharp_namespace = "CMSMicroservice.Protobuf.Protos.Inventory";
// =============================================
// 📦 Inventory Management Service
// =============================================
service InventoryContract {
// ========== Warehouse Management ==========
rpc CreateWarehouse(CreateWarehouseRequest) returns (CreateWarehouseResponse) {
option (google.api.http) = {
post: "/api/inventory/warehouses"
body: "*"
};
};
rpc UpdateWarehouse(UpdateWarehouseRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
put: "/api/inventory/warehouses/{id}"
body: "*"
};
};
rpc DeleteWarehouse(DeleteWarehouseRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
delete: "/api/inventory/warehouses/{id}"
};
};
rpc GetWarehouse(GetWarehouseRequest) returns (GetWarehouseResponse) {
option (google.api.http) = {
get: "/api/inventory/warehouses/{id}"
};
};
rpc GetAllWarehouses(GetAllWarehousesRequest) returns (GetAllWarehousesResponse) {
option (google.api.http) = {
get: "/api/inventory/warehouses"
};
};
rpc SetDefaultWarehouse(SetDefaultWarehouseRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/api/inventory/warehouses/{id}/set-default"
body: "*"
};
};
// ========== Inventory Item Management ==========
rpc GetInventoryItem(GetInventoryItemRequest) returns (GetInventoryItemResponse) {
option (google.api.http) = {
get: "/api/inventory/items/{id}"
};
};
rpc GetInventoryByProduct(GetInventoryByProductRequest) returns (GetInventoryByProductResponse) {
option (google.api.http) = {
get: "/api/inventory/by-product/{product_id}"
};
};
rpc GetAllInventoryItems(GetAllInventoryItemsRequest) returns (GetAllInventoryItemsResponse) {
option (google.api.http) = {
get: "/api/inventory/items"
};
};
rpc GetLowStockItems(GetLowStockItemsRequest) returns (GetLowStockItemsResponse) {
option (google.api.http) = {
get: "/api/inventory/low-stock"
};
};
rpc UpdateInventorySettings(UpdateInventorySettingsRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
put: "/api/inventory/items/{id}/settings"
body: "*"
};
};
// ========== Stock Operations ==========
rpc AddStock(AddStockRequest) returns (AddStockResponse) {
option (google.api.http) = {
post: "/api/inventory/stock/add"
body: "*"
};
};
rpc AdjustStock(AdjustStockRequest) returns (AdjustStockResponse) {
option (google.api.http) = {
post: "/api/inventory/stock/adjust"
body: "*"
};
};
rpc ReserveStock(ReserveStockRequest) returns (ReserveStockResponse) {
option (google.api.http) = {
post: "/api/inventory/stock/reserve"
body: "*"
};
};
rpc ReleaseReservation(ReleaseReservationRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/api/inventory/stock/release"
body: "*"
};
};
rpc ConfirmSale(ConfirmSaleRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/api/inventory/stock/confirm-sale"
body: "*"
};
};
rpc ProcessReturn(ProcessReturnRequest) returns (ProcessReturnResponse) {
option (google.api.http) = {
post: "/api/inventory/stock/return"
body: "*"
};
};
rpc RecordLoss(RecordLossRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/api/inventory/stock/loss"
body: "*"
};
};
// ========== Bulk Operations ==========
rpc BulkAddStock(BulkAddStockRequest) returns (BulkAddStockResponse) {
option (google.api.http) = {
post: "/api/inventory/stock/bulk-add"
body: "*"
};
};
rpc BulkAdjustStock(BulkAdjustStockRequest) returns (BulkAdjustStockResponse) {
option (google.api.http) = {
post: "/api/inventory/stock/bulk-adjust"
body: "*"
};
};
// ========== Stock Movements ==========
rpc GetStockMovements(GetStockMovementsRequest) returns (GetStockMovementsResponse) {
option (google.api.http) = {
get: "/api/inventory/movements"
};
};
rpc GetStockMovementsByInventoryItem(GetStockMovementsByInventoryItemRequest) returns (GetStockMovementsByInventoryItemResponse) {
option (google.api.http) = {
get: "/api/inventory/items/{inventory_item_id}/movements"
};
};
// ========== Reports ==========
rpc GetInventorySummary(GetInventorySummaryRequest) returns (GetInventorySummaryResponse) {
option (google.api.http) = {
get: "/api/inventory/summary"
};
};
rpc GetStockValueReport(GetStockValueReportRequest) returns (GetStockValueReportResponse) {
option (google.api.http) = {
get: "/api/inventory/reports/stock-value"
};
};
}
// =============================================
// Enums
// =============================================
enum ProductType {
PRODUCT_TYPE_UNSPECIFIED = 0;
REGULAR_PRODUCT = 1;
DISCOUNT_PRODUCT = 2;
}
enum StockMovementType {
MOVEMENT_TYPE_UNSPECIFIED = 0;
INITIAL_STOCK = 1;
RESTOCK = 2;
RETURN = 3;
SALE = 10;
ADJUSTMENT_INCREASE = 20;
ADJUSTMENT_DECREASE = 21;
RESERVED = 30;
RELEASED = 31;
LOSS = 40;
DAMAGED = 41;
EXPIRED = 42;
TRANSFER_OUT = 50;
TRANSFER_IN = 51;
}
// =============================================
// Warehouse Messages
// =============================================
message WarehouseDto {
int64 id = 1;
string name = 2;
string code = 3;
string address = 4;
bool is_default = 5;
bool is_active = 6;
google.protobuf.Timestamp created = 7;
google.protobuf.Timestamp last_modified = 8;
}
message CreateWarehouseRequest {
string name = 1;
string code = 2;
string address = 3;
bool is_default = 4;
}
message CreateWarehouseResponse {
int64 id = 1;
}
message UpdateWarehouseRequest {
int64 id = 1;
string name = 2;
string code = 3;
string address = 4;
bool is_active = 5;
}
message DeleteWarehouseRequest {
int64 id = 1;
}
message GetWarehouseRequest {
int64 id = 1;
}
message GetWarehouseResponse {
WarehouseDto warehouse = 1;
}
message GetAllWarehousesRequest {
google.protobuf.BoolValue is_active = 1;
int32 page = 2;
int32 page_size = 3;
}
message GetAllWarehousesResponse {
repeated WarehouseDto warehouses = 1;
int32 total_count = 2;
}
message SetDefaultWarehouseRequest {
int64 id = 1;
}
// =============================================
// Inventory Item Messages
// =============================================
message InventoryItemDto {
int64 id = 1;
google.protobuf.Int64Value product_id = 2;
google.protobuf.Int64Value discount_product_id = 3;
ProductType product_type = 4;
int32 quantity = 5;
int32 reserved_quantity = 6;
int32 available_quantity = 7;
int32 low_stock_threshold = 8;
int32 reorder_point = 9;
int32 max_stock_level = 10;
google.protobuf.Timestamp last_restocked_at = 11;
google.protobuf.Timestamp last_sold_at = 12;
int64 warehouse_id = 13;
string warehouse_name = 14;
string product_title = 15;
int64 product_price = 16;
google.protobuf.Timestamp created = 17;
}
message GetInventoryItemRequest {
int64 id = 1;
}
message GetInventoryItemResponse {
InventoryItemDto item = 1;
}
message GetInventoryByProductRequest {
int64 product_id = 1;
ProductType product_type = 2;
}
message GetInventoryByProductResponse {
InventoryItemDto item = 1;
}
message GetAllInventoryItemsRequest {
google.protobuf.Int64Value warehouse_id = 1;
ProductType product_type = 2;
string search = 3;
int32 page = 4;
int32 page_size = 5;
string sort_by = 6;
bool sort_desc = 7;
}
message GetAllInventoryItemsResponse {
repeated InventoryItemDto items = 1;
int32 total_count = 2;
}
message GetLowStockItemsRequest {
google.protobuf.Int64Value warehouse_id = 1;
ProductType product_type = 2;
int32 page = 3;
int32 page_size = 4;
}
message GetLowStockItemsResponse {
repeated InventoryItemDto items = 1;
int32 total_count = 2;
}
message UpdateInventorySettingsRequest {
int64 id = 1;
int32 low_stock_threshold = 2;
int32 reorder_point = 3;
int32 max_stock_level = 4;
}
// =============================================
// Stock Operation Messages
// =============================================
message AddStockRequest {
int64 product_id = 1;
ProductType product_type = 2;
int32 quantity = 3;
string reference_number = 4;
string note = 5;
google.protobuf.Int64Value warehouse_id = 6;
}
message AddStockResponse {
int64 inventory_item_id = 1;
int32 new_quantity = 2;
}
message AdjustStockRequest {
int64 product_id = 1;
ProductType product_type = 2;
int32 new_quantity = 3;
string reason = 4;
string reference_number = 5;
}
message AdjustStockResponse {
int32 previous_quantity = 1;
int32 new_quantity = 2;
int32 difference = 3;
}
message ReserveStockRequest {
int64 product_id = 1;
ProductType product_type = 2;
int32 quantity = 3;
google.protobuf.Int64Value order_id = 4;
google.protobuf.Int64Value discount_order_id = 5;
}
message ReserveStockResponse {
bool success = 1;
string message = 2;
int32 available_quantity = 3;
}
message ReleaseReservationRequest {
int64 product_id = 1;
ProductType product_type = 2;
int32 quantity = 3;
google.protobuf.Int64Value order_id = 4;
google.protobuf.Int64Value discount_order_id = 5;
}
message ConfirmSaleRequest {
int64 product_id = 1;
ProductType product_type = 2;
int32 quantity = 3;
google.protobuf.Int64Value order_id = 4;
google.protobuf.Int64Value discount_order_id = 5;
bool from_reservation = 6;
}
message ProcessReturnRequest {
int64 product_id = 1;
ProductType product_type = 2;
int32 quantity = 3;
google.protobuf.Int64Value order_id = 4;
google.protobuf.Int64Value discount_order_id = 5;
string reason = 6;
}
message ProcessReturnResponse {
int32 new_quantity = 1;
}
message RecordLossRequest {
int64 product_id = 1;
ProductType product_type = 2;
int32 quantity = 3;
StockMovementType loss_type = 4; // LOSS, DAMAGED, or EXPIRED
string reason = 5;
string reference_number = 6;
}
// =============================================
// Bulk Operation Messages
// =============================================
message BulkStockItem {
int64 product_id = 1;
ProductType product_type = 2;
int32 quantity = 3;
}
message BulkAddStockRequest {
repeated BulkStockItem items = 1;
string reference_number = 2;
string note = 3;
google.protobuf.Int64Value warehouse_id = 4;
}
message BulkAddStockResponse {
int32 success_count = 1;
int32 failed_count = 2;
repeated string errors = 3;
}
message BulkAdjustStockRequest {
repeated BulkStockItem items = 1;
string reason = 2;
string reference_number = 3;
}
message BulkAdjustStockResponse {
int32 success_count = 1;
int32 failed_count = 2;
repeated string errors = 3;
}
// =============================================
// Stock Movement Messages
// =============================================
message StockMovementDto {
int64 id = 1;
int64 inventory_item_id = 2;
StockMovementType movement_type = 3;
int32 quantity = 4;
int32 quantity_before = 5;
int32 quantity_after = 6;
google.protobuf.Int64Value order_id = 7;
google.protobuf.Int64Value discount_order_id = 8;
string reference_number = 9;
string note = 10;
google.protobuf.Int64Value performed_by_user_id = 11;
google.protobuf.Timestamp created = 12;
string product_title = 13;
}
message GetStockMovementsRequest {
google.protobuf.Int64Value inventory_item_id = 1;
google.protobuf.Int64Value product_id = 2;
ProductType product_type = 3;
StockMovementType movement_type = 4;
google.protobuf.Timestamp from_date = 5;
google.protobuf.Timestamp to_date = 6;
int32 page = 7;
int32 page_size = 8;
}
message GetStockMovementsResponse {
repeated StockMovementDto movements = 1;
int32 total_count = 2;
}
message GetStockMovementsByInventoryItemRequest {
int64 inventory_item_id = 1;
int32 page = 2;
int32 page_size = 3;
}
message GetStockMovementsByInventoryItemResponse {
repeated StockMovementDto movements = 1;
int32 total_count = 2;
}
// =============================================
// Report Messages
// =============================================
message GetInventorySummaryRequest {
google.protobuf.Int64Value warehouse_id = 1;
}
message GetInventorySummaryResponse {
int32 total_products = 1;
int32 total_discount_products = 2;
int32 total_quantity = 3;
int32 total_reserved = 4;
int32 low_stock_count = 5;
int32 out_of_stock_count = 6;
int64 total_stock_value = 7;
}
message GetStockValueReportRequest {
google.protobuf.Int64Value warehouse_id = 1;
ProductType product_type = 2;
}
message StockValueItem {
int64 product_id = 1;
string product_title = 2;
ProductType product_type = 3;
int32 quantity = 4;
int64 unit_price = 5;
int64 total_value = 6;
}
message GetStockValueReportResponse {
repeated StockValueItem items = 1;
int64 total_value = 2;
int32 total_items = 3;
}
@@ -76,6 +76,7 @@ message CreateManualPaymentRequest
ManualPaymentType type = 3;
string description = 4;
google.protobuf.StringValue reference_number = 5;
google.protobuf.StringValue image_path = 6;
}
message CreateManualPaymentResponse
@@ -133,6 +134,7 @@ message ManualPaymentModel
google.protobuf.StringValue rejection_reason = 17;
google.protobuf.Int64Value transaction_id = 18;
google.protobuf.Timestamp created = 19;
google.protobuf.StringValue image_path = 20;
}
message ProcessManualMembershipPaymentRequest
@@ -0,0 +1,234 @@
using CMSMicroservice.Protobuf.Protos.Inventory;
using CMSMicroservice.WebApi.Common.Services;
// Warehouse Commands & Queries
using CMSMicroservice.Application.Features.Warehouses.Commands;
using CMSMicroservice.Application.Features.Warehouses.Queries;
// InventoryItem Commands & Queries
using CMSMicroservice.Application.Features.InventoryItems.Commands;
using CMSMicroservice.Application.Features.InventoryItems.Queries;
// StockMovement Commands & Queries
using CMSMicroservice.Application.Features.StockMovements.Commands;
using CMSMicroservice.Application.Features.StockMovements.Queries;
using CMSMicroservice.Domain.Entities;
using Mapster;
using Google.Protobuf.WellKnownTypes;
using System.Collections.Generic;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
/// <summary>
/// gRPC Service for Inventory Management
/// </summary>
public class InventoryService : InventoryContract.InventoryContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public InventoryService(IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
#region Warehouse Management
public override async Task<CreateWarehouseResponse> CreateWarehouse(CreateWarehouseRequest request, ServerCallContext context)
{
var id = await _dispatchRequestToCQRS.Handle<CreateWarehouseRequest, CreateWarehouseCommand, long>(request, context);
return new CreateWarehouseResponse { Id = id };
}
public override async Task<Empty> UpdateWarehouse(UpdateWarehouseRequest request, ServerCallContext context)
{
await _dispatchRequestToCQRS.Handle<UpdateWarehouseRequest, UpdateWarehouseCommand, bool>(request, context);
return new Empty();
}
public override async Task<Empty> DeleteWarehouse(DeleteWarehouseRequest request, ServerCallContext context)
{
await _dispatchRequestToCQRS.Handle<DeleteWarehouseRequest, DeleteWarehouseCommand, bool>(request, context);
return new Empty();
}
public override async Task<GetWarehouseResponse> GetWarehouse(GetWarehouseRequest request, ServerCallContext context)
{
var warehouse = await _dispatchRequestToCQRS.Handle<GetWarehouseRequest, GetWarehouseByIdQuery, Warehouse?>(request, context);
return new GetWarehouseResponse
{
Warehouse = warehouse?.Adapt<WarehouseDto>()
};
}
public override async Task<GetAllWarehousesResponse> GetAllWarehouses(GetAllWarehousesRequest request, ServerCallContext context)
{
var warehouses = await _dispatchRequestToCQRS.Handle<GetAllWarehousesRequest, GetAllWarehousesQuery, List<Warehouse>>(request, context);
var response = new GetAllWarehousesResponse { TotalCount = warehouses.Count };
response.Warehouses.AddRange(warehouses.Select(w => w.Adapt<WarehouseDto>()));
return response;
}
public override async Task<Empty> SetDefaultWarehouse(SetDefaultWarehouseRequest request, ServerCallContext context)
{
await _dispatchRequestToCQRS.Handle<SetDefaultWarehouseRequest, SetDefaultWarehouseCommand, bool>(request, context);
return new Empty();
}
#endregion
#region Inventory Item Management
public override async Task<GetInventoryItemResponse> GetInventoryItem(GetInventoryItemRequest request, ServerCallContext context)
{
var item = await _dispatchRequestToCQRS.Handle<GetInventoryItemRequest, GetInventoryItemByIdQuery, InventoryItem?>(request, context);
return new GetInventoryItemResponse
{
Item = item?.Adapt<InventoryItemDto>()
};
}
public override async Task<GetInventoryByProductResponse> GetInventoryByProduct(GetInventoryByProductRequest request, ServerCallContext context)
{
InventoryItem? item;
if (request.ProductType == Protobuf.Protos.Inventory.ProductType.RegularProduct)
{
item = await _dispatchRequestToCQRS.Handle<GetInventoryByProductRequest, GetInventoryItemByProductIdQuery, InventoryItem?>(request, context);
}
else
{
item = await _dispatchRequestToCQRS.Handle<GetInventoryByProductRequest, GetInventoryItemByDiscountProductIdQuery, InventoryItem?>(request, context);
}
return new GetInventoryByProductResponse
{
Item = item?.Adapt<InventoryItemDto>()
};
}
public override async Task<GetAllInventoryItemsResponse> GetAllInventoryItems(GetAllInventoryItemsRequest request, ServerCallContext context)
{
var items = await _dispatchRequestToCQRS.Handle<GetAllInventoryItemsRequest, SearchInventoryItemsQuery, List<InventoryItem>>(request, context);
var response = new GetAllInventoryItemsResponse { TotalCount = items.Count };
response.Items.AddRange(items.Select(i => i.Adapt<InventoryItemDto>()));
return response;
}
public override async Task<GetLowStockItemsResponse> GetLowStockItems(GetLowStockItemsRequest request, ServerCallContext context)
{
var items = await _dispatchRequestToCQRS.Handle<GetLowStockItemsRequest, GetLowStockItemsQuery, List<InventoryItem>>(request, context);
var response = new GetLowStockItemsResponse { TotalCount = items.Count };
response.Items.AddRange(items.Select(i => i.Adapt<InventoryItemDto>()));
return response;
}
public override async Task<Empty> UpdateInventorySettings(UpdateInventorySettingsRequest request, ServerCallContext context)
{
await _dispatchRequestToCQRS.Handle<UpdateInventorySettingsRequest, UpdateInventoryItemCommand, bool>(request, context);
return new Empty();
}
#endregion
#region Stock Operations
public override async Task<AddStockResponse> AddStock(AddStockRequest request, ServerCallContext context)
{
var success = await _dispatchRequestToCQRS.Handle<AddStockRequest, IncreaseInventoryCommand, bool>(request, context);
return new AddStockResponse
{
InventoryItemId = 0, // Will be filled by mapping
NewQuantity = 0
};
}
public override async Task<AdjustStockResponse> AdjustStock(AdjustStockRequest request, ServerCallContext context)
{
await _dispatchRequestToCQRS.Handle<AdjustStockRequest, UpdateInventoryQuantityCommand, bool>(request, context);
return new AdjustStockResponse();
}
public override async Task<ReserveStockResponse> ReserveStock(ReserveStockRequest request, ServerCallContext context)
{
var success = await _dispatchRequestToCQRS.Handle<ReserveStockRequest, ReserveInventoryCommand, bool>(request, context);
return new ReserveStockResponse
{
Success = success,
Message = success ? "Stock reserved successfully" : "Failed to reserve stock"
};
}
public override async Task<Empty> ReleaseReservation(ReleaseReservationRequest request, ServerCallContext context)
{
await _dispatchRequestToCQRS.Handle<ReleaseReservationRequest, ReleaseReservedInventoryCommand, bool>(request, context);
return new Empty();
}
public override async Task<Empty> ConfirmSale(ConfirmSaleRequest request, ServerCallContext context)
{
await _dispatchRequestToCQRS.Handle<ConfirmSaleRequest, ReduceInventoryCommand, bool>(request, context);
return new Empty();
}
public override async Task<ProcessReturnResponse> ProcessReturn(ProcessReturnRequest request, ServerCallContext context)
{
await _dispatchRequestToCQRS.Handle<ProcessReturnRequest, IncreaseInventoryCommand, bool>(request, context);
return new ProcessReturnResponse { NewQuantity = 0 };
}
public override async Task<Empty> RecordLoss(RecordLossRequest request, ServerCallContext context)
{
await _dispatchRequestToCQRS.Handle<RecordLossRequest, CreateStockMovementCommand, long>(request, context);
return new Empty();
}
#endregion
#region Bulk Operations
public override async Task<BulkAddStockResponse> BulkAddStock(BulkAddStockRequest request, ServerCallContext context)
{
// TODO: Implement bulk add stock
return new BulkAddStockResponse { SuccessCount = 0, FailedCount = 0 };
}
public override async Task<BulkAdjustStockResponse> BulkAdjustStock(BulkAdjustStockRequest request, ServerCallContext context)
{
// TODO: Implement bulk adjust stock
return new BulkAdjustStockResponse { SuccessCount = 0, FailedCount = 0 };
}
#endregion
#region Stock Movements
public override async Task<GetStockMovementsResponse> GetStockMovements(GetStockMovementsRequest request, ServerCallContext context)
{
var movements = await _dispatchRequestToCQRS.Handle<GetStockMovementsRequest, SearchStockMovementsQuery, List<StockMovement>>(request, context);
var response = new GetStockMovementsResponse { TotalCount = movements.Count };
response.Movements.AddRange(movements.Select(m => m.Adapt<StockMovementDto>()));
return response;
}
public override async Task<GetStockMovementsByInventoryItemResponse> GetStockMovementsByInventoryItem(GetStockMovementsByInventoryItemRequest request, ServerCallContext context)
{
var movements = await _dispatchRequestToCQRS.Handle<GetStockMovementsByInventoryItemRequest, GetInventoryItemMovementHistoryQuery, List<StockMovement>>(request, context);
var response = new GetStockMovementsByInventoryItemResponse { TotalCount = movements.Count };
response.Movements.AddRange(movements.Select(m => m.Adapt<StockMovementDto>()));
return response;
}
#endregion
#region Reports
public override async Task<GetInventorySummaryResponse> GetInventorySummary(GetInventorySummaryRequest request, ServerCallContext context)
{
// TODO: Implement summary query
return new GetInventorySummaryResponse();
}
public override async Task<GetStockValueReportResponse> GetStockValueReport(GetStockValueReportRequest request, ServerCallContext context)
{
// TODO: Implement stock value report
return new GetStockValueReportResponse();
}
#endregion
}