diff --git a/README.md b/README.md index 432e77f..829ad70 100644 --- a/README.md +++ b/README.md @@ -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 --- diff --git a/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs b/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs index bc6c049..637481e 100644 --- a/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs @@ -61,6 +61,11 @@ public interface IApplicationDbContext DbSet States { get; } DbSet Cities { get; } + // ============= Inventory Management ============= + DbSet Warehouses { get; } + DbSet InventoryItems { get; } + DbSet StockMovements { get; } + /// /// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures /// diff --git a/src/CMSMicroservice.Application/Common/Interfaces/IInventoryService.cs b/src/CMSMicroservice.Application/Common/Interfaces/IInventoryService.cs new file mode 100644 index 0000000..e4a024c --- /dev/null +++ b/src/CMSMicroservice.Application/Common/Interfaces/IInventoryService.cs @@ -0,0 +1,216 @@ +using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Enums; + +namespace CMSMicroservice.Application.Common.Interfaces; + +/// +/// سرویس مدیریت موجودی - لایه بالاتر برای عملیات business +/// این سرویس مسئول همگام‌سازی موجودی بین InventoryItem و Product.RemainingCount است +/// +public interface IInventoryService +{ + #region Initialization + + /// + /// ایجاد رکورد موجودی برای محصول جدید + /// این متد باید در CreateProductCommandHandler و CreateDiscountProductCommandHandler فراخوانی شود + /// + /// شناسه محصول (Product.Id یا DiscountProduct.Id) + /// نوع محصول (RegularProduct یا DiscountProduct) + /// موجودی اولیه + /// شناسه انبار (پیش‌فرض: انبار اصلی) + /// آستانه هشدار کم‌موجودی + /// CancellationToken + /// شناسه InventoryItem ایجاد شده + Task InitializeInventoryAsync( + long productId, + ProductType productType, + int initialQuantity, + long? warehouseId = null, + int lowStockThreshold = 10, + CancellationToken ct = default); + + #endregion + + #region Query Operations + + /// + /// دریافت موجودی یک محصول + /// + Task GetInventoryAsync( + long productId, + ProductType productType, + long? warehouseId = null, + CancellationToken ct = default); + + /// + /// دریافت موجودی قابل فروش (Quantity - ReservedQuantity) + /// + Task GetAvailableQuantityAsync( + long productId, + ProductType productType, + long? warehouseId = null, + CancellationToken ct = default); + + /// + /// بررسی اینکه آیا موجودی کافی برای فروش وجود دارد + /// + Task CheckAvailabilityAsync( + long productId, + ProductType productType, + int requiredQuantity, + long? warehouseId = null, + CancellationToken ct = default); + + /// + /// دریافت لیست محصولات کم‌موجود + /// + Task> GetLowStockItemsAsync( + ProductType? productType = null, + long? warehouseId = null, + int count = 50, + CancellationToken ct = default); + + /// + /// دریافت تاریخچه حرکات موجودی + /// + Task> GetStockMovementsAsync( + long productId, + ProductType productType, + DateTime? fromDate = null, + DateTime? toDate = null, + CancellationToken ct = default); + + #endregion + + #region Order Flow Operations + + /// + /// رزرو موجودی برای سفارش pending + /// این متد در PlaceOrderCommandHandler فراخوانی می‌شود + /// فقط ReservedQuantity را افزایش می‌دهد، Quantity تغییر نمی‌کند + /// + /// شناسه محصول + /// نوع محصول + /// تعداد رزرو + /// شناسه سفارش (Order.Id یا DiscountOrder.Id) + /// CancellationToken + /// true اگر رزرو موفق بود + Task ReserveStockAsync( + long productId, + ProductType productType, + int quantity, + long? orderId = null, + CancellationToken ct = default); + + /// + /// آزادسازی رزرو (لغو سفارش یا timeout) + /// این متد در CancelOrderCommandHandler فراخوانی می‌شود + /// + Task ReleaseReservationAsync( + long productId, + ProductType productType, + int quantity, + long? orderId = null, + CancellationToken ct = default); + + /// + /// تایید فروش - کسر واقعی موجودی + /// این متد در CompleteOrderPaymentCommandHandler فراخوانی می‌شود + /// ReservedQuantity کاهش می‌یابد، Quantity کاهش می‌یابد، Product.RemainingCount sync می‌شود + /// + Task ConfirmSaleAsync( + long productId, + ProductType productType, + int quantity, + long? orderId = null, + CancellationToken ct = default); + + #endregion + + #region Stock Management Operations + + /// + /// ورود کالا به انبار (Restock) + /// + /// شناسه محصول + /// نوع محصول + /// تعداد ورودی + /// شماره مرجع (مثل شماره فاکتور خرید) + /// یادداشت + /// شناسه کاربر انجام‌دهنده + /// CancellationToken + Task AddStockAsync( + long productId, + ProductType productType, + int quantity, + string? referenceNumber = null, + string? note = null, + long? performedByUserId = null, + CancellationToken ct = default); + + /// + /// تعدیل موجودی (تنظیم به مقدار جدید) + /// + Task AdjustStockAsync( + long productId, + ProductType productType, + int newQuantity, + string? note = null, + long? performedByUserId = null, + CancellationToken ct = default); + + /// + /// ثبت برگشت کالا از مشتری + /// + Task ProcessReturnAsync( + long productId, + ProductType productType, + int quantity, + long? orderId = null, + string? note = null, + long? performedByUserId = null, + CancellationToken ct = default); + + /// + /// ثبت ضایعات/مفقودی + /// + Task 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 + + /// + /// رزرو موجودی برای چند آیتم (یک سفارش با چند محصول) + /// + Task BulkReserveStockAsync( + IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items, + long? orderId = null, + CancellationToken ct = default); + + /// + /// آزادسازی رزرو برای چند آیتم + /// + Task BulkReleaseReservationAsync( + IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items, + long? orderId = null, + CancellationToken ct = default); + + /// + /// تایید فروش برای چند آیتم + /// + Task BulkConfirmSaleAsync( + IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items, + long? orderId = null, + CancellationToken ct = default); + + #endregion +} diff --git a/src/CMSMicroservice.Application/Common/Interfaces/Repositories/IInventoryItemRepository.cs b/src/CMSMicroservice.Application/Common/Interfaces/Repositories/IInventoryItemRepository.cs new file mode 100644 index 0000000..1940164 --- /dev/null +++ b/src/CMSMicroservice.Application/Common/Interfaces/Repositories/IInventoryItemRepository.cs @@ -0,0 +1,152 @@ +using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Enums; + +namespace CMSMicroservice.Application.Common.Interfaces.Repositories; + +/// +/// Repository interface برای مدیریت موجودی محصولات +/// +public interface IInventoryItemRepository +{ + #region Read Operations + + /// + /// دریافت آیتم موجودی بر اساس شناسه + /// + Task GetByIdAsync(long id, CancellationToken cancellationToken = default); + + /// + /// دریافت آیتم موجودی بر اساس محصول معمولی + /// + Task GetByProductIdAsync(long productId, long warehouseId = 1, CancellationToken cancellationToken = default); + + /// + /// دریافت آیتم موجودی بر اساس محصول تخفیفی + /// + Task GetByDiscountProductIdAsync(long discountProductId, long warehouseId = 1, CancellationToken cancellationToken = default); + + /// + /// دریافت تمام آیتم‌های موجودی یک انبار + /// + Task> GetByWarehouseIdAsync(long warehouseId, CancellationToken cancellationToken = default); + + /// + /// دریافت محصولات کم‌موجود + /// + Task> GetLowStockItemsAsync(ProductType? productType = null, long warehouseId = 1, CancellationToken cancellationToken = default); + + /// + /// دریافت محصولات با موجودی صفر + /// + Task> GetOutOfStockItemsAsync(ProductType? productType = null, long warehouseId = 1, CancellationToken cancellationToken = default); + + /// + /// جستجوی آیتم‌های موجودی با فیلتر + /// + Task> 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); + + /// + /// شمارش کل آیتم‌های موجودی با فیلتر + /// + Task CountAsync( + string? searchTerm = null, + ProductType? productType = null, + long? warehouseId = null, + int? minQuantity = null, + int? maxQuantity = null, + CancellationToken cancellationToken = default); + + #endregion + + #region Write Operations + + /// + /// افزودن آیتم موجودی جدید + /// + Task AddAsync(InventoryItem inventoryItem, CancellationToken cancellationToken = default); + + /// + /// بروزرسانی آیتم موجودی + /// + Task UpdateAsync(InventoryItem inventoryItem, CancellationToken cancellationToken = default); + + /// + /// حذف آیتم موجودی + /// + Task DeleteAsync(long id, CancellationToken cancellationToken = default); + + /// + /// بروزرسانی موجودی (با ثبت حرکت) + /// + Task 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); + + /// + /// رزرو موجودی + /// + Task ReserveQuantityAsync( + long inventoryItemId, + int quantity, + string? note = null, + string? referenceNumber = null, + long? orderId = null, + long? discountOrderId = null, + long? performedByUserId = null, + CancellationToken cancellationToken = default); + + /// + /// آزاد کردن موجودی رزرو شده + /// + Task 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 + + /// + /// بروزرسانی انبوه موجودی چندین محصول + /// + Task BulkUpdateQuantityAsync( + List<(long InventoryItemId, int QuantityChange, string? Note)> updates, + StockMovementType movementType, + string? referenceNumber = null, + long? performedByUserId = null, + CancellationToken cancellationToken = default); + + /// + /// رزرو انبوه موجودی چندین محصول + /// + Task 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 +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Common/Interfaces/Repositories/IStockMovementRepository.cs b/src/CMSMicroservice.Application/Common/Interfaces/Repositories/IStockMovementRepository.cs new file mode 100644 index 0000000..48b4226 --- /dev/null +++ b/src/CMSMicroservice.Application/Common/Interfaces/Repositories/IStockMovementRepository.cs @@ -0,0 +1,146 @@ +using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Enums; + +namespace CMSMicroservice.Application.Common.Interfaces.Repositories; + +/// +/// Repository interface برای مدیریت حرکات موجودی +/// +public interface IStockMovementRepository +{ + #region Read Operations + + /// + /// دریافت حرکت موجودی بر اساس شناسه + /// + Task GetByIdAsync(long id, CancellationToken cancellationToken = default); + + /// + /// دریافت تاریخچه حرکات یک آیتم موجودی + /// + Task> GetByInventoryItemIdAsync( + long inventoryItemId, + StockMovementType? movementType = null, + DateTime? fromDate = null, + DateTime? toDate = null, + int skip = 0, + int take = 100, + CancellationToken cancellationToken = default); + + /// + /// دریافت حرکات مربوط به یک سفارش + /// + Task> GetByOrderIdAsync(long orderId, CancellationToken cancellationToken = default); + + /// + /// دریافت حرکات مربوط به یک سفارش تخفیفی + /// + Task> GetByDiscountOrderIdAsync(long discountOrderId, CancellationToken cancellationToken = default); + + /// + /// دریافت حرکات بر اساس شماره مرجع + /// + Task> GetByReferenceNumberAsync(string referenceNumber, CancellationToken cancellationToken = default); + + /// + /// دریافت حرکات بر اساس نوع حرکت + /// + Task> GetByMovementTypeAsync( + StockMovementType movementType, + DateTime? fromDate = null, + DateTime? toDate = null, + int skip = 0, + int take = 100, + CancellationToken cancellationToken = default); + + /// + /// دریافت آخرین حرکات موجودی + /// + Task> GetRecentMovementsAsync( + int count = 50, + StockMovementType? movementType = null, + CancellationToken cancellationToken = default); + + /// + /// جستجوی حرکات موجودی با فیلتر پیشرفته + /// + Task> 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); + + /// + /// شمارش حرکات موجودی با فیلتر + /// + Task 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 + + /// + /// افزودن حرکت موجودی جدید + /// + Task AddAsync(StockMovement stockMovement, CancellationToken cancellationToken = default); + + /// + /// حذف حرکت موجودی (نرم‌افزاری) + /// + Task DeleteAsync(long id, CancellationToken cancellationToken = default); + + /// + /// افزودن انبوه حرکات موجودی + /// + Task> BulkAddAsync(List stockMovements, CancellationToken cancellationToken = default); + + #endregion + + #region Analytics & Reports + + /// + /// گزارش خلاصه حرکات در بازه زمانی + /// + Task> GetMovementSummaryAsync( + DateTime fromDate, + DateTime toDate, + long? inventoryItemId = null, + CancellationToken cancellationToken = default); + + /// + /// گزارش حجم ورود و خروج روزانه + /// + Task> GetDailyMovementVolumeAsync( + DateTime fromDate, + DateTime toDate, + long? inventoryItemId = null, + CancellationToken cancellationToken = default); + + /// + /// گزارش بیشترین حرکات محصولات + /// + Task> GetTopMovingProductsAsync( + DateTime fromDate, + DateTime toDate, + int count = 10, + StockMovementType? movementType = null, + CancellationToken cancellationToken = default); + + #endregion +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Common/Interfaces/Repositories/IWarehouseRepository.cs b/src/CMSMicroservice.Application/Common/Interfaces/Repositories/IWarehouseRepository.cs new file mode 100644 index 0000000..d0d7a3a --- /dev/null +++ b/src/CMSMicroservice.Application/Common/Interfaces/Repositories/IWarehouseRepository.cs @@ -0,0 +1,111 @@ +using CMSMicroservice.Domain.Entities; + +namespace CMSMicroservice.Application.Common.Interfaces.Repositories; + +/// +/// Repository interface برای مدیریت انبارها +/// +public interface IWarehouseRepository +{ + #region Read Operations + + /// + /// دریافت انبار بر اساس شناسه + /// + Task GetByIdAsync(long id, CancellationToken cancellationToken = default); + + /// + /// دریافت انبار بر اساس کد + /// + Task GetByCodeAsync(string code, CancellationToken cancellationToken = default); + + /// + /// دریافت انبار پیش‌فرض + /// + Task GetDefaultWarehouseAsync(CancellationToken cancellationToken = default); + + /// + /// دریافت تمام انبارهای فعال + /// + Task> GetActiveWarehousesAsync(CancellationToken cancellationToken = default); + + /// + /// دریافت تمام انبارها + /// + Task> GetAllAsync(bool includeInactive = false, CancellationToken cancellationToken = default); + + /// + /// جستجوی انبارها + /// + Task> SearchAsync( + string? searchTerm = null, + bool? isActive = null, + int skip = 0, + int take = 50, + CancellationToken cancellationToken = default); + + /// + /// شمارش انبارها + /// + Task CountAsync( + string? searchTerm = null, + bool? isActive = null, + CancellationToken cancellationToken = default); + + /// + /// بررسی وجود انبار با کد مشخص + /// + Task ExistsByCodeAsync(string code, long? excludeId = null, CancellationToken cancellationToken = default); + + #endregion + + #region Write Operations + + /// + /// افزودن انبار جدید + /// + Task AddAsync(Warehouse warehouse, CancellationToken cancellationToken = default); + + /// + /// بروزرسانی انبار + /// + Task UpdateAsync(Warehouse warehouse, CancellationToken cancellationToken = default); + + /// + /// حذف انبار (نرم‌افزاری) + /// + Task DeleteAsync(long id, CancellationToken cancellationToken = default); + + /// + /// فعال/غیرفعال کردن انبار + /// + Task SetActiveStatusAsync(long id, bool isActive, CancellationToken cancellationToken = default); + + /// + /// تنظیم انبار پیش‌فرض + /// + Task SetAsDefaultAsync(long id, CancellationToken cancellationToken = default); + + #endregion + + #region Analytics + + /// + /// گزارش آمار کلی انبار + /// + Task<(int TotalProducts, int LowStockProducts, int OutOfStockProducts, decimal TotalValue)> GetWarehouseStatisticsAsync( + long warehouseId, + CancellationToken cancellationToken = default); + + /// + /// گزارش محصولات پرفروش انبار + /// + Task> GetTopSellingProductsAsync( + long warehouseId, + DateTime fromDate, + DateTime toDate, + int count = 10, + CancellationToken cancellationToken = default); + + #endregion +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/DiscountShopCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandHandler.cs b/src/CMSMicroservice.Application/DiscountShopCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandHandler.cs index 002f14c..24f706c 100644 --- a/src/CMSMicroservice.Application/DiscountShopCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandHandler.cs +++ b/src/CMSMicroservice.Application/DiscountShopCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandHandler.cs @@ -8,10 +8,14 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayme public class CompleteOrderPaymentCommandHandler : IRequestHandler { 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 Handle(CompleteOrderPaymentCommand request, CancellationToken cancellationToken) @@ -63,12 +67,18 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler { 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 Handle(CreateDiscountProductCommand request, CancellationToken cancellationToken) @@ -35,6 +40,13 @@ public class CreateDiscountProductCommandHandler : IRequestHandler { 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 Handle(PlaceOrderCommand request, CancellationToken cancellationToken) @@ -152,6 +156,17 @@ public class PlaceOrderCommandHandler : IRequestHandler +/// Command برای ایجاد آیتم موجودی جدید +/// +public record CreateInventoryItemCommand : IRequest +{ + 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; +} + +/// +/// Command برای آپدیت آیتم موجودی +/// +public record UpdateInventoryItemCommand : IRequest +{ + 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; } +} + +/// +/// Command برای آپدیت کردن موجودی یک آیتم +/// +public record UpdateInventoryQuantityCommand : IRequest +{ + 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; } +} + +/// +/// Command برای رزرو کردن موجودی +/// +public record ReserveInventoryCommand : IRequest +{ + 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; } +} + +/// +/// Command برای آزاد کردن موجودی رزرو شده +/// +public record ReleaseReservedInventoryCommand : IRequest +{ + 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; } +} + +/// +/// Command برای کم کردن موجودی (فروش) +/// +public record ReduceInventoryCommand : IRequest +{ + 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; // آیا از موجودی رزرو شده کم شود؟ +} + +/// +/// Command برای اضافه کردن موجودی (خرید) +/// +public record IncreaseInventoryCommand : IRequest +{ + 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; } +} + +/// +/// Command برای حذف آیتم موجودی +/// +public record DeleteInventoryItemCommand : IRequest +{ + public long Id { get; init; } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Features/InventoryItems/Handlers/InventoryItemCommandHandlers.cs b/src/CMSMicroservice.Application/Features/InventoryItems/Handlers/InventoryItemCommandHandlers.cs new file mode 100644 index 0000000..292384f --- /dev/null +++ b/src/CMSMicroservice.Application/Features/InventoryItems/Handlers/InventoryItemCommandHandlers.cs @@ -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; + +/// +/// Handler برای ایجاد آیتم موجودی جدید +/// +public class CreateInventoryItemCommandHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + private readonly IStockMovementRepository _stockMovementRepository; + + public CreateInventoryItemCommandHandler( + IInventoryItemRepository repository, + IStockMovementRepository stockMovementRepository) + { + _repository = repository; + _stockMovementRepository = stockMovementRepository; + } + + public async Task 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; + } +} + +/// +/// Handler برای آپدیت آیتم موجودی +/// +public class UpdateInventoryItemCommandHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public UpdateInventoryItemCommandHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task 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; + } +} + +/// +/// Handler برای آپدیت موجودی +/// +public class UpdateInventoryQuantityCommandHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public UpdateInventoryQuantityCommandHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task 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; + } +} + +/// +/// Handler برای رزرو موجودی +/// +public class ReserveInventoryCommandHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public ReserveInventoryCommandHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task 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); + } +} + +/// +/// Handler برای آزاد کردن موجودی رزرو شده +/// +public class ReleaseReservedInventoryCommandHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public ReleaseReservedInventoryCommandHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task 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); + } +} + +/// +/// Handler برای کم کردن موجودی +/// +public class ReduceInventoryCommandHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public ReduceInventoryCommandHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task 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); + } +} + +/// +/// Handler برای اضافه کردن موجودی +/// +public class IncreaseInventoryCommandHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public IncreaseInventoryCommandHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task 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); + } +} + +/// +/// Handler برای حذف آیتم موجودی +/// +public class DeleteInventoryItemCommandHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public DeleteInventoryItemCommandHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task Handle(DeleteInventoryItemCommand request, CancellationToken cancellationToken) + { + await _repository.DeleteAsync(request.Id, cancellationToken); + return true; + } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Features/InventoryItems/Handlers/InventoryItemQueryHandlers.cs b/src/CMSMicroservice.Application/Features/InventoryItems/Handlers/InventoryItemQueryHandlers.cs new file mode 100644 index 0000000..4684117 --- /dev/null +++ b/src/CMSMicroservice.Application/Features/InventoryItems/Handlers/InventoryItemQueryHandlers.cs @@ -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; + +/// +/// Handler برای دریافت آیتم موجودی به ID +/// +public class GetInventoryItemByIdQueryHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public GetInventoryItemByIdQueryHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task Handle(GetInventoryItemByIdQuery request, CancellationToken cancellationToken) + { + return await _repository.GetByIdAsync(request.Id, cancellationToken); + } +} + +/// +/// Handler برای دریافت آیتم موجودی به Product ID +/// +public class GetInventoryItemByProductIdQueryHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public GetInventoryItemByProductIdQueryHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task Handle(GetInventoryItemByProductIdQuery request, CancellationToken cancellationToken) + { + return await _repository.GetByProductIdAsync(request.ProductId, request.WarehouseId ?? 1, cancellationToken); + } +} + +/// +/// Handler برای دریافت آیتم موجودی به DiscountProduct ID +/// +public class GetInventoryItemByDiscountProductIdQueryHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public GetInventoryItemByDiscountProductIdQueryHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task Handle(GetInventoryItemByDiscountProductIdQuery request, CancellationToken cancellationToken) + { + return await _repository.GetByDiscountProductIdAsync(request.DiscountProductId, request.WarehouseId ?? 1, cancellationToken); + } +} + +/// +/// Handler برای جستجوی آیتم های موجودی +/// +public class SearchInventoryItemsQueryHandler : IRequestHandler> +{ + private readonly IInventoryItemRepository _repository; + + public SearchInventoryItemsQueryHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task> Handle(SearchInventoryItemsQuery request, CancellationToken cancellationToken) + { + return await _repository.SearchAsync( + searchTerm: request.ProductName, + warehouseId: request.WarehouseId, + skip: request.Skip, + take: request.Take, + cancellationToken: cancellationToken); + } +} + +/// +/// Handler برای شمارش آیتم های موجودی +/// +public class GetInventoryItemsCountQueryHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public GetInventoryItemsCountQueryHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task Handle(GetInventoryItemsCountQuery request, CancellationToken cancellationToken) + { + return await _repository.CountAsync( + searchTerm: request.ProductName, + warehouseId: request.WarehouseId, + cancellationToken: cancellationToken); + } +} + +/// +/// Handler برای دریافت آیتم های کم موجود +/// +public class GetLowStockItemsQueryHandler : IRequestHandler> +{ + private readonly IInventoryItemRepository _repository; + + public GetLowStockItemsQueryHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetLowStockItemsQuery request, CancellationToken cancellationToken) + { + return await _repository.GetLowStockItemsAsync(warehouseId: request.WarehouseId ?? 1, cancellationToken: cancellationToken); + } +} + +/// +/// Handler برای دریافت آیتم های ناموجود +/// +public class GetOutOfStockItemsQueryHandler : IRequestHandler> +{ + private readonly IInventoryItemRepository _repository; + + public GetOutOfStockItemsQueryHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetOutOfStockItemsQuery request, CancellationToken cancellationToken) + { + return await _repository.GetOutOfStockItemsAsync(warehouseId: request.WarehouseId ?? 1, cancellationToken: cancellationToken); + } +} + +/// +/// Handler برای چک کردن دسترسی موجودی +/// +public class CheckInventoryAvailabilityQueryHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public CheckInventoryAvailabilityQueryHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task Handle(CheckInventoryAvailabilityQuery request, CancellationToken cancellationToken) + { + var item = await _repository.GetByIdAsync(request.InventoryItemId, cancellationToken); + if (item == null) return false; + return item.AvailableQuantity >= request.RequiredQuantity; + } +} + +/// +/// Handler برای دریافت موجودی قابل دسترس +/// +public class GetAvailableQuantityQueryHandler : IRequestHandler +{ + private readonly IInventoryItemRepository _repository; + + public GetAvailableQuantityQueryHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task Handle(GetAvailableQuantityQuery request, CancellationToken cancellationToken) + { + var item = await _repository.GetByIdAsync(request.InventoryItemId, cancellationToken); + return item?.AvailableQuantity ?? 0; + } +} + +/// +/// Handler برای دریافت آیتم های موجودی در انبار +/// +public class GetWarehouseInventoryItemsQueryHandler : IRequestHandler> +{ + private readonly IInventoryItemRepository _repository; + + public GetWarehouseInventoryItemsQueryHandler(IInventoryItemRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetWarehouseInventoryItemsQuery request, CancellationToken cancellationToken) + { + return await _repository.GetByWarehouseIdAsync(request.WarehouseId, cancellationToken); + } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Features/InventoryItems/Queries/InventoryItemQueries.cs b/src/CMSMicroservice.Application/Features/InventoryItems/Queries/InventoryItemQueries.cs new file mode 100644 index 0000000..9611d40 --- /dev/null +++ b/src/CMSMicroservice.Application/Features/InventoryItems/Queries/InventoryItemQueries.cs @@ -0,0 +1,93 @@ +using MediatR; +using CMSMicroservice.Domain.Entities; + +namespace CMSMicroservice.Application.Features.InventoryItems.Queries; + +/// +/// Query برای دریافت آیتم موجودی به ID +/// +public record GetInventoryItemByIdQuery(long Id) : IRequest; + +/// +/// Query برای دریافت آیتم موجودی به Product ID +/// +public record GetInventoryItemByProductIdQuery(long ProductId, long? WarehouseId = null) : IRequest; + +/// +/// Query برای دریافت آیتم موجودی به DiscountProduct ID +/// +public record GetInventoryItemByDiscountProductIdQuery(long DiscountProductId, long? WarehouseId = null) : IRequest; + +/// +/// Query برای جستجوی آیتم های موجودی +/// +public record SearchInventoryItemsQuery : IRequest> +{ + 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; +} + +/// +/// Query برای دریافت تعداد آیتم های موجودی +/// +public record GetInventoryItemsCountQuery : IRequest +{ + 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; } +} + +/// +/// Query برای دریافت آیتم های کم موجود +/// +public record GetLowStockItemsQuery : IRequest> +{ + public long? WarehouseId { get; init; } + public int Count { get; init; } = 50; +} + +/// +/// Query برای دریافت آیتم های ناموجود +/// +public record GetOutOfStockItemsQuery : IRequest> +{ + public long? WarehouseId { get; init; } + public int Count { get; init; } = 50; +} + +/// +/// Query برای چک کردن دسترسی موجودی +/// +public record CheckInventoryAvailabilityQuery : IRequest +{ + public long InventoryItemId { get; init; } + public int RequiredQuantity { get; init; } +} + +/// +/// Query برای دریافت موجودی قابل دسترس +/// +public record GetAvailableQuantityQuery(long InventoryItemId) : IRequest; + +/// +/// Query برای دریافت آیتم های موجودی در انبار +/// +public record GetWarehouseInventoryItemsQuery : IRequest> +{ + 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; +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Features/StockMovements/Commands/StockMovementCommands.cs b/src/CMSMicroservice.Application/Features/StockMovements/Commands/StockMovementCommands.cs new file mode 100644 index 0000000..7ca42de --- /dev/null +++ b/src/CMSMicroservice.Application/Features/StockMovements/Commands/StockMovementCommands.cs @@ -0,0 +1,44 @@ +using MediatR; +using CMSMicroservice.Domain.Enums; + +namespace CMSMicroservice.Application.Features.StockMovements.Commands; + +/// +/// Command برای ثبت حرکت موجودی +/// +public record CreateStockMovementCommand : IRequest +{ + 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; } +} + +/// +/// Command برای ثبت چندین حرکت موجودی به صورت bulk +/// +public record BulkCreateStockMovementCommand : IRequest> +{ + public List 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; } + } +} + +/// +/// Command برای حذف حرکت موجودی +/// +public record DeleteStockMovementCommand(long Id) : IRequest; \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Features/StockMovements/Handlers/StockMovementCommandHandlers.cs b/src/CMSMicroservice.Application/Features/StockMovements/Handlers/StockMovementCommandHandlers.cs new file mode 100644 index 0000000..b1cc85f --- /dev/null +++ b/src/CMSMicroservice.Application/Features/StockMovements/Handlers/StockMovementCommandHandlers.cs @@ -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; + +/// +/// Handler برای ثبت حرکت موجودی +/// +public class CreateStockMovementCommandHandler : IRequestHandler +{ + private readonly IStockMovementRepository _repository; + private readonly IInventoryItemRepository _inventoryRepository; + + public CreateStockMovementCommandHandler( + IStockMovementRepository repository, + IInventoryItemRepository inventoryRepository) + { + _repository = repository; + _inventoryRepository = inventoryRepository; + } + + public async Task 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; + } +} + +/// +/// Handler برای ثبت چندین حرکت موجودی bulk +/// +public class BulkCreateStockMovementCommandHandler : IRequestHandler> +{ + private readonly IStockMovementRepository _repository; + private readonly IInventoryItemRepository _inventoryRepository; + + public BulkCreateStockMovementCommandHandler( + IStockMovementRepository repository, + IInventoryItemRepository inventoryRepository) + { + _repository = repository; + _inventoryRepository = inventoryRepository; + } + + public async Task> Handle(BulkCreateStockMovementCommand request, CancellationToken cancellationToken) + { + var stockMovements = new List(); + + // بررسی وجود تمام آیتم های موجودی + 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(); + } +} + +/// +/// Handler برای حذف حرکت موجودی +/// +public class DeleteStockMovementCommandHandler : IRequestHandler +{ + private readonly IStockMovementRepository _repository; + + public DeleteStockMovementCommandHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task Handle(DeleteStockMovementCommand request, CancellationToken cancellationToken) + { + await _repository.DeleteAsync(request.Id, cancellationToken); + return true; + } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Features/StockMovements/Handlers/StockMovementQueryHandlers.cs b/src/CMSMicroservice.Application/Features/StockMovements/Handlers/StockMovementQueryHandlers.cs new file mode 100644 index 0000000..522ad28 --- /dev/null +++ b/src/CMSMicroservice.Application/Features/StockMovements/Handlers/StockMovementQueryHandlers.cs @@ -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; + +/// +/// Handler برای دریافت حرکت موجودی به ID +/// +public class GetStockMovementByIdQueryHandler : IRequestHandler +{ + private readonly IStockMovementRepository _repository; + + public GetStockMovementByIdQueryHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task Handle(GetStockMovementByIdQuery request, CancellationToken cancellationToken) + { + return await _repository.GetByIdAsync(request.Id, cancellationToken); + } +} + +/// +/// Handler برای دریافت تاریخچه حرکت موجودی یک آیتم +/// +public class GetInventoryItemMovementHistoryQueryHandler : IRequestHandler> +{ + private readonly IStockMovementRepository _repository; + + public GetInventoryItemMovementHistoryQueryHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetInventoryItemMovementHistoryQuery request, CancellationToken cancellationToken) + { + return await _repository.GetByInventoryItemIdAsync( + request.InventoryItemId, + request.MovementType, + request.FromDate, + request.ToDate, + request.Skip, + request.Take, + cancellationToken); + } +} + +/// +/// Handler برای دریافت حرکات موجودی بر اساس سفارش +/// +public class GetStockMovementsByOrderQueryHandler : IRequestHandler> +{ + private readonly IStockMovementRepository _repository; + + public GetStockMovementsByOrderQueryHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetStockMovementsByOrderQuery request, CancellationToken cancellationToken) + { + return await _repository.GetByOrderIdAsync(request.OrderId, cancellationToken); + } +} + +/// +/// Handler برای دریافت حرکات موجودی بر اساس سفارش تخفیف +/// +public class GetStockMovementsByDiscountOrderQueryHandler : IRequestHandler> +{ + private readonly IStockMovementRepository _repository; + + public GetStockMovementsByDiscountOrderQueryHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetStockMovementsByDiscountOrderQuery request, CancellationToken cancellationToken) + { + return await _repository.GetByDiscountOrderIdAsync(request.DiscountOrderId, cancellationToken); + } +} + +/// +/// Handler برای دریافت حرکات موجودی بر اساس شماره مرجع +/// +public class GetStockMovementsByReferenceQueryHandler : IRequestHandler> +{ + private readonly IStockMovementRepository _repository; + + public GetStockMovementsByReferenceQueryHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetStockMovementsByReferenceQuery request, CancellationToken cancellationToken) + { + return await _repository.GetByReferenceNumberAsync(request.ReferenceNumber, cancellationToken); + } +} + +/// +/// Handler برای دریافت حرکات موجودی بر اساس نوع +/// +public class GetStockMovementsByTypeQueryHandler : IRequestHandler> +{ + private readonly IStockMovementRepository _repository; + + public GetStockMovementsByTypeQueryHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetStockMovementsByTypeQuery request, CancellationToken cancellationToken) + { + return await _repository.GetByMovementTypeAsync( + request.MovementType, + request.FromDate, + request.ToDate, + request.Skip, + request.Take, + cancellationToken); + } +} + +/// +/// Handler برای دریافت آخرین حرکات موجودی +/// +public class GetRecentStockMovementsQueryHandler : IRequestHandler> +{ + private readonly IStockMovementRepository _repository; + + public GetRecentStockMovementsQueryHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetRecentStockMovementsQuery request, CancellationToken cancellationToken) + { + return await _repository.GetRecentMovementsAsync(request.Count, request.MovementType, cancellationToken); + } +} + +/// +/// Handler برای جستجوی حرکات موجودی +/// +public class SearchStockMovementsQueryHandler : IRequestHandler> +{ + private readonly IStockMovementRepository _repository; + + public SearchStockMovementsQueryHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task> 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); + } +} + +/// +/// Handler برای شمارش حرکات موجودی +/// +public class GetStockMovementsCountQueryHandler : IRequestHandler +{ + private readonly IStockMovementRepository _repository; + + public GetStockMovementsCountQueryHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task 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); + } +} + +/// +/// Handler برای دریافت خلاصه حرکات موجودی +/// +public class GetMovementSummaryQueryHandler : IRequestHandler> +{ + private readonly IStockMovementRepository _repository; + + public GetMovementSummaryQueryHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetMovementSummaryQuery request, CancellationToken cancellationToken) + { + return await _repository.GetMovementSummaryAsync( + request.FromDate, + request.ToDate, + request.InventoryItemId, + cancellationToken); + } +} + +/// +/// Handler برای دریافت حجم حرکات روزانه +/// +public class GetDailyMovementVolumeQueryHandler : IRequestHandler> +{ + private readonly IStockMovementRepository _repository; + + public GetDailyMovementVolumeQueryHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetDailyMovementVolumeQuery request, CancellationToken cancellationToken) + { + return await _repository.GetDailyMovementVolumeAsync( + request.FromDate, + request.ToDate, + request.InventoryItemId, + cancellationToken); + } +} + +/// +/// Handler برای دریافت محصولات پر حرکت +/// +public class GetTopMovingProductsQueryHandler : IRequestHandler> +{ + private readonly IStockMovementRepository _repository; + + public GetTopMovingProductsQueryHandler(IStockMovementRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetTopMovingProductsQuery request, CancellationToken cancellationToken) + { + return await _repository.GetTopMovingProductsAsync( + request.FromDate, + request.ToDate, + request.Count, + request.MovementType, + cancellationToken); + } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Features/StockMovements/Queries/StockMovementQueries.cs b/src/CMSMicroservice.Application/Features/StockMovements/Queries/StockMovementQueries.cs new file mode 100644 index 0000000..0557280 --- /dev/null +++ b/src/CMSMicroservice.Application/Features/StockMovements/Queries/StockMovementQueries.cs @@ -0,0 +1,122 @@ +using MediatR; +using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Enums; + +namespace CMSMicroservice.Application.Features.StockMovements.Queries; + +/// +/// Query برای دریافت حرکت موجودی به ID +/// +public record GetStockMovementByIdQuery(long Id) : IRequest; + +/// +/// Query برای دریافت تاریخچه حرکت موجودی یک آیتم +/// +public record GetInventoryItemMovementHistoryQuery : IRequest> +{ + 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; +} + +/// +/// Query برای دریافت حرکات موجودی بر اساس سفارش +/// +public record GetStockMovementsByOrderQuery(long OrderId) : IRequest>; + +/// +/// Query برای دریافت حرکات موجودی بر اساس سفارش تخفیف +/// +public record GetStockMovementsByDiscountOrderQuery(long DiscountOrderId) : IRequest>; + +/// +/// Query برای دریافت حرکات موجودی بر اساس شماره مرجع +/// +public record GetStockMovementsByReferenceQuery(string ReferenceNumber) : IRequest>; + +/// +/// Query برای دریافت حرکات موجودی بر اساس نوع +/// +public record GetStockMovementsByTypeQuery : IRequest> +{ + 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; +} + +/// +/// Query برای دریافت آخرین حرکات موجودی +/// +public record GetRecentStockMovementsQuery : IRequest> +{ + public int Count { get; init; } = 50; + public StockMovementType? MovementType { get; init; } +} + +/// +/// Query برای جستجوی حرکات موجودی +/// +public record SearchStockMovementsQuery : IRequest> +{ + 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; +} + +/// +/// Query برای شمارش حرکات موجودی +/// +public record GetStockMovementsCountQuery : IRequest +{ + 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; } +} + +/// +/// Query برای دریافت خلاصه حرکات موجودی +/// +public record GetMovementSummaryQuery : IRequest> +{ + public DateTime FromDate { get; init; } + public DateTime ToDate { get; init; } + public long? InventoryItemId { get; init; } +} + +/// +/// Query برای دریافت حجم حرکات روزانه +/// +public record GetDailyMovementVolumeQuery : IRequest> +{ + public DateTime FromDate { get; init; } + public DateTime ToDate { get; init; } + public long? InventoryItemId { get; init; } +} + +/// +/// Query برای دریافت محصولات پر حرکت +/// +public record GetTopMovingProductsQuery : IRequest> +{ + public DateTime FromDate { get; init; } + public DateTime ToDate { get; init; } + public int Count { get; init; } = 10; + public StockMovementType? MovementType { get; init; } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Features/Warehouses/Commands/WarehouseCommands.cs b/src/CMSMicroservice.Application/Features/Warehouses/Commands/WarehouseCommands.cs new file mode 100644 index 0000000..f420a5b --- /dev/null +++ b/src/CMSMicroservice.Application/Features/Warehouses/Commands/WarehouseCommands.cs @@ -0,0 +1,66 @@ +using MediatR; + +namespace CMSMicroservice.Application.Features.Warehouses.Commands; + +/// +/// Command برای ایجاد انبار جدید +/// +public record CreateWarehouseCommand : IRequest +{ + 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; +} + +/// +/// Command برای آپدیت انبار +/// +public record UpdateWarehouseCommand : IRequest +{ + 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; } +} + +/// +/// Command برای حذف انبار +/// +public record DeleteWarehouseCommand(long Id) : IRequest; + +/// +/// Command برای تعیین انبار پیش‌فرض +/// +public record SetDefaultWarehouseCommand(long Id) : IRequest; + +/// +/// Command برای فعال/غیرفعال کردن انبار +/// +public record ActivateWarehouseCommand(long Id, bool IsActive) : IRequest; + +/// +/// Command برای ایجاد چندین انبار به صورت bulk +/// +public record BulkCreateWarehousesCommand : IRequest> +{ + public List 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; + } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Features/Warehouses/Handlers/WarehouseCommandHandlers.cs b/src/CMSMicroservice.Application/Features/Warehouses/Handlers/WarehouseCommandHandlers.cs new file mode 100644 index 0000000..d2d9262 --- /dev/null +++ b/src/CMSMicroservice.Application/Features/Warehouses/Handlers/WarehouseCommandHandlers.cs @@ -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; + +/// +/// Handler برای ایجاد انبار جدید +/// +public class CreateWarehouseCommandHandler : IRequestHandler +{ + private readonly IWarehouseRepository _repository; + + public CreateWarehouseCommandHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task 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; + } +} + +/// +/// Handler برای آپدیت انبار +/// +public class UpdateWarehouseCommandHandler : IRequestHandler +{ + private readonly IWarehouseRepository _repository; + + public UpdateWarehouseCommandHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task 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; + } +} + +/// +/// Handler برای حذف انبار +/// +public class DeleteWarehouseCommandHandler : IRequestHandler +{ + private readonly IWarehouseRepository _repository; + + public DeleteWarehouseCommandHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task Handle(DeleteWarehouseCommand request, CancellationToken cancellationToken) + { + try + { + await _repository.DeleteAsync(request.Id, cancellationToken); + return true; + } + catch (InvalidOperationException) + { + // انبار دارای موجودی است + return false; + } + } +} + +/// +/// Handler برای تعیین انبار پیش‌فرض +/// +public class SetDefaultWarehouseCommandHandler : IRequestHandler +{ + private readonly IWarehouseRepository _repository; + + public SetDefaultWarehouseCommandHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task Handle(SetDefaultWarehouseCommand request, CancellationToken cancellationToken) + { + await _repository.SetAsDefaultAsync(request.Id, cancellationToken); + return true; + } +} + +/// +/// Handler برای فعال/غیرفعال کردن انبار +/// +public class ActivateWarehouseCommandHandler : IRequestHandler +{ + private readonly IWarehouseRepository _repository; + + public ActivateWarehouseCommandHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task Handle(ActivateWarehouseCommand request, CancellationToken cancellationToken) + { + await _repository.SetActiveStatusAsync(request.Id, request.IsActive, cancellationToken); + return true; + } +} + +/// +/// Handler برای ایجاد چندین انبار bulk +/// +public class BulkCreateWarehousesCommandHandler : IRequestHandler> +{ + private readonly IWarehouseRepository _repository; + + public BulkCreateWarehousesCommandHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task> Handle(BulkCreateWarehousesCommand request, CancellationToken cancellationToken) + { + var ids = new List(); + + // بررسی تکراری نبودن کدها + 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; + } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Features/Warehouses/Handlers/WarehouseQueryHandlers.cs b/src/CMSMicroservice.Application/Features/Warehouses/Handlers/WarehouseQueryHandlers.cs new file mode 100644 index 0000000..e283bb8 --- /dev/null +++ b/src/CMSMicroservice.Application/Features/Warehouses/Handlers/WarehouseQueryHandlers.cs @@ -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; + +/// +/// Handler برای دریافت انبار به ID +/// +public class GetWarehouseByIdQueryHandler : IRequestHandler +{ + private readonly IWarehouseRepository _repository; + + public GetWarehouseByIdQueryHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task Handle(GetWarehouseByIdQuery request, CancellationToken cancellationToken) + { + return await _repository.GetByIdAsync(request.Id, cancellationToken); + } +} + +/// +/// Handler برای دریافت انبار به کد +/// +public class GetWarehouseByCodeQueryHandler : IRequestHandler +{ + private readonly IWarehouseRepository _repository; + + public GetWarehouseByCodeQueryHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task Handle(GetWarehouseByCodeQuery request, CancellationToken cancellationToken) + { + return await _repository.GetByCodeAsync(request.Code, cancellationToken); + } +} + +/// +/// Handler برای دریافت انبار پیش‌فرض +/// +public class GetDefaultWarehouseQueryHandler : IRequestHandler +{ + private readonly IWarehouseRepository _repository; + + public GetDefaultWarehouseQueryHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task Handle(GetDefaultWarehouseQuery request, CancellationToken cancellationToken) + { + return await _repository.GetDefaultWarehouseAsync(cancellationToken); + } +} + +/// +/// Handler برای دریافت انبارهای فعال +/// +public class GetActiveWarehousesQueryHandler : IRequestHandler> +{ + private readonly IWarehouseRepository _repository; + + public GetActiveWarehousesQueryHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetActiveWarehousesQuery request, CancellationToken cancellationToken) + { + return await _repository.GetActiveWarehousesAsync(cancellationToken); + } +} + +/// +/// Handler برای دریافت تمام انبارها +/// +public class GetAllWarehousesQueryHandler : IRequestHandler> +{ + private readonly IWarehouseRepository _repository; + + public GetAllWarehousesQueryHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetAllWarehousesQuery request, CancellationToken cancellationToken) + { + return await _repository.GetAllAsync(includeInactive: true, cancellationToken); + } +} + +/// +/// Handler برای جستجوی انبارها +/// +public class SearchWarehousesQueryHandler : IRequestHandler> +{ + private readonly IWarehouseRepository _repository; + + public SearchWarehousesQueryHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task> Handle(SearchWarehousesQuery request, CancellationToken cancellationToken) + { + return await _repository.SearchAsync( + request.SearchTerm, + request.IsActive, + request.Skip, + request.Take, + cancellationToken); + } +} + +/// +/// Handler برای شمارش انبارها +/// +public class GetWarehousesCountQueryHandler : IRequestHandler +{ + private readonly IWarehouseRepository _repository; + + public GetWarehousesCountQueryHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task Handle(GetWarehousesCountQuery request, CancellationToken cancellationToken) + { + return await _repository.CountAsync( + request.SearchTerm, + request.IsActive, + cancellationToken); + } +} + +/// +/// Handler برای بررسی وجود انبار +/// +public class WarehouseExistsQueryHandler : IRequestHandler +{ + private readonly IWarehouseRepository _repository; + + public WarehouseExistsQueryHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task Handle(WarehouseExistsQuery request, CancellationToken cancellationToken) + { + var warehouse = await _repository.GetByIdAsync(request.Id, cancellationToken); + return warehouse != null; + } +} + +/// +/// Handler برای بررسی وجود انبار با کد +/// +public class WarehouseExistsByCodeQueryHandler : IRequestHandler +{ + private readonly IWarehouseRepository _repository; + + public WarehouseExistsByCodeQueryHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task Handle(WarehouseExistsByCodeQuery request, CancellationToken cancellationToken) + { + return await _repository.ExistsByCodeAsync(request.Code, request.ExcludeId, cancellationToken); + } +} + +/// +/// Handler برای دریافت آمار انبار +/// +public class GetWarehouseStatisticsQueryHandler : IRequestHandler> +{ + private readonly IWarehouseRepository _repository; + + public GetWarehouseStatisticsQueryHandler(IWarehouseRepository repository) + { + _repository = repository; + } + + public async Task> Handle(GetWarehouseStatisticsQuery request, CancellationToken cancellationToken) + { + var stats = await _repository.GetWarehouseStatisticsAsync(request.Id, cancellationToken); + return new Dictionary + { + ["TotalProducts"] = stats.TotalProducts, + ["LowStockProducts"] = stats.LowStockProducts, + ["OutOfStockProducts"] = stats.OutOfStockProducts, + ["TotalValue"] = stats.TotalValue + }; + } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/Features/Warehouses/Queries/WarehouseQueries.cs b/src/CMSMicroservice.Application/Features/Warehouses/Queries/WarehouseQueries.cs new file mode 100644 index 0000000..295b791 --- /dev/null +++ b/src/CMSMicroservice.Application/Features/Warehouses/Queries/WarehouseQueries.cs @@ -0,0 +1,68 @@ +using MediatR; +using CMSMicroservice.Domain.Entities; + +namespace CMSMicroservice.Application.Features.Warehouses.Queries; + +/// +/// Query برای دریافت انبار به ID +/// +public record GetWarehouseByIdQuery(long Id) : IRequest; + +/// +/// Query برای دریافت انبار به کد +/// +public record GetWarehouseByCodeQuery(string Code) : IRequest; + +/// +/// Query برای دریافت انبار پیش‌فرض +/// +public record GetDefaultWarehouseQuery : IRequest; + +/// +/// Query برای دریافت انبارهای فعال +/// +public record GetActiveWarehousesQuery : IRequest>; + +/// +/// Query برای دریافت تمام انبارها +/// +public record GetAllWarehousesQuery : IRequest>; + +/// +/// Query برای جستجوی انبارها +/// +public record SearchWarehousesQuery : IRequest> +{ + public string? SearchTerm { get; init; } + public bool? IsActive { get; init; } + public int Skip { get; init; } = 0; + public int Take { get; init; } = 100; +} + +/// +/// Query برای شمارش انبارها +/// +public record GetWarehousesCountQuery : IRequest +{ + public string? SearchTerm { get; init; } + public bool? IsActive { get; init; } +} + +/// +/// Query برای بررسی وجود انبار +/// +public record WarehouseExistsQuery(long Id) : IRequest; + +/// +/// Query برای بررسی وجود انبار با کد +/// +public record WarehouseExistsByCodeQuery : IRequest +{ + public string Code { get; init; } = string.Empty; + public long? ExcludeId { get; init; } +} + +/// +/// Query برای دریافت آمار انبار +/// +public record GetWarehouseStatisticsQuery(long Id) : IRequest>; \ No newline at end of file diff --git a/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommand.cs b/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommand.cs index 7bbc290..0172a87 100644 --- a/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommand.cs +++ b/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommand.cs @@ -32,4 +32,9 @@ public class CreateManualPaymentCommand : IRequest /// شماره مرجع یا شماره فیش (اختیاری) /// public string? ReferenceNumber { get; set; } + + /// + /// مسیر تصویر فیش واریزی (اختیاری) + /// + public string? ImagePath { get; set; } } diff --git a/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommandHandler.cs b/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommandHandler.cs index 2f353de..aaec9aa 100644 --- a/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommandHandler.cs +++ b/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommandHandler.cs @@ -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 u.Id == request.UserId, cancellationToken); @@ -48,47 +60,102 @@ public class CreateManualPaymentCommandHandler : IRequestHandler 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; diff --git a/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/ProcessManualMembershipPayment/ProcessManualMembershipPaymentCommandHandler.cs b/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/ProcessManualMembershipPayment/ProcessManualMembershipPaymentCommandHandler.cs index e36f50a..60ebda5 100644 --- a/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/ProcessManualMembershipPayment/ProcessManualMembershipPaymentCommandHandler.cs +++ b/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/ProcessManualMembershipPayment/ProcessManualMembershipPaymentCommandHandler.cs @@ -118,58 +118,19 @@ public class ProcessManualMembershipPaymentCommandHandler : IRequestHandler 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 = "پرداخت دستی با موفقیت ثبت شد" }; diff --git a/src/CMSMicroservice.Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommandHandler.cs b/src/CMSMicroservice.Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommandHandler.cs index 4e38c2e..e4d778d 100644 --- a/src/CMSMicroservice.Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommandHandler.cs +++ b/src/CMSMicroservice.Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommandHandler.cs @@ -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 { 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 Handle(CreateNewProductsCommand request, @@ -17,6 +25,13 @@ public class CreateNewProductsCommandHandler : IRequestHandler 0 }) { diff --git a/src/CMSMicroservice.Application/UserOrderCQ/Commands/CancelOrder/CancelOrderCommandHandler.cs b/src/CMSMicroservice.Application/UserOrderCQ/Commands/CancelOrder/CancelOrderCommandHandler.cs index 1f51d09..233e45d 100644 --- a/src/CMSMicroservice.Application/UserOrderCQ/Commands/CancelOrder/CancelOrderCommandHandler.cs +++ b/src/CMSMicroservice.Application/UserOrderCQ/Commands/CancelOrder/CancelOrderCommandHandler.cs @@ -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 { 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 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 { private readonly IApplicationDbContext _context; + private readonly IInventoryService _inventoryService; private readonly ILogger _logger; private float _vatRate; public SubmitShopBuyOrderCommandHandler( IApplicationDbContext context, + IInventoryService inventoryService, ILogger 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; } diff --git a/src/CMSMicroservice.Domain/Entities/InventoryItem.cs b/src/CMSMicroservice.Domain/Entities/InventoryItem.cs new file mode 100644 index 0000000..5762a0b --- /dev/null +++ b/src/CMSMicroservice.Domain/Entities/InventoryItem.cs @@ -0,0 +1,105 @@ +using CMSMicroservice.Domain.Common; +using CMSMicroservice.Domain.Enums; +using CMSMicroservice.Domain.Entities.DiscountShop; + +namespace CMSMicroservice.Domain.Entities; + +/// +/// موجودی کالا در انبار +/// +public class InventoryItem : BaseAuditableEntity +{ + // ========== شناسه محصول (یکی از دو فیلد زیر پر است) ========== + + /// + /// شناسه محصول فروشگاه معمولی + /// + public long? ProductId { get; set; } + + /// + /// محصول فروشگاه معمولی + /// + public Product? Product { get; set; } + + /// + /// شناسه محصول فروشگاه تخفیفی + /// + public long? DiscountProductId { get; set; } + + /// + /// محصول فروشگاه تخفیفی + /// + public DiscountProduct? DiscountProduct { get; set; } + + // ========== نوع محصول ========== + + /// + /// نوع محصول (معمولی یا تخفیفی) + /// + public ProductType ProductType { get; set; } + + // ========== موجودی ========== + + /// + /// موجودی فعلی (منبع اصلی) + /// + public int Quantity { get; set; } + + /// + /// مقدار رزرو شده برای سفارشات pending + /// + public int ReservedQuantity { get; set; } + + /// + /// موجودی قابل فروش (محاسباتی) + /// + public int AvailableQuantity => Quantity - ReservedQuantity; + + // ========== تنظیمات انبار ========== + + /// + /// آستانه هشدار کم‌موجودی + /// + public int LowStockThreshold { get; set; } = 10; + + /// + /// نقطه سفارش مجدد + /// + public int ReorderPoint { get; set; } = 5; + + /// + /// حداکثر موجودی مجاز + /// + public int MaxStockLevel { get; set; } = 1000; + + // ========== آمار ========== + + /// + /// تاریخ آخرین ورود کالا + /// + public DateTime? LastRestockedAt { get; set; } + + /// + /// تاریخ آخرین فروش + /// + public DateTime? LastSoldAt { get; set; } + + // ========== انبار (برای آینده) ========== + + /// + /// شناسه انبار (پیش‌فرض: انبار اصلی) + /// + public long WarehouseId { get; set; } = 1; + + /// + /// انبار + /// + public Warehouse? Warehouse { get; set; } + + // ========== Navigation Properties ========== + + /// + /// حرکات انبار مرتبط با این آیتم + /// + public ICollection StockMovements { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/CMSMicroservice.Domain/Entities/Payment/ManualPayment.cs b/src/CMSMicroservice.Domain/Entities/Payment/ManualPayment.cs index 4fd2f8d..b5cacba 100644 --- a/src/CMSMicroservice.Domain/Entities/Payment/ManualPayment.cs +++ b/src/CMSMicroservice.Domain/Entities/Payment/ManualPayment.cs @@ -38,6 +38,11 @@ public class ManualPayment : BaseAuditableEntity /// public string? ReferenceNumber { get; set; } + /// + /// مسیر تصویر فیش واریزی (اختیاری) + /// + public string? ImagePath { get; set; } + /// /// وضعیت تایید /// diff --git a/src/CMSMicroservice.Domain/Entities/StockMovement.cs b/src/CMSMicroservice.Domain/Entities/StockMovement.cs new file mode 100644 index 0000000..14cb0f7 --- /dev/null +++ b/src/CMSMicroservice.Domain/Entities/StockMovement.cs @@ -0,0 +1,77 @@ +using CMSMicroservice.Domain.Common; +using CMSMicroservice.Domain.Enums; + +namespace CMSMicroservice.Domain.Entities; + +/// +/// حرکات انبار (تاریخچه تغییرات موجودی) +/// +public class StockMovement : BaseAuditableEntity +{ + // ========== ارتباط با InventoryItem ========== + + /// + /// شناسه آیتم انبار + /// + public long InventoryItemId { get; set; } + + /// + /// آیتم انبار + /// + public InventoryItem InventoryItem { get; set; } = null!; + + // ========== نوع حرکت ========== + + /// + /// نوع حرکت انبار + /// + public StockMovementType MovementType { get; set; } + + // ========== مقادیر ========== + + /// + /// مقدار تغییر (مثبت برای ورود، منفی برای خروج) + /// + public int Quantity { get; set; } + + /// + /// موجودی قبل از این حرکت + /// + public int QuantityBefore { get; set; } + + /// + /// موجودی بعد از این حرکت + /// + public int QuantityAfter { get; set; } + + // ========== مراجع ========== + + /// + /// شناسه سفارش (برای فروش/برگشت در فروشگاه معمولی) + /// + public long? OrderId { get; set; } + + /// + /// شناسه سفارش تخفیفی (برای فروش/برگشت در فروشگاه تخفیفی) + /// + public long? DiscountOrderId { get; set; } + + /// + /// شماره مرجع (مثل شماره فاکتور ورود کالا) + /// + public string? ReferenceNumber { get; set; } + + // ========== توضیحات ========== + + /// + /// یادداشت و توضیحات اضافی + /// + public string? Note { get; set; } + + // ========== کاربر ========== + + /// + /// شناسه کاربری که این عملیات را انجام داده + /// + public long? PerformedByUserId { get; set; } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Domain/Entities/Warehouse.cs b/src/CMSMicroservice.Domain/Entities/Warehouse.cs new file mode 100644 index 0000000..5c92797 --- /dev/null +++ b/src/CMSMicroservice.Domain/Entities/Warehouse.cs @@ -0,0 +1,45 @@ +using CMSMicroservice.Domain.Common; + +namespace CMSMicroservice.Domain.Entities; + +/// +/// انبار (برای آینده - چند انبار) +/// +public class Warehouse : BaseAuditableEntity +{ + // ========== اطلاعات اصلی ========== + + /// + /// نام انبار + /// + public string Name { get; set; } = null!; + + /// + /// کد انبار + /// + public string Code { get; set; } = null!; + + /// + /// آدرس انبار + /// + public string? Address { get; set; } + + // ========== تنظیمات ========== + + /// + /// انبار پیش‌فرض سیستم + /// + public bool IsDefault { get; set; } + + /// + /// وضعیت فعال/غیرفعال + /// + public bool IsActive { get; set; } = true; + + // ========== Navigation Properties ========== + + /// + /// آیتم‌های موجودی در این انبار + /// + public ICollection InventoryItems { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/CMSMicroservice.Domain/Enums/ProductType.cs b/src/CMSMicroservice.Domain/Enums/ProductType.cs new file mode 100644 index 0000000..27be3d6 --- /dev/null +++ b/src/CMSMicroservice.Domain/Enums/ProductType.cs @@ -0,0 +1,17 @@ +namespace CMSMicroservice.Domain.Enums; + +/// +/// نوع محصول در سیستم +/// +public enum ProductType +{ + /// + /// محصول فروشگاه معمولی + /// + RegularProduct = 1, + + /// + /// محصول فروشگاه تخفیفی + /// + DiscountProduct = 2 +} \ No newline at end of file diff --git a/src/CMSMicroservice.Domain/Enums/StockMovementType.cs b/src/CMSMicroservice.Domain/Enums/StockMovementType.cs new file mode 100644 index 0000000..b2d050c --- /dev/null +++ b/src/CMSMicroservice.Domain/Enums/StockMovementType.cs @@ -0,0 +1,75 @@ +namespace CMSMicroservice.Domain.Enums; + +/// +/// نوع حرکت انبار +/// +public enum StockMovementType +{ + // ========== ورودی ========== + + /// + /// موجودی اولیه محصول + /// + InitialStock = 1, + + /// + /// ورود کالا به انبار (خرید از تامین‌کننده) + /// + Restock = 2, + + /// + /// برگشت کالا از مشتری + /// + Return = 3, + + /// + /// انتقال کالا از انبار دیگر + /// + TransferIn = 4, + + // ========== خروجی ========== + + /// + /// فروش کالا به مشتری + /// + Sale = 10, + + /// + /// ضایعات (کالای خراب شده) + /// + Damaged = 11, + + /// + /// مفقودی انبار + /// + Lost = 12, + + /// + /// انتقال کالا به انبار دیگر + /// + TransferOut = 13, + + // ========== تعدیل ========== + + /// + /// تعدیل افزایشی موجودی (انبارگردانی مثبت) + /// + AdjustmentPlus = 20, + + /// + /// تعدیل کاهشی موجودی (انبارگردانی منفی) + /// + AdjustmentMinus = 21, + + // ========== رزرو ========== + + /// + /// رزرو موجودی برای سفارش pending + /// + Reserved = 30, + + /// + /// آزادسازی رزرو (لغو سفارش) + /// + Released = 31 +} \ No newline at end of file diff --git a/src/CMSMicroservice.Infrastructure/DependencyInjection.cs b/src/CMSMicroservice.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..014eb2a --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/DependencyInjection.cs @@ -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; + +/// +/// کلاس تزریق وابستگی برای لایه Infrastructure +/// +public static class DependencyInjection +{ + /// + /// افزودن سرویس های Infrastructure به DI Container + /// + /// IServiceCollection + /// IConfiguration + /// IServiceCollection + public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) + { + // Database Configuration + services.AddDbContext(options => + options.UseSqlServer( + configuration.GetConnectionString("DefaultConnection"), + b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName))); + + // Application Context Interface + services.AddScoped(provider => provider.GetRequiredService()); + + // Repository Pattern Registration + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // Business Services + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs b/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs index ee49e54..02e3f58 100644 --- a/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs @@ -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; + /// + /// Constructor برای design-time (migrations) + /// + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + + /// + /// Constructor اصلی برای runtime + /// public ApplicationDbContext( DbContextOptions 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 Countries => Set(); public DbSet States => Set(); public DbSet Cities => Set(); + + // ============= Inventory Management DbSets ============= + public DbSet Warehouses => Set(); + public DbSet InventoryItems => Set(); + public DbSet StockMovements => Set(); } diff --git a/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContextFactory.cs b/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContextFactory.cs new file mode 100644 index 0000000..7f5782d --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContextFactory.cs @@ -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; + +/// +/// Factory برای ایجاد DbContext در زمان طراحی (migrations, scaffolding) +/// +public class ApplicationDbContextFactory : IDesignTimeDbContextFactory +{ + 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(); + optionsBuilder.UseSqlServer(connectionString, + b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)); + + return new ApplicationDbContext(optionsBuilder.Options); + } +} diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Configurations/InventoryItemConfiguration.cs b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/InventoryItemConfiguration.cs new file mode 100644 index 0000000..3588e26 --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/InventoryItemConfiguration.cs @@ -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; + +/// +/// تنظیمات Entity Framework برای موجودی کالا +/// +public class InventoryItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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(); + + 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"); + } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Configurations/StockMovementConfiguration.cs b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/StockMovementConfiguration.cs new file mode 100644 index 0000000..e4596f1 --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/StockMovementConfiguration.cs @@ -0,0 +1,117 @@ +using CMSMicroservice.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CMSMicroservice.Infrastructure.Persistence.Configurations; + +/// +/// تنظیمات Entity Framework برای حرکات انبار +/// +public class StockMovementConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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(); + + // مقدار تغییر (می‌تواند منفی باشد) + 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"); + } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Configurations/WarehouseConfiguration.cs b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/WarehouseConfiguration.cs new file mode 100644 index 0000000..f700e3e --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/WarehouseConfiguration.cs @@ -0,0 +1,89 @@ +using CMSMicroservice.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CMSMicroservice.Infrastructure.Persistence.Configurations; + +/// +/// تنظیمات Entity Framework برای انبار +/// +public class WarehouseConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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 + }); + } +} \ No newline at end of file diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Migrations/20251231234634_AddInventorySystem.Designer.cs b/src/CMSMicroservice.Infrastructure/Persistence/Migrations/20251231234634_AddInventorySystem.Designer.cs new file mode 100644 index 0000000..6f5604a --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Persistence/Migrations/20251231234634_AddInventorySystem.Designer.cs @@ -0,0 +1,3942 @@ +// +using System; +using CMSMicroservice.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CMSMicroservice.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251231234634_AddInventorySystem")] + partial class AddInventorySystem + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("CMS") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ImagePath") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ParentId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("Categories", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Club.ClubFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("IsActive", "SortOrder") + .HasDatabaseName("IX_ClubFeature_IsActive_SortOrder"); + + b.ToTable("ClubFeatures", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Club.ClubMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActivatedAt") + .HasColumnType("datetime2"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("GiftValue") + .HasColumnType("bigint"); + + b.Property("InitialContribution") + .HasColumnType("bigint"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("PurchaseMethod") + .HasColumnType("int"); + + b.Property("TotalEarned") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_ClubMembership_IsActive"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("IX_ClubMembership_UserId"); + + b.ToTable("ClubMemberships", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Club.UserClubFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClubFeatureId") + .HasColumnType("bigint"); + + b.Property("ClubMembershipId") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("GrantedAt") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ClubFeatureId"); + + b.HasIndex("ClubMembershipId") + .HasDatabaseName("IX_UserClubFeature_ClubMembershipId"); + + b.HasIndex("UserId", "ClubFeatureId") + .IsUnique() + .HasDatabaseName("IX_UserClubFeature_UserId_ClubFeatureId"); + + b.ToTable("UserClubFeatures", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.UserCommissionPayout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BalancesEarned") + .HasColumnType("int"); + + b.Property("BankReferenceId") + .HasColumnType("nvarchar(max)"); + + b.Property("BankTrackingCode") + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IbanNumber") + .HasMaxLength(26) + .HasColumnType("nvarchar(26)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("PaidAt") + .HasColumnType("datetime2"); + + b.Property("PaymentFailureReason") + .HasColumnType("nvarchar(max)"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("ProcessedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RejectionReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("ValuePerBalance") + .HasColumnType("bigint"); + + b.Property("WeekDefinitionId") + .HasColumnType("bigint"); + + b.Property("WeeklyPoolId") + .HasColumnType("bigint"); + + b.Property("WithdrawalMethod") + .HasColumnType("int"); + + b.Property("WithdrawnAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Status") + .HasDatabaseName("IX_UserCommissionPayout_Status"); + + b.HasIndex("WeekDefinitionId"); + + b.HasIndex("WeeklyPoolId") + .HasDatabaseName("IX_UserCommissionPayout_WeeklyPoolId"); + + b.HasIndex("UserId", "WeekDefinitionId") + .IsUnique() + .HasDatabaseName("IX_UserCommissionPayout_UserId_WeekDefinitionId"); + + b.ToTable("UserCommissionPayouts", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.WeeklyCommissionPool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CalculatedAt") + .HasColumnType("datetime2"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsCalculated") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("TotalBalances") + .HasColumnType("int"); + + b.Property("TotalPoolAmount") + .HasColumnType("bigint"); + + b.Property("ValuePerBalance") + .HasColumnType("bigint"); + + b.Property("WeekDefinitionId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("IsCalculated") + .HasDatabaseName("IX_WeeklyCommissionPool_IsCalculated"); + + b.HasIndex("WeekDefinitionId") + .IsUnique() + .HasDatabaseName("IX_WeeklyCommissionPool_WeekDefinitionId"); + + b.ToTable("WeeklyCommissionPools", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.WorkerExecutionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .HasColumnType("nvarchar(max)"); + + b.Property("DurationMs") + .HasColumnType("bigint"); + + b.Property("ErrorCount") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ErrorStackTrace") + .HasColumnType("nvarchar(max)"); + + b.Property("ExecutionId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ProcessedCount") + .HasColumnType("int"); + + b.Property("StartedAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("WeekDefinitionId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("StartedAt"); + + b.HasIndex("Status"); + + b.HasIndex("WeekDefinitionId"); + + b.ToTable("WorkerExecutionLogs", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Configuration.AppVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CurrentVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("MinRequiredVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReleaseNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("RequiresFullCacheClear") + .HasColumnType("bit"); + + b.Property("UpdateMessage") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AppVersions", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Contract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("HtmlContent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Contracts", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DayaLoanContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContractNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsProcessed") + .HasColumnType("bit"); + + b.Property("LastCheckDate") + .HasColumnType("datetime2"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("NationalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProcessedDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TransactionId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TransactionId"); + + b.HasIndex("UserId"); + + b.ToTable("DayaLoanContracts", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ImagePath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ParentCategoryId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("DiscountCategories", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeliveryDescription") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DeliveryStatus") + .HasColumnType("int"); + + b.Property("DiscountBalanceUsed") + .HasColumnType("bigint"); + + b.Property("GatewayAmountPaid") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentDate") + .HasColumnType("datetime2"); + + b.Property("PaymentStatus") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("bigint"); + + b.Property("TrackingCode") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TransactionId") + .HasColumnType("bigint"); + + b.Property("UserAddressId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VatAmount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TransactionId"); + + b.HasIndex("UserAddressId"); + + b.HasIndex("UserId"); + + b.ToTable("DiscountOrders", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountOrderDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountAmount") + .HasColumnType("bigint"); + + b.Property("DiscountOrderId") + .HasColumnType("bigint"); + + b.Property("DiscountPercentUsed") + .HasColumnType("int"); + + b.Property("FinalPrice") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UnitPrice") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DiscountOrderId"); + + b.HasIndex("ProductId"); + + b.ToTable("DiscountOrderDetails", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountProduct", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("FullInformation") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ImagePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("MaxDiscountPercent") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("Rate") + .HasColumnType("int"); + + b.Property("RemainingCount") + .HasColumnType("int"); + + b.Property("SaleCount") + .HasColumnType("int"); + + b.Property("ShortInfomation") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThumbnailPath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ViewCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("DiscountProducts", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId", "CategoryId") + .IsUnique(); + + b.ToTable("DiscountProductCategories", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AltText") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountProductId") + .HasColumnType("bigint"); + + b.Property("ImagePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("ThumbnailPath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Title") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DiscountProductId"); + + b.HasIndex("DiscountProductId", "SortOrder"); + + b.ToTable("DiscountProductImages", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("UserId", "ProductId") + .IsUnique(); + + b.ToTable("DiscountShoppingCarts", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.FactorDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsChangePrice") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UnitDiscount") + .HasColumnType("int"); + + b.Property("UnitDiscountPrice") + .HasColumnType("bigint"); + + b.Property("UnitPrice") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductId"); + + b.ToTable("FactorDetails", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Geography.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ExternalId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Latitude") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Longitude") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Native") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasDefaultValue(""); + + b.Property("StateId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("StateId") + .HasDatabaseName("IX_Cities_StateId"); + + b.ToTable("Cities", "GMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Geography.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Capital") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("CurrencyName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CurrencySymbol") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Emoji") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("EmojiU") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ExternalId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("Iso2") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("nvarchar(2)"); + + b.Property("Iso3") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Latitude") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Longitude") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Native") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("NumericCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("PhoneCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Region") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Subregion") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Tld") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.HasKey("Id"); + + b.ToTable("Countries", "GMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Geography.State", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CountryId") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ExternalId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Latitude") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Longitude") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Native") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasDefaultValue(""); + + b.Property("StateCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("CountryId") + .HasDatabaseName("IX_States_CountryId"); + + b.ToTable("States", "GMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.History.ClubMembershipHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ClubMembershipId") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("NewInitialContribution") + .HasColumnType("bigint"); + + b.Property("NewIsActive") + .HasColumnType("bit"); + + b.Property("OldInitialContribution") + .HasColumnType("bigint"); + + b.Property("OldIsActive") + .HasColumnType("bit"); + + b.Property("PerformedBy") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Reason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Action") + .HasDatabaseName("IX_ClubMembershipHistory_Action"); + + b.HasIndex("ClubMembershipId") + .HasDatabaseName("IX_ClubMembershipHistory_ClubMembershipId"); + + b.HasIndex("UserId", "Created") + .HasDatabaseName("IX_ClubMembershipHistory_UserId_Created"); + + b.ToTable("ClubMembershipHistories", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.History.CommissionPayoutHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("AmountAfter") + .HasColumnType("bigint"); + + b.Property("AmountBefore") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("NewStatus") + .HasColumnType("int"); + + b.Property("OldStatus") + .HasColumnType("int"); + + b.Property("PerformedBy") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Reason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserCommissionPayoutId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("WeekDefinitionId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Action") + .HasDatabaseName("IX_CommissionPayoutHistory_Action"); + + b.HasIndex("UserCommissionPayoutId") + .HasDatabaseName("IX_CommissionPayoutHistory_PayoutId"); + + b.HasIndex("WeekDefinitionId") + .HasDatabaseName("IX_CommissionPayoutHistory_WeekDefinitionId"); + + b.HasIndex("UserId", "Created") + .HasDatabaseName("IX_CommissionPayoutHistory_UserId_Created"); + + b.ToTable("CommissionPayoutHistories", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.History.NetworkMembershipHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("NewLegPosition") + .HasColumnType("int"); + + b.Property("NewParentId") + .HasColumnType("bigint"); + + b.Property("OldLegPosition") + .HasColumnType("int"); + + b.Property("OldParentId") + .HasColumnType("bigint"); + + b.Property("PerformedBy") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Reason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Action") + .HasDatabaseName("IX_NetworkMembershipHistory_Action"); + + b.HasIndex("UserId", "Created") + .HasDatabaseName("IX_NetworkMembershipHistory_UserId_Created"); + + b.ToTable("NetworkMembershipHistories", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountProductId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("LastRestockedAt") + .HasColumnType("datetime2"); + + b.Property("LastSoldAt") + .HasColumnType("datetime2"); + + b.Property("LowStockThreshold") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(10); + + b.Property("MaxStockLevel") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1000); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductType") + .HasColumnType("int"); + + b.Property("Quantity") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("ReorderPoint") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(5); + + b.Property("ReservedQuantity") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CalculatedAt") + .HasColumnType("datetime2"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("FlushedPerSide") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsExpired") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("LeftLegBalances") + .HasColumnType("int"); + + b.Property("LeftLegCarryover") + .HasColumnType("int"); + + b.Property("LeftLegNewMembers") + .HasColumnType("int"); + + b.Property("LeftLegRemainder") + .HasColumnType("int"); + + b.Property("LeftLegTotal") + .HasColumnType("int"); + + b.Property("RightLegBalances") + .HasColumnType("int"); + + b.Property("RightLegCarryover") + .HasColumnType("int"); + + b.Property("RightLegNewMembers") + .HasColumnType("int"); + + b.Property("RightLegRemainder") + .HasColumnType("int"); + + b.Property("RightLegTotal") + .HasColumnType("int"); + + b.Property("SubordinateBalances") + .HasColumnType("int"); + + b.Property("TotalBalances") + .HasColumnType("int"); + + b.Property("TotalFlushed") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("WeekDefinitionId") + .HasColumnType("bigint"); + + b.Property("WeeklyPoolContribution") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("IsExpired") + .HasDatabaseName("IX_NetworkWeeklyBalance_IsExpired"); + + b.HasIndex("WeekDefinitionId") + .HasDatabaseName("IX_NetworkWeeklyBalance_WeekDefinitionId"); + + b.HasIndex("UserId", "WeekDefinitionId") + .IsUnique() + .HasDatabaseName("IX_NetworkWeeklyBalance_UserId_WeekDefinitionId"); + + b.ToTable("NetworkWeeklyBalances", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Order.OrderVAT", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BaseAmount") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsPaid") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaidAt") + .HasColumnType("datetime2"); + + b.Property("TotalAmount") + .HasColumnType("bigint"); + + b.Property("VATAmount") + .HasColumnType("bigint"); + + b.Property("VATRate") + .HasColumnType("decimal(5,4)"); + + b.HasKey("Id"); + + b.HasIndex("Created") + .HasDatabaseName("IX_OrderVATs_Created"); + + b.HasIndex("IsPaid") + .HasDatabaseName("IX_OrderVATs_IsPaid"); + + b.HasIndex("OrderId") + .IsUnique() + .HasDatabaseName("IX_OrderVATs_OrderId"); + + b.ToTable("OrderVATs", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.OtpToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("CodeHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsUsed") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Mobile") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Purpose") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OtpTokens", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ImagePath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Packages", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Payment.ManualPayment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedBy") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ReferenceNumber") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RejectionReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RequestedBy") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TransactionId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("Created"); + + b.HasIndex("RequestedBy"); + + b.HasIndex("Status"); + + b.HasIndex("TransactionId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Status"); + + b.ToTable("ManualPayments", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClubDiscountPercent") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Discount") + .HasColumnType("int"); + + b.Property("FullInformation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ImagePath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsClubExclusive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("Rate") + .HasColumnType("int"); + + b.Property("RemainingCount") + .HasColumnType("int"); + + b.Property("SaleCount") + .HasColumnType("int"); + + b.Property("ShortInfomation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ThumbnailPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ViewCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IsClubExclusive") + .HasDatabaseName("IX_Products_IsClubExclusive"); + + b.ToTable("Products", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.ProductGallery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductImageId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductImageId"); + + b.ToTable("ProductGalleries", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ImagePath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ImageThumbnailPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ProductImages", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.ProductTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("TagId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("TagId"); + + b.ToTable("ProductTags", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.PublicMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ArchivedAt") + .HasColumnType("datetime2"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedByUserId") + .HasColumnType("bigint"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsArchived") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("LinkText") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LinkUrl") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("StartsAt") + .HasColumnType("datetime2"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("ViewCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId") + .HasDatabaseName("IX_PublicMessages_CreatedByUserId"); + + b.HasIndex("ExpiresAt") + .HasDatabaseName("IX_PublicMessages_ExpiresAt"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_PublicMessages_IsActive"); + + b.HasIndex("Priority") + .HasDatabaseName("IX_PublicMessages_Priority"); + + b.HasIndex("StartsAt") + .HasDatabaseName("IX_PublicMessages_StartsAt"); + + b.HasIndex("Type") + .HasDatabaseName("IX_PublicMessages_Type"); + + b.HasIndex("IsActive", "ExpiresAt") + .HasDatabaseName("IX_PublicMessages_IsActive_ExpiresAt"); + + b.ToTable("PublicMessages", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Roles", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.StockMovement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountOrderId") + .HasColumnType("bigint"); + + b.Property("InventoryItemId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("MovementType") + .HasColumnType("int"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PerformedByUserId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("QuantityAfter") + .HasColumnType("int"); + + b.Property("QuantityBefore") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Tags", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentDate") + .HasColumnType("datetime2"); + + b.Property("PaymentStatus") + .HasColumnType("int"); + + b.Property("RefId") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Transactions", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvatarPath") + .HasColumnType("nvarchar(max)"); + + b.Property("BirthDate") + .HasColumnType("datetime2"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DayaCreditReceivedAt") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("EmailNotifications") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("HasReceivedDayaCredit") + .HasColumnType("bit"); + + b.Property("HashPassword") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsMobileVerified") + .HasColumnType("bit"); + + b.Property("IsRulesAccepted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("LegPosition") + .HasColumnType("int"); + + b.Property("Mobile") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("MobileVerifiedAt") + .HasColumnType("datetime2"); + + b.Property("NationalCode") + .HasColumnType("nvarchar(max)"); + + b.Property("NetworkParentId") + .HasColumnType("bigint"); + + b.Property("PackagePurchaseMethod") + .HasColumnType("int"); + + b.Property("PushNotifications") + .HasColumnType("bit"); + + b.Property("ReferralCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RulesAcceptedAt") + .HasColumnType("datetime2"); + + b.Property("SmsNotifications") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("LegPosition") + .HasDatabaseName("IX_User_LegPosition"); + + b.HasIndex("NetworkParentId") + .HasDatabaseName("IX_User_NetworkParentId"); + + b.ToTable("Users", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDefault") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserAddresses", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("UserId"); + + b.ToTable("UserCarts", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContractId") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("SignGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SignedPdfFile") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ContractId"); + + b.HasIndex("UserId"); + + b.ToTable("UserContracts", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeliveryDescription") + .HasColumnType("nvarchar(max)"); + + b.Property("DeliveryStatus") + .HasColumnType("int"); + + b.Property("HasVAT") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("OrderVATId") + .HasColumnType("bigint"); + + b.Property("PackageId") + .HasColumnType("bigint"); + + b.Property("PaymentDate") + .HasColumnType("datetime2"); + + b.Property("PaymentMethod") + .HasColumnType("int"); + + b.Property("PaymentStatus") + .HasColumnType("int"); + + b.Property("TrackingCode") + .HasColumnType("nvarchar(max)"); + + b.Property("TransactionId") + .HasColumnType("bigint"); + + b.Property("UserAddressId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrderVATId"); + + b.HasIndex("PackageId"); + + b.HasIndex("TransactionId"); + + b.HasIndex("UserAddressId"); + + b.HasIndex("UserId"); + + b.ToTable("UserOrders", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserPackagePurchase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PackageId") + .HasColumnType("bigint"); + + b.Property("PurchaseMethod") + .HasColumnType("int"); + + b.Property("PurchasedAt") + .HasColumnType("datetime2"); + + b.Property("TransactionId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PackageId") + .HasDatabaseName("IX_UserPackagePurchase_PackageId"); + + b.HasIndex("PurchasedAt") + .HasDatabaseName("IX_UserPackagePurchase_PurchasedAt"); + + b.HasIndex("TransactionId"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_UserPackagePurchase_UserId"); + + b.HasIndex("UserId", "PurchasedAt") + .HasDatabaseName("IX_UserPackagePurchase_UserId_PurchasedAt"); + + b.ToTable("UserPackagePurchases", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("UserRoles", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserWallet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountBalance") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("NetworkBalance") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserWallets", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserWalletChangeLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChangeDiscountValue") + .HasColumnType("bigint"); + + b.Property("ChangeNerworkValue") + .HasColumnType("bigint"); + + b.Property("ChangeValue") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CurrentBalance") + .HasColumnType("bigint"); + + b.Property("CurrentDiscountBalance") + .HasColumnType("bigint"); + + b.Property("CurrentNetworkBalance") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsIncrease") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RefrenceId") + .HasColumnType("bigint"); + + b.Property("WalletId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("WalletId"); + + b.ToTable("UserWalletChangeLogs", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsDefault") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("GregorianWeekNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("GregorianYear") + .HasColumnType("int"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("PersianWeekNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("PersianYear") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("WeekOrder") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GregorianWeekNumber") + .IsUnique() + .HasDatabaseName("IX_WeekDefinition_GregorianWeekNumber"); + + b.HasIndex("GregorianYear") + .HasDatabaseName("IX_WeekDefinition_GregorianYear"); + + b.HasIndex("PersianWeekNumber") + .HasDatabaseName("IX_WeekDefinition_PersianWeekNumber"); + + b.HasIndex("PersianYear") + .HasDatabaseName("IX_WeekDefinition_PersianYear"); + + b.HasIndex("StartDate") + .HasDatabaseName("IX_WeekDefinition_StartDate"); + + b.ToTable("WeekDefinitions", "CMS"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Category", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Category", "Parent") + .WithMany("Categories") + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Club.ClubMembership", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithOne("ClubMembership") + .HasForeignKey("CMSMicroservice.Domain.Entities.Club.ClubMembership", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Club.UserClubFeature", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Club.ClubFeature", "ClubFeature") + .WithMany("UserClubFeatures") + .HasForeignKey("ClubFeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.Club.ClubMembership", "ClubMembership") + .WithMany("UserClubFeatures") + .HasForeignKey("ClubMembershipId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany("UserClubFeatures") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ClubFeature"); + + b.Navigation("ClubMembership"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.UserCommissionPayout", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany("CommissionPayouts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.WeekDefinition", "WeekDefinition") + .WithMany("UserCommissionPayouts") + .HasForeignKey("WeekDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.Commission.WeeklyCommissionPool", "WeeklyPool") + .WithMany("UserCommissionPayouts") + .HasForeignKey("WeeklyPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("WeekDefinition"); + + b.Navigation("WeeklyPool"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.WeeklyCommissionPool", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.WeekDefinition", "WeekDefinition") + .WithMany("WeeklyCommissionPools") + .HasForeignKey("WeekDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("WeekDefinition"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.WorkerExecutionLog", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.WeekDefinition", "WeekDefinition") + .WithMany("WorkerExecutionLogs") + .HasForeignKey("WeekDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("WeekDefinition"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DayaLoanContract", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Transaction", "Transaction") + .WithMany() + .HasForeignKey("TransactionId"); + + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany("DayaLoanContracts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Transaction"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountCategory", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.DiscountShop.DiscountCategory", "ParentCategory") + .WithMany("ChildCategories") + .HasForeignKey("ParentCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountOrder", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Transaction", "Transaction") + .WithMany() + .HasForeignKey("TransactionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("CMSMicroservice.Domain.Entities.UserAddress", "UserAddress") + .WithMany() + .HasForeignKey("UserAddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany("DiscountOrders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Transaction"); + + b.Navigation("User"); + + b.Navigation("UserAddress"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountOrderDetail", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.DiscountShop.DiscountOrder", "DiscountOrder") + .WithMany("OrderDetails") + .HasForeignKey("DiscountOrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.DiscountShop.DiscountProduct", "Product") + .WithMany("OrderDetails") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DiscountOrder"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountProductCategory", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.DiscountShop.DiscountCategory", "Category") + .WithMany("ProductCategories") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.DiscountShop.DiscountProduct", "Product") + .WithMany("ProductCategories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountProductImage", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.DiscountShop.DiscountProduct", "DiscountProduct") + .WithMany("Images") + .HasForeignKey("DiscountProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountProduct"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountShoppingCart", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.DiscountShop.DiscountProduct", "Product") + .WithMany("ShoppingCarts") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany("DiscountShoppingCarts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.FactorDetails", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.UserOrder", "Order") + .WithMany("FactorDetails") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.Product", "Product") + .WithMany("FactorDetails") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Geography.City", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Geography.State", "State") + .WithMany("Cities") + .HasForeignKey("StateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("State"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Geography.State", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Geography.Country", "Country") + .WithMany("States") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.History.ClubMembershipHistory", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Club.ClubMembership", "ClubMembership") + .WithMany("ClubMembershipHistories") + .HasForeignKey("ClubMembershipId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ClubMembership"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.History.CommissionPayoutHistory", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Commission.UserCommissionPayout", "UserCommissionPayout") + .WithMany("CommissionPayoutHistories") + .HasForeignKey("UserCommissionPayoutId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.WeekDefinition", "WeekDefinition") + .WithMany("CommissionPayoutHistories") + .HasForeignKey("WeekDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("UserCommissionPayout"); + + 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") + .WithMany("NetworkWeeklyBalances") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.WeekDefinition", "WeekDefinition") + .WithMany("NetworkWeeklyBalances") + .HasForeignKey("WeekDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("WeekDefinition"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Order.OrderVAT", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.UserOrder", "Order") + .WithOne() + .HasForeignKey("CMSMicroservice.Domain.Entities.Order.OrderVAT", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Payment.ManualPayment", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Transaction", "Transaction") + .WithMany() + .HasForeignKey("TransactionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Transaction"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.ProductCategory", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Category", "Category") + .WithMany("ProductCategories") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.Product", "Product") + .WithMany("ProductCategories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.ProductGallery", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Product", "Product") + .WithMany("ProductGalleries") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.ProductImage", "ProductImage") + .WithMany("ProductGalleries") + .HasForeignKey("ProductImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("ProductImage"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.ProductTag", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Product", "Product") + .WithMany("ProductTags") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.Tag", "Tag") + .WithMany("ProductTags") + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + 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") + .WithMany("NetworkChildren") + .HasForeignKey("NetworkParentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("NetworkParent"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserAddress", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany("UserAddresses") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserCart", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Product", "Product") + .WithMany("UserCarts") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany("UserCarts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserContract", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Contract", "Contract") + .WithMany("UserContracts") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany("UserContracts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Order.OrderVAT", "OrderVAT") + .WithMany() + .HasForeignKey("OrderVATId"); + + b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package") + .WithMany("UserOrders") + .HasForeignKey("PackageId"); + + b.HasOne("CMSMicroservice.Domain.Entities.Transaction", "Transaction") + .WithMany("UserOrders") + .HasForeignKey("TransactionId"); + + b.HasOne("CMSMicroservice.Domain.Entities.UserAddress", "UserAddress") + .WithMany("UserOrders") + .HasForeignKey("UserAddressId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany("UserOrders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrderVAT"); + + b.Navigation("Package"); + + b.Navigation("Transaction"); + + b.Navigation("User"); + + b.Navigation("UserAddress"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserPackagePurchase", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.UserOrder", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.Transaction", "Transaction") + .WithMany() + .HasForeignKey("TransactionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Package"); + + b.Navigation("Transaction"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserRole", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.Role", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserWallet", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.User", "User") + .WithMany("UserWallets") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserWalletChangeLog", b => + { + b.HasOne("CMSMicroservice.Domain.Entities.UserWallet", "Wallet") + .WithMany("UserWalletChangeLogs") + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Category", b => + { + b.Navigation("Categories"); + + b.Navigation("ProductCategories"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Club.ClubFeature", b => + { + b.Navigation("UserClubFeatures"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Club.ClubMembership", b => + { + b.Navigation("ClubMembershipHistories"); + + b.Navigation("UserClubFeatures"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.UserCommissionPayout", b => + { + b.Navigation("CommissionPayoutHistories"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.WeeklyCommissionPool", b => + { + b.Navigation("UserCommissionPayouts"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Contract", b => + { + b.Navigation("UserContracts"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountCategory", b => + { + b.Navigation("ChildCategories"); + + b.Navigation("ProductCategories"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountOrder", b => + { + b.Navigation("OrderDetails"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.DiscountShop.DiscountProduct", b => + { + b.Navigation("Images"); + + b.Navigation("OrderDetails"); + + b.Navigation("ProductCategories"); + + b.Navigation("ShoppingCarts"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Geography.Country", b => + { + b.Navigation("States"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Geography.State", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.InventoryItem", b => + { + b.Navigation("StockMovements"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Package", b => + { + b.Navigation("UserOrders"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Product", b => + { + b.Navigation("FactorDetails"); + + b.Navigation("ProductCategories"); + + b.Navigation("ProductGalleries"); + + b.Navigation("ProductTags"); + + b.Navigation("UserCarts"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.ProductImage", b => + { + b.Navigation("ProductGalleries"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Role", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Tag", b => + { + b.Navigation("ProductTags"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Transaction", b => + { + b.Navigation("UserOrders"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.User", b => + { + b.Navigation("ClubMembership"); + + b.Navigation("CommissionPayouts"); + + b.Navigation("DayaLoanContracts"); + + b.Navigation("DiscountOrders"); + + b.Navigation("DiscountShoppingCarts"); + + b.Navigation("NetworkChildren"); + + b.Navigation("NetworkWeeklyBalances"); + + b.Navigation("UserAddresses"); + + b.Navigation("UserCarts"); + + b.Navigation("UserClubFeatures"); + + b.Navigation("UserContracts"); + + b.Navigation("UserOrders"); + + b.Navigation("UserRoles"); + + b.Navigation("UserWallets"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserAddress", b => + { + b.Navigation("UserOrders"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b => + { + b.Navigation("FactorDetails"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserWallet", b => + { + b.Navigation("UserWalletChangeLogs"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Warehouse", b => + { + b.Navigation("InventoryItems"); + }); + + modelBuilder.Entity("CMSMicroservice.Domain.Entities.WeekDefinition", b => + { + b.Navigation("CommissionPayoutHistories"); + + b.Navigation("NetworkWeeklyBalances"); + + b.Navigation("UserCommissionPayouts"); + + b.Navigation("WeeklyCommissionPools"); + + b.Navigation("WorkerExecutionLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Migrations/20251231234634_AddInventorySystem.cs b/src/CMSMicroservice.Infrastructure/Persistence/Migrations/20251231234634_AddInventorySystem.cs new file mode 100644 index 0000000..d2b5020 --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Persistence/Migrations/20251231234634_AddInventorySystem.cs @@ -0,0 +1,242 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CMSMicroservice.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddInventorySystem : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Warehouses", + schema: "CMS", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Code = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Address = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + IsDefault = table.Column(type: "bit", nullable: false, defaultValue: false), + IsActive = table.Column(type: "bit", nullable: false, defaultValue: true), + Created = table.Column(type: "datetime2", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + LastModified = table.Column(type: "datetime2", nullable: true), + LastModifiedBy = table.Column(type: "nvarchar(max)", nullable: true), + IsDeleted = table.Column(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(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ProductId = table.Column(type: "bigint", nullable: true), + DiscountProductId = table.Column(type: "bigint", nullable: true), + ProductType = table.Column(type: "int", nullable: false), + Quantity = table.Column(type: "int", nullable: false, defaultValue: 0), + ReservedQuantity = table.Column(type: "int", nullable: false, defaultValue: 0), + LowStockThreshold = table.Column(type: "int", nullable: false, defaultValue: 10), + ReorderPoint = table.Column(type: "int", nullable: false, defaultValue: 5), + MaxStockLevel = table.Column(type: "int", nullable: false, defaultValue: 1000), + LastRestockedAt = table.Column(type: "datetime2", nullable: true), + LastSoldAt = table.Column(type: "datetime2", nullable: true), + WarehouseId = table.Column(type: "bigint", nullable: false, defaultValue: 1L), + Created = table.Column(type: "datetime2", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + LastModified = table.Column(type: "datetime2", nullable: true), + LastModifiedBy = table.Column(type: "nvarchar(max)", nullable: true), + IsDeleted = table.Column(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(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + InventoryItemId = table.Column(type: "bigint", nullable: false), + MovementType = table.Column(type: "int", nullable: false), + Quantity = table.Column(type: "int", nullable: false), + QuantityBefore = table.Column(type: "int", nullable: false), + QuantityAfter = table.Column(type: "int", nullable: false), + OrderId = table.Column(type: "bigint", nullable: true), + DiscountOrderId = table.Column(type: "bigint", nullable: true), + ReferenceNumber = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + Note = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + PerformedByUserId = table.Column(type: "bigint", nullable: true), + Created = table.Column(type: "datetime2", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + LastModified = table.Column(type: "datetime2", nullable: true), + LastModifiedBy = table.Column(type: "nvarchar(max)", nullable: true), + IsDeleted = table.Column(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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "StockMovements", + schema: "CMS"); + + migrationBuilder.DropTable( + name: "InventoryItems", + schema: "CMS"); + + migrationBuilder.DropTable( + name: "Warehouses", + schema: "CMS"); + } + } +} diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/CMSMicroservice.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index c165802..f95736b 100644 --- a/src/CMSMicroservice.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/CMSMicroservice.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1503,6 +1503,102 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations b.ToTable("NetworkMembershipHistories", "CMS"); }); + modelBuilder.Entity("CMSMicroservice.Domain.Entities.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountProductId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("LastRestockedAt") + .HasColumnType("datetime2"); + + b.Property("LastSoldAt") + .HasColumnType("datetime2"); + + b.Property("LowStockThreshold") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(10); + + b.Property("MaxStockLevel") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1000); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductType") + .HasColumnType("int"); + + b.Property("Quantity") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("ReorderPoint") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(5); + + b.Property("ReservedQuantity") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("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("Id") @@ -2210,6 +2306,97 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations b.ToTable("Roles", "CMS"); }); + modelBuilder.Entity("CMSMicroservice.Domain.Entities.StockMovement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountOrderId") + .HasColumnType("bigint"); + + b.Property("InventoryItemId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("MovementType") + .HasColumnType("int"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PerformedByUserId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("QuantityAfter") + .HasColumnType("int"); + + b.Property("QuantityBefore") + .HasColumnType("int"); + + b.Property("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("Id") @@ -2818,6 +3005,83 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations b.ToTable("UserWalletChangeLogs", "CMS"); }); + modelBuilder.Entity("CMSMicroservice.Domain.Entities.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsDefault") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("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("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"); diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Repositories/InventoryItemRepository.cs b/src/CMSMicroservice.Infrastructure/Persistence/Repositories/InventoryItemRepository.cs new file mode 100644 index 0000000..d0e212b --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Persistence/Repositories/InventoryItemRepository.cs @@ -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; + +/// +/// Repository implementation برای مدیریت موجودی محصولات +/// +public class InventoryItemRepository : IInventoryItemRepository +{ + private readonly IApplicationDbContext _context; + + public InventoryItemRepository(IApplicationDbContext context) + { + _context = context; + } + + #region Read Operations + + public async Task 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 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 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> 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> 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> 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> 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 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 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 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 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 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 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(); + + 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 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(); + + 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 +} diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Repositories/StockMovementRepository.cs b/src/CMSMicroservice.Infrastructure/Persistence/Repositories/StockMovementRepository.cs new file mode 100644 index 0000000..6f0af38 --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Persistence/Repositories/StockMovementRepository.cs @@ -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; + +/// +/// Repository implementation برای مدیریت حرکات موجودی +/// +public class StockMovementRepository : IStockMovementRepository +{ + private readonly IApplicationDbContext _context; + + public StockMovementRepository(IApplicationDbContext context) + { + _context = context; + } + + #region Read Operations + + public async Task 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> 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> 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> 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> 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> 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> 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> 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 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 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> BulkAddAsync(List stockMovements, CancellationToken cancellationToken = default) + { + _context.StockMovements.AddRange(stockMovements); + await _context.SaveChangesAsync(cancellationToken); + return stockMovements; + } + + #endregion + + #region Analytics & Reports + + public async Task> 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> 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> 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 +} diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Repositories/WarehouseRepository.cs b/src/CMSMicroservice.Infrastructure/Persistence/Repositories/WarehouseRepository.cs new file mode 100644 index 0000000..ef58f1f --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Persistence/Repositories/WarehouseRepository.cs @@ -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; + +/// +/// Repository implementation برای مدیریت انبارها +/// +public class WarehouseRepository : IWarehouseRepository +{ + private readonly IApplicationDbContext _context; + + public WarehouseRepository(IApplicationDbContext context) + { + _context = context; + } + + #region Read Operations + + public async Task 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 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 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> GetActiveWarehousesAsync(CancellationToken cancellationToken = default) + { + return await _context.Warehouses + .Where(w => w.IsActive) + .OrderBy(w => w.Name) + .ToListAsync(cancellationToken); + } + + public async Task> 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> 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 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 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 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> 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 +} diff --git a/src/CMSMicroservice.Infrastructure/Services/InventoryService.cs b/src/CMSMicroservice.Infrastructure/Services/InventoryService.cs new file mode 100644 index 0000000..423e7ee --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Services/InventoryService.cs @@ -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; + +/// +/// پیاده‌سازی سرویس مدیریت موجودی +/// این سرویس Source of Truth برای موجودی است و مسئول همگام‌سازی با Product.RemainingCount +/// +public class InventoryService : IInventoryService +{ + private readonly ApplicationDbContext _context; + private readonly ILogger _logger; + private const long DefaultWarehouseId = 1; // انبار پیش‌فرض + + public InventoryService(ApplicationDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + #region Initialization + + public async Task 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 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 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 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> 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> 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(); + + 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 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 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 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 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 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 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 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 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 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 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 + + /// + /// همگام‌سازی موجودی InventoryItem با Product.RemainingCount + /// این متد اطمینان می‌دهد که داده‌های قدیمی (RemainingCount) همیشه با سیستم جدید sync است + /// + 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 + } + } + + /// + /// ثبت حرکت موجودی در StockMovements + /// + 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 +} diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj index f63d02a..effb483 100644 --- a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj +++ b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj @@ -3,7 +3,7 @@ net9.0 enable enable - 0.0.164 + 0.0.165 None False False @@ -59,6 +59,8 @@ + + diff --git a/src/CMSMicroservice.Protobuf/Protos/inventory.proto b/src/CMSMicroservice.Protobuf/Protos/inventory.proto new file mode 100644 index 0000000..de68f74 --- /dev/null +++ b/src/CMSMicroservice.Protobuf/Protos/inventory.proto @@ -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; +} diff --git a/src/CMSMicroservice.Protobuf/Protos/manualpayment.proto b/src/CMSMicroservice.Protobuf/Protos/manualpayment.proto index a53a107..a12b134 100644 --- a/src/CMSMicroservice.Protobuf/Protos/manualpayment.proto +++ b/src/CMSMicroservice.Protobuf/Protos/manualpayment.proto @@ -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 diff --git a/src/CMSMicroservice.WebApi/Services/InventoryService.cs b/src/CMSMicroservice.WebApi/Services/InventoryService.cs new file mode 100644 index 0000000..fed831f --- /dev/null +++ b/src/CMSMicroservice.WebApi/Services/InventoryService.cs @@ -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; + +/// +/// gRPC Service for Inventory Management +/// +public class InventoryService : InventoryContract.InventoryContractBase +{ + private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS; + + public InventoryService(IDispatchRequestToCQRS dispatchRequestToCQRS) + { + _dispatchRequestToCQRS = dispatchRequestToCQRS; + } + + #region Warehouse Management + + public override async Task CreateWarehouse(CreateWarehouseRequest request, ServerCallContext context) + { + var id = await _dispatchRequestToCQRS.Handle(request, context); + return new CreateWarehouseResponse { Id = id }; + } + + public override async Task UpdateWarehouse(UpdateWarehouseRequest request, ServerCallContext context) + { + await _dispatchRequestToCQRS.Handle(request, context); + return new Empty(); + } + + public override async Task DeleteWarehouse(DeleteWarehouseRequest request, ServerCallContext context) + { + await _dispatchRequestToCQRS.Handle(request, context); + return new Empty(); + } + + public override async Task GetWarehouse(GetWarehouseRequest request, ServerCallContext context) + { + var warehouse = await _dispatchRequestToCQRS.Handle(request, context); + return new GetWarehouseResponse + { + Warehouse = warehouse?.Adapt() + }; + } + + public override async Task GetAllWarehouses(GetAllWarehousesRequest request, ServerCallContext context) + { + var warehouses = await _dispatchRequestToCQRS.Handle>(request, context); + var response = new GetAllWarehousesResponse { TotalCount = warehouses.Count }; + response.Warehouses.AddRange(warehouses.Select(w => w.Adapt())); + return response; + } + + public override async Task SetDefaultWarehouse(SetDefaultWarehouseRequest request, ServerCallContext context) + { + await _dispatchRequestToCQRS.Handle(request, context); + return new Empty(); + } + + #endregion + + #region Inventory Item Management + + public override async Task GetInventoryItem(GetInventoryItemRequest request, ServerCallContext context) + { + var item = await _dispatchRequestToCQRS.Handle(request, context); + return new GetInventoryItemResponse + { + Item = item?.Adapt() + }; + } + + public override async Task GetInventoryByProduct(GetInventoryByProductRequest request, ServerCallContext context) + { + InventoryItem? item; + if (request.ProductType == Protobuf.Protos.Inventory.ProductType.RegularProduct) + { + item = await _dispatchRequestToCQRS.Handle(request, context); + } + else + { + item = await _dispatchRequestToCQRS.Handle(request, context); + } + return new GetInventoryByProductResponse + { + Item = item?.Adapt() + }; + } + + public override async Task GetAllInventoryItems(GetAllInventoryItemsRequest request, ServerCallContext context) + { + var items = await _dispatchRequestToCQRS.Handle>(request, context); + var response = new GetAllInventoryItemsResponse { TotalCount = items.Count }; + response.Items.AddRange(items.Select(i => i.Adapt())); + return response; + } + + public override async Task GetLowStockItems(GetLowStockItemsRequest request, ServerCallContext context) + { + var items = await _dispatchRequestToCQRS.Handle>(request, context); + var response = new GetLowStockItemsResponse { TotalCount = items.Count }; + response.Items.AddRange(items.Select(i => i.Adapt())); + return response; + } + + public override async Task UpdateInventorySettings(UpdateInventorySettingsRequest request, ServerCallContext context) + { + await _dispatchRequestToCQRS.Handle(request, context); + return new Empty(); + } + + #endregion + + #region Stock Operations + + public override async Task AddStock(AddStockRequest request, ServerCallContext context) + { + var success = await _dispatchRequestToCQRS.Handle(request, context); + return new AddStockResponse + { + InventoryItemId = 0, // Will be filled by mapping + NewQuantity = 0 + }; + } + + public override async Task AdjustStock(AdjustStockRequest request, ServerCallContext context) + { + await _dispatchRequestToCQRS.Handle(request, context); + return new AdjustStockResponse(); + } + + public override async Task ReserveStock(ReserveStockRequest request, ServerCallContext context) + { + var success = await _dispatchRequestToCQRS.Handle(request, context); + return new ReserveStockResponse + { + Success = success, + Message = success ? "Stock reserved successfully" : "Failed to reserve stock" + }; + } + + public override async Task ReleaseReservation(ReleaseReservationRequest request, ServerCallContext context) + { + await _dispatchRequestToCQRS.Handle(request, context); + return new Empty(); + } + + public override async Task ConfirmSale(ConfirmSaleRequest request, ServerCallContext context) + { + await _dispatchRequestToCQRS.Handle(request, context); + return new Empty(); + } + + public override async Task ProcessReturn(ProcessReturnRequest request, ServerCallContext context) + { + await _dispatchRequestToCQRS.Handle(request, context); + return new ProcessReturnResponse { NewQuantity = 0 }; + } + + public override async Task RecordLoss(RecordLossRequest request, ServerCallContext context) + { + await _dispatchRequestToCQRS.Handle(request, context); + return new Empty(); + } + + #endregion + + #region Bulk Operations + + public override async Task BulkAddStock(BulkAddStockRequest request, ServerCallContext context) + { + // TODO: Implement bulk add stock + return new BulkAddStockResponse { SuccessCount = 0, FailedCount = 0 }; + } + + public override async Task 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 GetStockMovements(GetStockMovementsRequest request, ServerCallContext context) + { + var movements = await _dispatchRequestToCQRS.Handle>(request, context); + var response = new GetStockMovementsResponse { TotalCount = movements.Count }; + response.Movements.AddRange(movements.Select(m => m.Adapt())); + return response; + } + + public override async Task GetStockMovementsByInventoryItem(GetStockMovementsByInventoryItemRequest request, ServerCallContext context) + { + var movements = await _dispatchRequestToCQRS.Handle>(request, context); + var response = new GetStockMovementsByInventoryItemResponse { TotalCount = movements.Count }; + response.Movements.AddRange(movements.Select(m => m.Adapt())); + return response; + } + + #endregion + + #region Reports + + public override async Task GetInventorySummary(GetInventorySummaryRequest request, ServerCallContext context) + { + // TODO: Implement summary query + return new GetInventorySummaryResponse(); + } + + public override async Task GetStockValueReport(GetStockValueReportRequest request, ServerCallContext context) + { + // TODO: Implement stock value report + return new GetStockValueReportResponse(); + } + + #endregion +}