Implement Inventory Management Service with CRUD operations for warehouses and inventory items, stock operations, and bulk processing capabilities.
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m48s
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m48s
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Interfaces.Repositories;
|
||||
using CMSMicroservice.Infrastructure.Persistence;
|
||||
using CMSMicroservice.Infrastructure.Persistence.Repositories;
|
||||
using CMSMicroservice.Infrastructure.Services;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// کلاس تزریق وابستگی برای لایه Infrastructure
|
||||
/// </summary>
|
||||
public static class DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// افزودن سرویس های Infrastructure به DI Container
|
||||
/// </summary>
|
||||
/// <param name="services">IServiceCollection</param>
|
||||
/// <param name="configuration">IConfiguration</param>
|
||||
/// <returns>IServiceCollection</returns>
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// Database Configuration
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlServer(
|
||||
configuration.GetConnectionString("DefaultConnection"),
|
||||
b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
|
||||
|
||||
// Application Context Interface
|
||||
services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
|
||||
|
||||
// Repository Pattern Registration
|
||||
services.AddScoped<IInventoryItemRepository, InventoryItemRepository>();
|
||||
services.AddScoped<IStockMovementRepository, StockMovementRepository>();
|
||||
services.AddScoped<IWarehouseRepository, WarehouseRepository>();
|
||||
|
||||
// Business Services
|
||||
services.AddScoped<IInventoryService, InventoryService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,20 @@ namespace CMSMicroservice.Infrastructure.Persistence;
|
||||
|
||||
public class ApplicationDbContext : DbContext, IApplicationDbContext
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly AuditableEntitySaveChangesInterceptor _auditableEntitySaveChangesInterceptor;
|
||||
private readonly IMediator? _mediator;
|
||||
private readonly AuditableEntitySaveChangesInterceptor? _auditableEntitySaveChangesInterceptor;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor برای design-time (migrations)
|
||||
/// </summary>
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor اصلی برای runtime
|
||||
/// </summary>
|
||||
public ApplicationDbContext(
|
||||
DbContextOptions<ApplicationDbContext> options,
|
||||
IMediator mediator,
|
||||
@@ -39,7 +50,10 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder.AddInterceptors(_auditableEntitySaveChangesInterceptor);
|
||||
if (_auditableEntitySaveChangesInterceptor != null)
|
||||
{
|
||||
optionsBuilder.AddInterceptors(_auditableEntitySaveChangesInterceptor);
|
||||
}
|
||||
|
||||
// Suppress PendingModelChangesWarning in EF Core 9
|
||||
optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning));
|
||||
@@ -119,4 +133,9 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
|
||||
public DbSet<Country> Countries => Set<Country>();
|
||||
public DbSet<State> States => Set<State>();
|
||||
public DbSet<City> Cities => Set<City>();
|
||||
|
||||
// ============= Inventory Management DbSets =============
|
||||
public DbSet<Warehouse> Warehouses => Set<Warehouse>();
|
||||
public DbSet<InventoryItem> InventoryItems => Set<InventoryItem>();
|
||||
public DbSet<StockMovement> StockMovements => Set<StockMovement>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.IO;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using CMSMicroservice.Infrastructure.Persistence.Interceptors;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Factory برای ایجاد DbContext در زمان طراحی (migrations, scaffolding)
|
||||
/// </summary>
|
||||
public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
|
||||
{
|
||||
public ApplicationDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
// سعی در خواندن connection string از appsettings.json
|
||||
var basePath = Directory.GetCurrentDirectory();
|
||||
var webApiPath = Path.Combine(basePath, "../CMSMicroservice.WebApi");
|
||||
|
||||
if (Directory.Exists(webApiPath))
|
||||
{
|
||||
basePath = webApiPath;
|
||||
}
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(basePath)
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddJsonFile("appsettings.Development.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection");
|
||||
|
||||
// اگر connection string پیدا نشد، از یک مقدار پیشفرض استفاده کن
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
connectionString = "Server=localhost;Database=CMS;Trusted_Connection=True;TrustServerCertificate=True;";
|
||||
}
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
optionsBuilder.UseSqlServer(connectionString,
|
||||
b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName));
|
||||
|
||||
return new ApplicationDbContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// تنظیمات Entity Framework برای موجودی کالا
|
||||
/// </summary>
|
||||
public class InventoryItemConfiguration : IEntityTypeConfiguration<InventoryItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<InventoryItem> builder)
|
||||
{
|
||||
// ========== تنظیمات پایه ==========
|
||||
builder.HasQueryFilter(i => !i.IsDeleted);
|
||||
builder.Ignore(entity => entity.DomainEvents);
|
||||
builder.HasKey(entity => entity.Id);
|
||||
builder.Property(entity => entity.Id).UseIdentityColumn();
|
||||
|
||||
// ========== فیلدهای اصلی ==========
|
||||
builder.Property(e => e.ProductType)
|
||||
.IsRequired()
|
||||
.HasConversion<int>();
|
||||
|
||||
builder.Property(e => e.Quantity)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0);
|
||||
|
||||
builder.Property(e => e.ReservedQuantity)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0);
|
||||
|
||||
// ========== تنظیمات انبار ==========
|
||||
builder.Property(e => e.LowStockThreshold)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(10);
|
||||
|
||||
builder.Property(e => e.ReorderPoint)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(5);
|
||||
|
||||
builder.Property(e => e.MaxStockLevel)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(1000);
|
||||
|
||||
// ========== تاریخها ==========
|
||||
builder.Property(e => e.LastRestockedAt)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(e => e.LastSoldAt)
|
||||
.IsRequired(false);
|
||||
|
||||
// ========== انبار ==========
|
||||
builder.Property(e => e.WarehouseId)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(1);
|
||||
|
||||
// ========== روابط ==========
|
||||
|
||||
// رابطه با Product (اختیاری)
|
||||
builder.HasOne(e => e.Product)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.ProductId)
|
||||
.IsRequired(false)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// رابطه با DiscountProduct (اختیاری)
|
||||
builder.HasOne(e => e.DiscountProduct)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.DiscountProductId)
|
||||
.IsRequired(false)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// رابطه با Warehouse
|
||||
builder.HasOne(e => e.Warehouse)
|
||||
.WithMany(w => w.InventoryItems)
|
||||
.HasForeignKey(e => e.WarehouseId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// ========== Index ها ==========
|
||||
|
||||
// Index روی ProductId برای جستجوی سریع محصولات معمولی
|
||||
builder.HasIndex(e => e.ProductId)
|
||||
.HasDatabaseName("IX_InventoryItems_ProductId");
|
||||
|
||||
// Index روی DiscountProductId برای جستجوی سریع محصولات تخفیفی
|
||||
builder.HasIndex(e => e.DiscountProductId)
|
||||
.HasDatabaseName("IX_InventoryItems_DiscountProductId");
|
||||
|
||||
// Index ترکیبی روی ProductType و Quantity برای گزارشگیری
|
||||
builder.HasIndex(e => new { e.ProductType, e.Quantity })
|
||||
.HasDatabaseName("IX_InventoryItems_ProductType_Quantity");
|
||||
|
||||
// Index ساده روی WarehouseId برای انبار
|
||||
builder.HasIndex(e => e.WarehouseId)
|
||||
.HasDatabaseName("IX_InventoryItems_WarehouseId");
|
||||
|
||||
// ========== محدودیتها ==========
|
||||
|
||||
// محدودیت: یا ProductId یا DiscountProductId باید پر باشد (نه هر دو، نه هیچکدام)
|
||||
builder.HasCheckConstraint("CK_InventoryItem_ProductReference",
|
||||
"(ProductId IS NOT NULL AND DiscountProductId IS NULL) OR (ProductId IS NULL AND DiscountProductId IS NOT NULL)");
|
||||
|
||||
// محدودیت: ProductType باید با نوع محصول مطابقت داشته باشد
|
||||
builder.HasCheckConstraint("CK_InventoryItem_ProductType_Match",
|
||||
"(ProductType = 1 AND ProductId IS NOT NULL) OR (ProductType = 2 AND DiscountProductId IS NOT NULL)");
|
||||
|
||||
// محدودیت: موجودی نمیتواند منفی باشد
|
||||
builder.HasCheckConstraint("CK_InventoryItem_Quantity_NonNegative",
|
||||
"Quantity >= 0");
|
||||
|
||||
// محدودیت: موجودی رزرو شده نمیتواند منفی باشد
|
||||
builder.HasCheckConstraint("CK_InventoryItem_ReservedQuantity_NonNegative",
|
||||
"ReservedQuantity >= 0");
|
||||
|
||||
// محدودیت: موجودی رزرو شده نمیتواند بیشتر از موجودی کل باشد
|
||||
builder.HasCheckConstraint("CK_InventoryItem_ReservedQuantity_LessOrEqualQuantity",
|
||||
"ReservedQuantity <= Quantity");
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// تنظیمات Entity Framework برای حرکات انبار
|
||||
/// </summary>
|
||||
public class StockMovementConfiguration : IEntityTypeConfiguration<StockMovement>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StockMovement> builder)
|
||||
{
|
||||
// ========== تنظیمات پایه ==========
|
||||
builder.HasQueryFilter(s => !s.IsDeleted);
|
||||
builder.Ignore(entity => entity.DomainEvents);
|
||||
builder.HasKey(entity => entity.Id);
|
||||
builder.Property(entity => entity.Id).UseIdentityColumn();
|
||||
|
||||
// ========== فیلدهای اصلی ==========
|
||||
|
||||
// نوع حرکت (enum به int)
|
||||
builder.Property(e => e.MovementType)
|
||||
.IsRequired()
|
||||
.HasConversion<int>();
|
||||
|
||||
// مقدار تغییر (میتواند منفی باشد)
|
||||
builder.Property(e => e.Quantity)
|
||||
.IsRequired();
|
||||
|
||||
// موجودی قبل و بعد
|
||||
builder.Property(e => e.QuantityBefore)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.QuantityAfter)
|
||||
.IsRequired();
|
||||
|
||||
// ========== فیلدهای اختیاری ==========
|
||||
|
||||
// شناسه سفارش معمولی
|
||||
builder.Property(e => e.OrderId)
|
||||
.IsRequired(false);
|
||||
|
||||
// شناسه سفارش تخفیفی
|
||||
builder.Property(e => e.DiscountOrderId)
|
||||
.IsRequired(false);
|
||||
|
||||
// شماره مرجع
|
||||
builder.Property(e => e.ReferenceNumber)
|
||||
.IsRequired(false)
|
||||
.HasMaxLength(100);
|
||||
|
||||
// یادداشت
|
||||
builder.Property(e => e.Note)
|
||||
.IsRequired(false)
|
||||
.HasMaxLength(500);
|
||||
|
||||
// کاربر انجامدهنده
|
||||
builder.Property(e => e.PerformedByUserId)
|
||||
.IsRequired(false);
|
||||
|
||||
// ========== روابط ==========
|
||||
|
||||
// رابطه با InventoryItem (اجباری)
|
||||
builder.HasOne(e => e.InventoryItem)
|
||||
.WithMany(i => i.StockMovements)
|
||||
.HasForeignKey(e => e.InventoryItemId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// ========== Index ها ==========
|
||||
|
||||
// Index روی InventoryItemId برای جستجوی حرکات یک آیتم
|
||||
builder.HasIndex(e => e.InventoryItemId)
|
||||
.HasDatabaseName("IX_StockMovements_InventoryItemId");
|
||||
|
||||
// Index روی MovementType برای فیلتر بر اساس نوع حرکت
|
||||
builder.HasIndex(e => e.MovementType)
|
||||
.HasDatabaseName("IX_StockMovements_MovementType");
|
||||
|
||||
// Index روی Created برای مرتبسازی زمانی
|
||||
builder.HasIndex(e => e.Created)
|
||||
.HasDatabaseName("IX_StockMovements_Created");
|
||||
|
||||
// Index ترکیبی برای گزارشگیری
|
||||
builder.HasIndex(e => new { e.InventoryItemId, e.MovementType, e.Created })
|
||||
.HasDatabaseName("IX_StockMovements_Item_Type_Date");
|
||||
|
||||
// Index روی OrderId برای ردیابی حرکات مرتبط با سفارش
|
||||
builder.HasIndex(e => e.OrderId)
|
||||
.HasDatabaseName("IX_StockMovements_OrderId")
|
||||
.HasFilter("[OrderId] IS NOT NULL");
|
||||
|
||||
// Index روی DiscountOrderId برای ردیابی حرکات مرتبط با سفارش تخفیفی
|
||||
builder.HasIndex(e => e.DiscountOrderId)
|
||||
.HasDatabaseName("IX_StockMovements_DiscountOrderId")
|
||||
.HasFilter("[DiscountOrderId] IS NOT NULL");
|
||||
|
||||
// Index روی ReferenceNumber برای جستجوی سریع با شماره مرجع
|
||||
builder.HasIndex(e => e.ReferenceNumber)
|
||||
.HasDatabaseName("IX_StockMovements_ReferenceNumber")
|
||||
.HasFilter("[ReferenceNumber] IS NOT NULL");
|
||||
|
||||
// ========== محدودیتها ==========
|
||||
|
||||
// محدودیت: QuantityAfter باید برابر QuantityBefore + Quantity باشد
|
||||
builder.HasCheckConstraint("CK_StockMovement_QuantityAfter_Calculation",
|
||||
"QuantityAfter = QuantityBefore + Quantity");
|
||||
|
||||
// محدودیت: QuantityBefore و QuantityAfter نمیتوانند منفی باشند
|
||||
builder.HasCheckConstraint("CK_StockMovement_QuantityBefore_NonNegative",
|
||||
"QuantityBefore >= 0");
|
||||
|
||||
builder.HasCheckConstraint("CK_StockMovement_QuantityAfter_NonNegative",
|
||||
"QuantityAfter >= 0");
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// تنظیمات Entity Framework برای انبار
|
||||
/// </summary>
|
||||
public class WarehouseConfiguration : IEntityTypeConfiguration<Warehouse>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Warehouse> builder)
|
||||
{
|
||||
// ========== تنظیمات پایه ==========
|
||||
builder.HasQueryFilter(w => !w.IsDeleted);
|
||||
builder.Ignore(entity => entity.DomainEvents);
|
||||
builder.HasKey(entity => entity.Id);
|
||||
builder.Property(entity => entity.Id).UseIdentityColumn();
|
||||
|
||||
// ========== فیلدهای اصلی ==========
|
||||
|
||||
// نام انبار (اجباری)
|
||||
builder.Property(e => e.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(200);
|
||||
|
||||
// کد انبار (اجباری و یکتا)
|
||||
builder.Property(e => e.Code)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50);
|
||||
|
||||
// آدرس انبار (اختیاری)
|
||||
builder.Property(e => e.Address)
|
||||
.IsRequired(false)
|
||||
.HasMaxLength(1000);
|
||||
|
||||
// ========== تنظیمات ==========
|
||||
|
||||
// انبار پیشفرض
|
||||
builder.Property(e => e.IsDefault)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(false);
|
||||
|
||||
// وضعیت فعال/غیرفعال
|
||||
builder.Property(e => e.IsActive)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(true);
|
||||
|
||||
// ========== Index ها ==========
|
||||
|
||||
// Index یکتا روی کد انبار
|
||||
builder.HasIndex(e => e.Code)
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Warehouses_Code_Unique");
|
||||
|
||||
// Index روی IsDefault برای پیدا کردن سریع انبار پیشفرض
|
||||
builder.HasIndex(e => e.IsDefault)
|
||||
.HasDatabaseName("IX_Warehouses_IsDefault")
|
||||
.HasFilter("[IsDefault] = 1");
|
||||
|
||||
// Index روی IsActive برای فیلتر انبارهای فعال
|
||||
builder.HasIndex(e => e.IsActive)
|
||||
.HasDatabaseName("IX_Warehouses_IsActive");
|
||||
|
||||
// ========== روابط ==========
|
||||
|
||||
// رابطه یک-به-چند با InventoryItems
|
||||
builder.HasMany(w => w.InventoryItems)
|
||||
.WithOne(i => i.Warehouse)
|
||||
.HasForeignKey(i => i.WarehouseId)
|
||||
.OnDelete(DeleteBehavior.Restrict); // جلوگیری از حذف انبار در صورت وجود موجودی
|
||||
|
||||
// ========== دادههای اولیه ==========
|
||||
|
||||
// انبار پیشفرض
|
||||
builder.HasData(new Warehouse
|
||||
{
|
||||
Id = 1,
|
||||
Name = "انبار اصلی",
|
||||
Code = "WH-001",
|
||||
Address = "تهران - انبار مرکزی فروشگاه",
|
||||
IsDefault = true,
|
||||
IsActive = true,
|
||||
Created = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
CreatedBy = "System",
|
||||
IsDeleted = false
|
||||
});
|
||||
}
|
||||
}
|
||||
+3942
File diff suppressed because it is too large
Load Diff
+242
@@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddInventorySystem : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Warehouses",
|
||||
schema: "CMS",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Code = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
Address = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
IsDefault = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
|
||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Warehouses", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "InventoryItems",
|
||||
schema: "CMS",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ProductId = table.Column<long>(type: "bigint", nullable: true),
|
||||
DiscountProductId = table.Column<long>(type: "bigint", nullable: true),
|
||||
ProductType = table.Column<int>(type: "int", nullable: false),
|
||||
Quantity = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
ReservedQuantity = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
LowStockThreshold = table.Column<int>(type: "int", nullable: false, defaultValue: 10),
|
||||
ReorderPoint = table.Column<int>(type: "int", nullable: false, defaultValue: 5),
|
||||
MaxStockLevel = table.Column<int>(type: "int", nullable: false, defaultValue: 1000),
|
||||
LastRestockedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastSoldAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
WarehouseId = table.Column<long>(type: "bigint", nullable: false, defaultValue: 1L),
|
||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_InventoryItems", x => x.Id);
|
||||
table.CheckConstraint("CK_InventoryItem_ProductReference", "(ProductId IS NOT NULL AND DiscountProductId IS NULL) OR (ProductId IS NULL AND DiscountProductId IS NOT NULL)");
|
||||
table.CheckConstraint("CK_InventoryItem_ProductType_Match", "(ProductType = 1 AND ProductId IS NOT NULL) OR (ProductType = 2 AND DiscountProductId IS NOT NULL)");
|
||||
table.CheckConstraint("CK_InventoryItem_Quantity_NonNegative", "Quantity >= 0");
|
||||
table.CheckConstraint("CK_InventoryItem_ReservedQuantity_LessOrEqualQuantity", "ReservedQuantity <= Quantity");
|
||||
table.CheckConstraint("CK_InventoryItem_ReservedQuantity_NonNegative", "ReservedQuantity >= 0");
|
||||
table.ForeignKey(
|
||||
name: "FK_InventoryItems_DiscountProducts_DiscountProductId",
|
||||
column: x => x.DiscountProductId,
|
||||
principalSchema: "CMS",
|
||||
principalTable: "DiscountProducts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_InventoryItems_Products_ProductId",
|
||||
column: x => x.ProductId,
|
||||
principalSchema: "CMS",
|
||||
principalTable: "Products",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_InventoryItems_Warehouses_WarehouseId",
|
||||
column: x => x.WarehouseId,
|
||||
principalSchema: "CMS",
|
||||
principalTable: "Warehouses",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "StockMovements",
|
||||
schema: "CMS",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
InventoryItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
MovementType = table.Column<int>(type: "int", nullable: false),
|
||||
Quantity = table.Column<int>(type: "int", nullable: false),
|
||||
QuantityBefore = table.Column<int>(type: "int", nullable: false),
|
||||
QuantityAfter = table.Column<int>(type: "int", nullable: false),
|
||||
OrderId = table.Column<long>(type: "bigint", nullable: true),
|
||||
DiscountOrderId = table.Column<long>(type: "bigint", nullable: true),
|
||||
ReferenceNumber = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
Note = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
PerformedByUserId = table.Column<long>(type: "bigint", nullable: true),
|
||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_StockMovements", x => x.Id);
|
||||
table.CheckConstraint("CK_StockMovement_QuantityAfter_Calculation", "QuantityAfter = QuantityBefore + Quantity");
|
||||
table.CheckConstraint("CK_StockMovement_QuantityAfter_NonNegative", "QuantityAfter >= 0");
|
||||
table.CheckConstraint("CK_StockMovement_QuantityBefore_NonNegative", "QuantityBefore >= 0");
|
||||
table.ForeignKey(
|
||||
name: "FK_StockMovements_InventoryItems_InventoryItemId",
|
||||
column: x => x.InventoryItemId,
|
||||
principalSchema: "CMS",
|
||||
principalTable: "InventoryItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
schema: "CMS",
|
||||
table: "Warehouses",
|
||||
columns: new[] { "Id", "Address", "Code", "Created", "CreatedBy", "IsActive", "IsDefault", "IsDeleted", "LastModified", "LastModifiedBy", "Name" },
|
||||
values: new object[] { 1L, "تهران - انبار مرکزی فروشگاه", "WH-001", new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), "System", true, true, false, null, null, "انبار اصلی" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InventoryItems_DiscountProductId",
|
||||
schema: "CMS",
|
||||
table: "InventoryItems",
|
||||
column: "DiscountProductId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InventoryItems_ProductId",
|
||||
schema: "CMS",
|
||||
table: "InventoryItems",
|
||||
column: "ProductId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InventoryItems_ProductType_Quantity",
|
||||
schema: "CMS",
|
||||
table: "InventoryItems",
|
||||
columns: new[] { "ProductType", "Quantity" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InventoryItems_WarehouseId",
|
||||
schema: "CMS",
|
||||
table: "InventoryItems",
|
||||
column: "WarehouseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StockMovements_Created",
|
||||
schema: "CMS",
|
||||
table: "StockMovements",
|
||||
column: "Created");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StockMovements_DiscountOrderId",
|
||||
schema: "CMS",
|
||||
table: "StockMovements",
|
||||
column: "DiscountOrderId",
|
||||
filter: "[DiscountOrderId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StockMovements_InventoryItemId",
|
||||
schema: "CMS",
|
||||
table: "StockMovements",
|
||||
column: "InventoryItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StockMovements_Item_Type_Date",
|
||||
schema: "CMS",
|
||||
table: "StockMovements",
|
||||
columns: new[] { "InventoryItemId", "MovementType", "Created" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StockMovements_MovementType",
|
||||
schema: "CMS",
|
||||
table: "StockMovements",
|
||||
column: "MovementType");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StockMovements_OrderId",
|
||||
schema: "CMS",
|
||||
table: "StockMovements",
|
||||
column: "OrderId",
|
||||
filter: "[OrderId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StockMovements_ReferenceNumber",
|
||||
schema: "CMS",
|
||||
table: "StockMovements",
|
||||
column: "ReferenceNumber",
|
||||
filter: "[ReferenceNumber] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Warehouses_Code_Unique",
|
||||
schema: "CMS",
|
||||
table: "Warehouses",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Warehouses_IsActive",
|
||||
schema: "CMS",
|
||||
table: "Warehouses",
|
||||
column: "IsActive");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Warehouses_IsDefault",
|
||||
schema: "CMS",
|
||||
table: "Warehouses",
|
||||
column: "IsDefault",
|
||||
filter: "[IsDefault] = 1");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "StockMovements",
|
||||
schema: "CMS");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "InventoryItems",
|
||||
schema: "CMS");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Warehouses",
|
||||
schema: "CMS");
|
||||
}
|
||||
}
|
||||
}
|
||||
+310
@@ -1503,6 +1503,102 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("NetworkMembershipHistories", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.InventoryItem", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long?>("DiscountProductId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("LastRestockedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("LastSoldAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("LowStockThreshold")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(10);
|
||||
|
||||
b.Property<int>("MaxStockLevel")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(1000);
|
||||
|
||||
b.Property<long?>("ProductId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("ProductType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<int>("ReorderPoint")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(5);
|
||||
|
||||
b.Property<int>("ReservedQuantity")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<long>("WarehouseId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasDefaultValue(1L);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DiscountProductId")
|
||||
.HasDatabaseName("IX_InventoryItems_DiscountProductId");
|
||||
|
||||
b.HasIndex("ProductId")
|
||||
.HasDatabaseName("IX_InventoryItems_ProductId");
|
||||
|
||||
b.HasIndex("WarehouseId")
|
||||
.HasDatabaseName("IX_InventoryItems_WarehouseId");
|
||||
|
||||
b.HasIndex("ProductType", "Quantity")
|
||||
.HasDatabaseName("IX_InventoryItems_ProductType_Quantity");
|
||||
|
||||
b.ToTable("InventoryItems", "CMS", t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_InventoryItem_ProductReference", "(ProductId IS NOT NULL AND DiscountProductId IS NULL) OR (ProductId IS NULL AND DiscountProductId IS NOT NULL)");
|
||||
|
||||
t.HasCheckConstraint("CK_InventoryItem_ProductType_Match", "(ProductType = 1 AND ProductId IS NOT NULL) OR (ProductType = 2 AND DiscountProductId IS NOT NULL)");
|
||||
|
||||
t.HasCheckConstraint("CK_InventoryItem_Quantity_NonNegative", "Quantity >= 0");
|
||||
|
||||
t.HasCheckConstraint("CK_InventoryItem_ReservedQuantity_LessOrEqualQuantity", "ReservedQuantity <= Quantity");
|
||||
|
||||
t.HasCheckConstraint("CK_InventoryItem_ReservedQuantity_NonNegative", "ReservedQuantity >= 0");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Network.NetworkWeeklyBalance", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -2210,6 +2306,97 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("Roles", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.StockMovement", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long?>("DiscountOrderId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("InventoryItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("MovementType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<long?>("OrderId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long?>("PerformedByUserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("QuantityAfter")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("QuantityBefore")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ReferenceNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Created")
|
||||
.HasDatabaseName("IX_StockMovements_Created");
|
||||
|
||||
b.HasIndex("DiscountOrderId")
|
||||
.HasDatabaseName("IX_StockMovements_DiscountOrderId")
|
||||
.HasFilter("[DiscountOrderId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("InventoryItemId")
|
||||
.HasDatabaseName("IX_StockMovements_InventoryItemId");
|
||||
|
||||
b.HasIndex("MovementType")
|
||||
.HasDatabaseName("IX_StockMovements_MovementType");
|
||||
|
||||
b.HasIndex("OrderId")
|
||||
.HasDatabaseName("IX_StockMovements_OrderId")
|
||||
.HasFilter("[OrderId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("ReferenceNumber")
|
||||
.HasDatabaseName("IX_StockMovements_ReferenceNumber")
|
||||
.HasFilter("[ReferenceNumber] IS NOT NULL");
|
||||
|
||||
b.HasIndex("InventoryItemId", "MovementType", "Created")
|
||||
.HasDatabaseName("IX_StockMovements_Item_Type_Date");
|
||||
|
||||
b.ToTable("StockMovements", "CMS", t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_StockMovement_QuantityAfter_Calculation", "QuantityAfter = QuantityBefore + Quantity");
|
||||
|
||||
t.HasCheckConstraint("CK_StockMovement_QuantityAfter_NonNegative", "QuantityAfter >= 0");
|
||||
|
||||
t.HasCheckConstraint("CK_StockMovement_QuantityBefore_NonNegative", "QuantityBefore >= 0");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Tag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -2818,6 +3005,83 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("UserWalletChangeLogs", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Warehouse", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Address")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsDefault")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Warehouses_Code_Unique");
|
||||
|
||||
b.HasIndex("IsActive")
|
||||
.HasDatabaseName("IX_Warehouses_IsActive");
|
||||
|
||||
b.HasIndex("IsDefault")
|
||||
.HasDatabaseName("IX_Warehouses_IsDefault")
|
||||
.HasFilter("[IsDefault] = 1");
|
||||
|
||||
b.ToTable("Warehouses", "CMS");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1L,
|
||||
Address = "تهران - انبار مرکزی فروشگاه",
|
||||
Code = "WH-001",
|
||||
Created = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
|
||||
CreatedBy = "System",
|
||||
IsActive = true,
|
||||
IsDefault = true,
|
||||
IsDeleted = false,
|
||||
Name = "انبار اصلی"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.WeekDefinition", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -3185,6 +3449,31 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("WeekDefinition");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.InventoryItem", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.DiscountShop.DiscountProduct", "DiscountProduct")
|
||||
.WithMany()
|
||||
.HasForeignKey("DiscountProductId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Product", "Product")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Warehouse", "Warehouse")
|
||||
.WithMany("InventoryItems")
|
||||
.HasForeignKey("WarehouseId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("DiscountProduct");
|
||||
|
||||
b.Navigation("Product");
|
||||
|
||||
b.Navigation("Warehouse");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Network.NetworkWeeklyBalance", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.User", "User")
|
||||
@@ -3290,6 +3579,17 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("Tag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.StockMovement", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.InventoryItem", "InventoryItem")
|
||||
.WithMany("StockMovements")
|
||||
.HasForeignKey("InventoryItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("InventoryItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.User", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.User", "NetworkParent")
|
||||
@@ -3527,6 +3827,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("Cities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.InventoryItem", b =>
|
||||
{
|
||||
b.Navigation("StockMovements");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Package", b =>
|
||||
{
|
||||
b.Navigation("UserOrders");
|
||||
@@ -3611,6 +3916,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("UserWalletChangeLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Warehouse", b =>
|
||||
{
|
||||
b.Navigation("InventoryItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.WeekDefinition", b =>
|
||||
{
|
||||
b.Navigation("CommissionPayoutHistories");
|
||||
|
||||
+454
@@ -0,0 +1,454 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Interfaces.Repositories;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation برای مدیریت موجودی محصولات
|
||||
/// </summary>
|
||||
public class InventoryItemRepository : IInventoryItemRepository
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public InventoryItemRepository(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
#region Read Operations
|
||||
|
||||
public async Task<InventoryItem?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Include(i => i.Warehouse)
|
||||
.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<InventoryItem?> GetByProductIdAsync(long productId, long warehouseId = 1, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.Warehouse)
|
||||
.FirstOrDefaultAsync(i => i.ProductId == productId && i.WarehouseId == warehouseId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<InventoryItem?> GetByDiscountProductIdAsync(long discountProductId, long warehouseId = 1, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InventoryItems
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Include(i => i.Warehouse)
|
||||
.FirstOrDefaultAsync(i => i.DiscountProductId == discountProductId && i.WarehouseId == warehouseId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<InventoryItem>> GetByWarehouseIdAsync(long warehouseId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Where(i => i.WarehouseId == warehouseId)
|
||||
.OrderBy(i => i.Product != null ? i.Product.Title : i.DiscountProduct != null ? i.DiscountProduct.Title : "")
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<InventoryItem>> GetLowStockItemsAsync(ProductType? productType = null, long warehouseId = 1, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Where(i => i.WarehouseId == warehouseId && i.Quantity <= i.LowStockThreshold && i.Quantity > 0);
|
||||
|
||||
if (productType.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.ProductType == productType.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(i => i.Quantity)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<InventoryItem>> GetOutOfStockItemsAsync(ProductType? productType = null, long warehouseId = 1, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Where(i => i.WarehouseId == warehouseId && i.Quantity == 0);
|
||||
|
||||
if (productType.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.ProductType == productType.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(i => i.Product != null ? i.Product.Title : i.DiscountProduct != null ? i.DiscountProduct.Title : "")
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<InventoryItem>> SearchAsync(
|
||||
string? searchTerm = null,
|
||||
ProductType? productType = null,
|
||||
long? warehouseId = null,
|
||||
int? minQuantity = null,
|
||||
int? maxQuantity = null,
|
||||
int skip = 0,
|
||||
int take = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Include(i => i.Warehouse)
|
||||
.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
var term = searchTerm.ToLower();
|
||||
query = query.Where(i =>
|
||||
(i.Product != null && i.Product.Title.ToLower().Contains(term)) ||
|
||||
(i.DiscountProduct != null && i.DiscountProduct.Title.ToLower().Contains(term)));
|
||||
}
|
||||
|
||||
if (productType.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.ProductType == productType.Value);
|
||||
}
|
||||
|
||||
if (warehouseId.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.WarehouseId == warehouseId.Value);
|
||||
}
|
||||
|
||||
if (minQuantity.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.Quantity >= minQuantity.Value);
|
||||
}
|
||||
|
||||
if (maxQuantity.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.Quantity <= maxQuantity.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(i => i.Product != null ? i.Product.Title : i.DiscountProduct != null ? i.DiscountProduct.Title : "")
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> CountAsync(
|
||||
string? searchTerm = null,
|
||||
ProductType? productType = null,
|
||||
long? warehouseId = null,
|
||||
int? minQuantity = null,
|
||||
int? maxQuantity = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.InventoryItems.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
var term = searchTerm.ToLower();
|
||||
query = query.Where(i =>
|
||||
(i.Product != null && i.Product.Title.ToLower().Contains(term)) ||
|
||||
(i.DiscountProduct != null && i.DiscountProduct.Title.ToLower().Contains(term)));
|
||||
}
|
||||
|
||||
if (productType.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.ProductType == productType.Value);
|
||||
}
|
||||
|
||||
if (warehouseId.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.WarehouseId == warehouseId.Value);
|
||||
}
|
||||
|
||||
if (minQuantity.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.Quantity >= minQuantity.Value);
|
||||
}
|
||||
|
||||
if (maxQuantity.HasValue)
|
||||
{
|
||||
query = query.Where(i => i.Quantity <= maxQuantity.Value);
|
||||
}
|
||||
|
||||
return await query.CountAsync(cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Write Operations
|
||||
|
||||
public async Task<InventoryItem> AddAsync(InventoryItem inventoryItem, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.InventoryItems.Add(inventoryItem);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return inventoryItem;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(InventoryItem inventoryItem, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.InventoryItems.Update(inventoryItem);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var item = await _context.InventoryItems.FindAsync(new object[] { id }, cancellationToken);
|
||||
if (item != null)
|
||||
{
|
||||
_context.InventoryItems.Remove(item);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateQuantityAsync(
|
||||
long inventoryItemId,
|
||||
int quantityChange,
|
||||
StockMovementType movementType,
|
||||
string? note = null,
|
||||
string? referenceNumber = null,
|
||||
long? orderId = null,
|
||||
long? discountOrderId = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var item = await _context.InventoryItems.FindAsync(new object[] { inventoryItemId }, cancellationToken);
|
||||
if (item == null) return false;
|
||||
|
||||
// بررسی اینکه موجودی کافی برای کاهش موجود باشد
|
||||
if (quantityChange < 0 && item.Quantity + quantityChange < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// بروزرسانی موجودی
|
||||
item.Quantity += quantityChange;
|
||||
|
||||
// بروزرسانی تاریخ آخرین فعالیت
|
||||
if (movementType == StockMovementType.Sale)
|
||||
{
|
||||
item.LastSoldAt = DateTime.UtcNow;
|
||||
}
|
||||
else if (movementType == StockMovementType.Restock || movementType == StockMovementType.InitialStock)
|
||||
{
|
||||
item.LastRestockedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ثبت حرکت موجودی
|
||||
var stockMovement = new StockMovement
|
||||
{
|
||||
InventoryItemId = inventoryItemId,
|
||||
MovementType = movementType,
|
||||
Quantity = Math.Abs(quantityChange),
|
||||
Note = note,
|
||||
ReferenceNumber = referenceNumber,
|
||||
OrderId = orderId,
|
||||
DiscountOrderId = discountOrderId,
|
||||
PerformedByUserId = performedByUserId
|
||||
};
|
||||
|
||||
_context.StockMovements.Add(stockMovement);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ReserveQuantityAsync(
|
||||
long inventoryItemId,
|
||||
int quantity,
|
||||
string? note = null,
|
||||
string? referenceNumber = null,
|
||||
long? orderId = null,
|
||||
long? discountOrderId = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var item = await _context.InventoryItems.FindAsync(new object[] { inventoryItemId }, cancellationToken);
|
||||
if (item == null) return false;
|
||||
|
||||
// بررسی موجودی قابل دسترس
|
||||
if (item.AvailableQuantity < quantity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// رزرو موجودی
|
||||
item.ReservedQuantity += quantity;
|
||||
|
||||
// ثبت حرکت رزرو
|
||||
var stockMovement = new StockMovement
|
||||
{
|
||||
InventoryItemId = inventoryItemId,
|
||||
MovementType = StockMovementType.Reserved,
|
||||
Quantity = quantity,
|
||||
Note = note ?? "Quantity reserved",
|
||||
ReferenceNumber = referenceNumber,
|
||||
OrderId = orderId,
|
||||
DiscountOrderId = discountOrderId,
|
||||
PerformedByUserId = performedByUserId
|
||||
};
|
||||
|
||||
_context.StockMovements.Add(stockMovement);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ReleaseReservedQuantityAsync(
|
||||
long inventoryItemId,
|
||||
int quantity,
|
||||
string? note = null,
|
||||
string? referenceNumber = null,
|
||||
long? orderId = null,
|
||||
long? discountOrderId = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var item = await _context.InventoryItems.FindAsync(new object[] { inventoryItemId }, cancellationToken);
|
||||
if (item == null) return false;
|
||||
|
||||
// بررسی اینکه مقدار رزرو شده کافی باشد
|
||||
if (item.ReservedQuantity < quantity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// آزاد کردن رزرو
|
||||
item.ReservedQuantity -= quantity;
|
||||
|
||||
// ثبت حرکت آزادسازی
|
||||
var stockMovement = new StockMovement
|
||||
{
|
||||
InventoryItemId = inventoryItemId,
|
||||
MovementType = StockMovementType.Released,
|
||||
Quantity = quantity,
|
||||
Note = note ?? "Reserved quantity released",
|
||||
ReferenceNumber = referenceNumber,
|
||||
OrderId = orderId,
|
||||
DiscountOrderId = discountOrderId,
|
||||
PerformedByUserId = performedByUserId
|
||||
};
|
||||
|
||||
_context.StockMovements.Add(stockMovement);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bulk Operations
|
||||
|
||||
public async Task<bool> BulkUpdateQuantityAsync(
|
||||
List<(long InventoryItemId, int QuantityChange, string? Note)> updates,
|
||||
StockMovementType movementType,
|
||||
string? referenceNumber = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inventoryItemIds = updates.Select(u => u.InventoryItemId).ToList();
|
||||
var items = await _context.InventoryItems
|
||||
.Where(i => inventoryItemIds.Contains(i.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (items.Count != updates.Count)
|
||||
{
|
||||
return false; // برخی آیتمها پیدا نشدند
|
||||
}
|
||||
|
||||
var stockMovements = new List<StockMovement>();
|
||||
|
||||
foreach (var update in updates)
|
||||
{
|
||||
var item = items.First(i => i.Id == update.InventoryItemId);
|
||||
|
||||
// بررسی موجودی کافی
|
||||
if (update.QuantityChange < 0 && item.Quantity + update.QuantityChange < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
item.Quantity += update.QuantityChange;
|
||||
|
||||
if (movementType == StockMovementType.Sale)
|
||||
{
|
||||
item.LastSoldAt = DateTime.UtcNow;
|
||||
}
|
||||
else if (movementType == StockMovementType.Restock || movementType == StockMovementType.InitialStock)
|
||||
{
|
||||
item.LastRestockedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
stockMovements.Add(new StockMovement
|
||||
{
|
||||
InventoryItemId = update.InventoryItemId,
|
||||
MovementType = movementType,
|
||||
Quantity = Math.Abs(update.QuantityChange),
|
||||
Note = update.Note,
|
||||
ReferenceNumber = referenceNumber,
|
||||
PerformedByUserId = performedByUserId
|
||||
});
|
||||
}
|
||||
|
||||
_context.StockMovements.AddRange(stockMovements);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> BulkReserveQuantityAsync(
|
||||
List<(long InventoryItemId, int Quantity, string? Note)> reservations,
|
||||
string? referenceNumber = null,
|
||||
long? orderId = null,
|
||||
long? discountOrderId = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inventoryItemIds = reservations.Select(r => r.InventoryItemId).ToList();
|
||||
var items = await _context.InventoryItems
|
||||
.Where(i => inventoryItemIds.Contains(i.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (items.Count != reservations.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stockMovements = new List<StockMovement>();
|
||||
|
||||
foreach (var reservation in reservations)
|
||||
{
|
||||
var item = items.First(i => i.Id == reservation.InventoryItemId);
|
||||
|
||||
// بررسی موجودی قابل دسترس
|
||||
if (item.AvailableQuantity < reservation.Quantity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
item.ReservedQuantity += reservation.Quantity;
|
||||
|
||||
stockMovements.Add(new StockMovement
|
||||
{
|
||||
InventoryItemId = reservation.InventoryItemId,
|
||||
MovementType = StockMovementType.Reserved,
|
||||
Quantity = reservation.Quantity,
|
||||
Note = reservation.Note ?? "Bulk reservation",
|
||||
ReferenceNumber = referenceNumber,
|
||||
OrderId = orderId,
|
||||
DiscountOrderId = discountOrderId,
|
||||
PerformedByUserId = performedByUserId
|
||||
});
|
||||
}
|
||||
|
||||
_context.StockMovements.AddRange(stockMovements);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Interfaces.Repositories;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation برای مدیریت حرکات موجودی
|
||||
/// </summary>
|
||||
public class StockMovementRepository : IStockMovementRepository
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public StockMovementRepository(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
#region Read Operations
|
||||
|
||||
public async Task<StockMovement?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.StockMovements
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.FirstOrDefaultAsync(m => m.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<StockMovement>> GetByInventoryItemIdAsync(
|
||||
long inventoryItemId,
|
||||
StockMovementType? movementType = null,
|
||||
DateTime? fromDate = null,
|
||||
DateTime? toDate = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.StockMovements
|
||||
.Include(m => m.InventoryItem)
|
||||
.Where(m => m.InventoryItemId == inventoryItemId);
|
||||
|
||||
if (movementType.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.MovementType == movementType.Value);
|
||||
}
|
||||
|
||||
if (fromDate.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.Created >= fromDate.Value);
|
||||
}
|
||||
|
||||
if (toDate.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.Created <= toDate.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderByDescending(m => m.Created)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<StockMovement>> GetByOrderIdAsync(long orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.StockMovements
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.Where(m => m.OrderId == orderId)
|
||||
.OrderByDescending(m => m.Created)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<StockMovement>> GetByDiscountOrderIdAsync(long discountOrderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.StockMovements
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.Where(m => m.DiscountOrderId == discountOrderId)
|
||||
.OrderByDescending(m => m.Created)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<StockMovement>> GetByReferenceNumberAsync(string referenceNumber, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.StockMovements
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.Where(m => m.ReferenceNumber == referenceNumber)
|
||||
.OrderByDescending(m => m.Created)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<StockMovement>> GetByMovementTypeAsync(
|
||||
StockMovementType movementType,
|
||||
DateTime? fromDate = null,
|
||||
DateTime? toDate = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.StockMovements
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.Where(m => m.MovementType == movementType);
|
||||
|
||||
if (fromDate.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.Created >= fromDate.Value);
|
||||
}
|
||||
|
||||
if (toDate.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.Created <= toDate.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderByDescending(m => m.Created)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<StockMovement>> GetRecentMovementsAsync(
|
||||
int count = 50,
|
||||
StockMovementType? movementType = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.StockMovements
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.AsQueryable();
|
||||
|
||||
if (movementType.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.MovementType == movementType.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderByDescending(m => m.Created)
|
||||
.Take(count)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<StockMovement>> SearchAsync(
|
||||
long? inventoryItemId = null,
|
||||
StockMovementType? movementType = null,
|
||||
DateTime? fromDate = null,
|
||||
DateTime? toDate = null,
|
||||
string? referenceNumber = null,
|
||||
long? orderId = null,
|
||||
long? discountOrderId = null,
|
||||
long? performedByUserId = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.StockMovements
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.AsQueryable();
|
||||
|
||||
if (inventoryItemId.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
|
||||
}
|
||||
|
||||
if (movementType.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.MovementType == movementType.Value);
|
||||
}
|
||||
|
||||
if (fromDate.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.Created >= fromDate.Value);
|
||||
}
|
||||
|
||||
if (toDate.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.Created <= toDate.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(referenceNumber))
|
||||
{
|
||||
query = query.Where(m => m.ReferenceNumber != null && m.ReferenceNumber.Contains(referenceNumber));
|
||||
}
|
||||
|
||||
if (orderId.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.OrderId == orderId.Value);
|
||||
}
|
||||
|
||||
if (discountOrderId.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.DiscountOrderId == discountOrderId.Value);
|
||||
}
|
||||
|
||||
if (performedByUserId.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.PerformedByUserId == performedByUserId.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderByDescending(m => m.Created)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> CountAsync(
|
||||
long? inventoryItemId = null,
|
||||
StockMovementType? movementType = null,
|
||||
DateTime? fromDate = null,
|
||||
DateTime? toDate = null,
|
||||
string? referenceNumber = null,
|
||||
long? orderId = null,
|
||||
long? discountOrderId = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.StockMovements.AsQueryable();
|
||||
|
||||
if (inventoryItemId.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
|
||||
}
|
||||
|
||||
if (movementType.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.MovementType == movementType.Value);
|
||||
}
|
||||
|
||||
if (fromDate.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.Created >= fromDate.Value);
|
||||
}
|
||||
|
||||
if (toDate.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.Created <= toDate.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(referenceNumber))
|
||||
{
|
||||
query = query.Where(m => m.ReferenceNumber != null && m.ReferenceNumber.Contains(referenceNumber));
|
||||
}
|
||||
|
||||
if (orderId.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.OrderId == orderId.Value);
|
||||
}
|
||||
|
||||
if (discountOrderId.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.DiscountOrderId == discountOrderId.Value);
|
||||
}
|
||||
|
||||
if (performedByUserId.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.PerformedByUserId == performedByUserId.Value);
|
||||
}
|
||||
|
||||
return await query.CountAsync(cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Write Operations
|
||||
|
||||
public async Task<StockMovement> AddAsync(StockMovement stockMovement, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.StockMovements.Add(stockMovement);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return stockMovement;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var stockMovement = await _context.StockMovements.FindAsync(new object[] { id }, cancellationToken);
|
||||
if (stockMovement != null)
|
||||
{
|
||||
_context.StockMovements.Remove(stockMovement);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<StockMovement>> BulkAddAsync(List<StockMovement> stockMovements, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.StockMovements.AddRange(stockMovements);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return stockMovements;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Analytics & Reports
|
||||
|
||||
public async Task<Dictionary<StockMovementType, int>> GetMovementSummaryAsync(
|
||||
DateTime fromDate,
|
||||
DateTime toDate,
|
||||
long? inventoryItemId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.StockMovements
|
||||
.Where(m => m.Created >= fromDate && m.Created <= toDate);
|
||||
|
||||
if (inventoryItemId.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
|
||||
}
|
||||
|
||||
var movements = await query
|
||||
.GroupBy(m => m.MovementType)
|
||||
.Select(g => new { MovementType = g.Key, TotalQuantity = g.Sum(m => m.Quantity) })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return movements.ToDictionary(x => x.MovementType, x => x.TotalQuantity);
|
||||
}
|
||||
|
||||
public async Task<List<(DateTime Date, int InboundQuantity, int OutboundQuantity)>> GetDailyMovementVolumeAsync(
|
||||
DateTime fromDate,
|
||||
DateTime toDate,
|
||||
long? inventoryItemId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.StockMovements
|
||||
.Where(m => m.Created >= fromDate && m.Created <= toDate);
|
||||
|
||||
if (inventoryItemId.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
|
||||
}
|
||||
|
||||
var movements = await query.ToListAsync(cancellationToken);
|
||||
|
||||
// نوعهای ورودی (افزایش موجودی)
|
||||
var inboundTypes = new[]
|
||||
{
|
||||
StockMovementType.InitialStock,
|
||||
StockMovementType.Restock,
|
||||
StockMovementType.Return,
|
||||
StockMovementType.TransferIn,
|
||||
StockMovementType.AdjustmentPlus,
|
||||
StockMovementType.Released
|
||||
};
|
||||
|
||||
// نوعهای خروجی (کاهش موجودی)
|
||||
var outboundTypes = new[]
|
||||
{
|
||||
StockMovementType.Sale,
|
||||
StockMovementType.Damaged,
|
||||
StockMovementType.Lost,
|
||||
StockMovementType.TransferOut,
|
||||
StockMovementType.AdjustmentMinus,
|
||||
StockMovementType.Reserved
|
||||
};
|
||||
|
||||
var dailyVolumes = movements
|
||||
.GroupBy(m => m.Created.Date)
|
||||
.Select(g => (
|
||||
Date: g.Key,
|
||||
InboundQuantity: g.Where(m => inboundTypes.Contains(m.MovementType)).Sum(m => m.Quantity),
|
||||
OutboundQuantity: g.Where(m => outboundTypes.Contains(m.MovementType)).Sum(m => m.Quantity)
|
||||
))
|
||||
.OrderBy(x => x.Date)
|
||||
.ToList();
|
||||
|
||||
return dailyVolumes;
|
||||
}
|
||||
|
||||
public async Task<List<(long InventoryItemId, string ProductName, int MovementCount, int TotalQuantityChange)>> GetTopMovingProductsAsync(
|
||||
DateTime fromDate,
|
||||
DateTime toDate,
|
||||
int count = 10,
|
||||
StockMovementType? movementType = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.StockMovements
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.Where(m => m.Created >= fromDate && m.Created <= toDate);
|
||||
|
||||
if (movementType.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.MovementType == movementType.Value);
|
||||
}
|
||||
|
||||
var movements = await query.ToListAsync(cancellationToken);
|
||||
|
||||
var topProducts = movements
|
||||
.GroupBy(m => m.InventoryItemId)
|
||||
.Select(g =>
|
||||
{
|
||||
var firstItem = g.First().InventoryItem;
|
||||
var productName = firstItem.Product?.Title ?? firstItem.DiscountProduct?.Title ?? "Unknown";
|
||||
return (
|
||||
InventoryItemId: g.Key,
|
||||
ProductName: productName,
|
||||
MovementCount: g.Count(),
|
||||
TotalQuantityChange: g.Sum(m => m.Quantity)
|
||||
);
|
||||
})
|
||||
.OrderByDescending(x => x.MovementCount)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
|
||||
return topProducts;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Interfaces.Repositories;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation برای مدیریت انبارها
|
||||
/// </summary>
|
||||
public class WarehouseRepository : IWarehouseRepository
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public WarehouseRepository(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
#region Read Operations
|
||||
|
||||
public async Task<Warehouse?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Warehouses
|
||||
.Include(w => w.InventoryItems)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(w => w.InventoryItems)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.FirstOrDefaultAsync(w => w.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Warehouse?> GetByCodeAsync(string code, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Warehouses
|
||||
.Include(w => w.InventoryItems)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(w => w.InventoryItems)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.FirstOrDefaultAsync(w => w.Code == code, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Warehouse?> GetDefaultWarehouseAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Warehouses
|
||||
.Include(w => w.InventoryItems)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(w => w.InventoryItems)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.FirstOrDefaultAsync(w => w.IsDefault, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Warehouse>> GetActiveWarehousesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Warehouses
|
||||
.Where(w => w.IsActive)
|
||||
.OrderBy(w => w.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Warehouse>> GetAllAsync(
|
||||
bool includeInactive = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Warehouses.AsQueryable();
|
||||
|
||||
if (!includeInactive)
|
||||
{
|
||||
query = query.Where(w => w.IsActive);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(w => w.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Warehouse>> SearchAsync(
|
||||
string? searchTerm = null,
|
||||
bool? isActive = null,
|
||||
int skip = 0,
|
||||
int take = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Warehouses.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
var term = searchTerm.ToLower();
|
||||
query = query.Where(w =>
|
||||
w.Name.ToLower().Contains(term) ||
|
||||
w.Code.ToLower().Contains(term) ||
|
||||
(w.Address != null && w.Address.ToLower().Contains(term)));
|
||||
}
|
||||
|
||||
if (isActive.HasValue)
|
||||
{
|
||||
query = query.Where(w => w.IsActive == isActive.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(w => w.Name)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> CountAsync(
|
||||
string? searchTerm = null,
|
||||
bool? isActive = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Warehouses.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
var term = searchTerm.ToLower();
|
||||
query = query.Where(w =>
|
||||
w.Name.ToLower().Contains(term) ||
|
||||
w.Code.ToLower().Contains(term) ||
|
||||
(w.Address != null && w.Address.ToLower().Contains(term)));
|
||||
}
|
||||
|
||||
if (isActive.HasValue)
|
||||
{
|
||||
query = query.Where(w => w.IsActive == isActive.Value);
|
||||
}
|
||||
|
||||
return await query.CountAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsByCodeAsync(string code, long? excludeId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Warehouses.Where(w => w.Code == code);
|
||||
|
||||
if (excludeId.HasValue)
|
||||
{
|
||||
query = query.Where(w => w.Id != excludeId.Value);
|
||||
}
|
||||
|
||||
return await query.AnyAsync(cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Write Operations
|
||||
|
||||
public async Task<Warehouse> AddAsync(Warehouse warehouse, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// اگر این انبار پیشفرض است، سایر انبارها را غیرپیشفرض کن
|
||||
if (warehouse.IsDefault)
|
||||
{
|
||||
await RemoveDefaultFromAllWarehousesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
_context.Warehouses.Add(warehouse);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return warehouse;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Warehouse warehouse, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// اگر این انبار پیشفرض شده، سایر انبارها را غیرپیشفرض کن
|
||||
if (warehouse.IsDefault)
|
||||
{
|
||||
await RemoveDefaultFromAllWarehousesAsync(warehouse.Id, cancellationToken);
|
||||
}
|
||||
|
||||
_context.Warehouses.Update(warehouse);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var warehouse = await _context.Warehouses.FindAsync(new object[] { id }, cancellationToken);
|
||||
if (warehouse != null)
|
||||
{
|
||||
warehouse.IsActive = false;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetActiveStatusAsync(long id, bool isActive, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var warehouse = await _context.Warehouses.FindAsync(new object[] { id }, cancellationToken);
|
||||
if (warehouse != null)
|
||||
{
|
||||
warehouse.IsActive = isActive;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetAsDefaultAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// ابتدا همه انبارها را غیرپیشفرض کن
|
||||
await RemoveDefaultFromAllWarehousesAsync(cancellationToken);
|
||||
|
||||
// سپس انبار مورد نظر را پیشفرض کن
|
||||
var warehouse = await _context.Warehouses.FindAsync(new object[] { id }, cancellationToken);
|
||||
if (warehouse != null)
|
||||
{
|
||||
warehouse.IsDefault = true;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Analytics
|
||||
|
||||
public async Task<(int TotalProducts, int LowStockProducts, int OutOfStockProducts, decimal TotalValue)> GetWarehouseStatisticsAsync(
|
||||
long warehouseId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inventoryItems = await _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Where(i => i.WarehouseId == warehouseId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var totalProducts = inventoryItems.Count;
|
||||
var lowStockProducts = inventoryItems.Count(i => i.Quantity <= i.LowStockThreshold && i.Quantity > 0);
|
||||
var outOfStockProducts = inventoryItems.Count(i => i.Quantity == 0);
|
||||
|
||||
// محاسبه ارزش کل بر اساس قیمت محصولات
|
||||
decimal totalValue = 0;
|
||||
foreach (var item in inventoryItems)
|
||||
{
|
||||
if (item.Product != null)
|
||||
{
|
||||
totalValue += item.Quantity * item.Product.Price;
|
||||
}
|
||||
else if (item.DiscountProduct != null)
|
||||
{
|
||||
totalValue += item.Quantity * item.DiscountProduct.Price;
|
||||
}
|
||||
}
|
||||
|
||||
return (totalProducts, lowStockProducts, outOfStockProducts, totalValue);
|
||||
}
|
||||
|
||||
public async Task<List<(long ProductId, string ProductName, int TotalSold, int CurrentStock)>> GetTopSellingProductsAsync(
|
||||
long warehouseId,
|
||||
DateTime fromDate,
|
||||
DateTime toDate,
|
||||
int count = 10,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// دریافت حرکات فروش برای این انبار در بازه زمانی مشخص
|
||||
var salesMovements = await _context.StockMovements
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.Product)
|
||||
.Include(m => m.InventoryItem)
|
||||
.ThenInclude(i => i.DiscountProduct)
|
||||
.Where(m => m.InventoryItem.WarehouseId == warehouseId &&
|
||||
m.MovementType == StockMovementType.Sale &&
|
||||
m.Created >= fromDate &&
|
||||
m.Created <= toDate)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// گروهبندی بر اساس محصول و محاسبه تعداد فروش
|
||||
var topProducts = salesMovements
|
||||
.GroupBy(m => m.InventoryItemId)
|
||||
.Select(g =>
|
||||
{
|
||||
var firstItem = g.First().InventoryItem;
|
||||
var productId = firstItem.ProductId ?? firstItem.DiscountProductId ?? 0;
|
||||
var productName = firstItem.Product?.Title ?? firstItem.DiscountProduct?.Title ?? "Unknown";
|
||||
var totalSold = g.Sum(m => m.Quantity);
|
||||
var currentStock = firstItem.Quantity;
|
||||
return (ProductId: productId, ProductName: productName, TotalSold: totalSold, CurrentStock: currentStock);
|
||||
})
|
||||
.OrderByDescending(x => x.TotalSold)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
|
||||
return topProducts;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private async Task RemoveDefaultFromAllWarehousesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var defaultWarehouses = await _context.Warehouses
|
||||
.Where(w => w.IsDefault)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var warehouse in defaultWarehouses)
|
||||
{
|
||||
warehouse.IsDefault = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveDefaultFromAllWarehousesAsync(long excludeId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var defaultWarehouses = await _context.Warehouses
|
||||
.Where(w => w.IsDefault && w.Id != excludeId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var warehouse in defaultWarehouses)
|
||||
{
|
||||
warehouse.IsDefault = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,662 @@
|
||||
using System.Collections.Generic;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using CMSMicroservice.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// پیادهسازی سرویس مدیریت موجودی
|
||||
/// این سرویس Source of Truth برای موجودی است و مسئول همگامسازی با Product.RemainingCount
|
||||
/// </summary>
|
||||
public class InventoryService : IInventoryService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly ILogger<InventoryService> _logger;
|
||||
private const long DefaultWarehouseId = 1; // انبار پیشفرض
|
||||
|
||||
public InventoryService(ApplicationDbContext context, ILogger<InventoryService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
#region Initialization
|
||||
|
||||
public async Task<long> InitializeInventoryAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int initialQuantity,
|
||||
long? warehouseId = null,
|
||||
int lowStockThreshold = 10,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var effectiveWarehouseId = warehouseId ?? DefaultWarehouseId;
|
||||
|
||||
// چک کردن اینکه آیا قبلاً InventoryItem برای این محصول وجود دارد
|
||||
var existingItem = productType == ProductType.RegularProduct
|
||||
? await _context.InventoryItems.FirstOrDefaultAsync(
|
||||
x => x.ProductId == productId && x.WarehouseId == effectiveWarehouseId && !x.IsDeleted, ct)
|
||||
: await _context.InventoryItems.FirstOrDefaultAsync(
|
||||
x => x.DiscountProductId == productId && x.WarehouseId == effectiveWarehouseId && !x.IsDeleted, ct);
|
||||
|
||||
if (existingItem != null)
|
||||
{
|
||||
_logger.LogWarning("InventoryItem already exists for {ProductType} with Id {ProductId}",
|
||||
productType, productId);
|
||||
return existingItem.Id;
|
||||
}
|
||||
|
||||
// ایجاد InventoryItem جدید
|
||||
var inventoryItem = new InventoryItem
|
||||
{
|
||||
ProductId = productType == ProductType.RegularProduct ? productId : null,
|
||||
DiscountProductId = productType == ProductType.DiscountProduct ? productId : null,
|
||||
ProductType = productType,
|
||||
Quantity = initialQuantity,
|
||||
ReservedQuantity = 0,
|
||||
LowStockThreshold = lowStockThreshold,
|
||||
WarehouseId = effectiveWarehouseId
|
||||
};
|
||||
|
||||
_context.InventoryItems.Add(inventoryItem);
|
||||
await _context.SaveChangesAsync(ct);
|
||||
|
||||
// ثبت StockMovement اولیه
|
||||
if (initialQuantity > 0)
|
||||
{
|
||||
await LogMovementAsync(
|
||||
inventoryItem.Id,
|
||||
StockMovementType.InitialStock,
|
||||
initialQuantity,
|
||||
0,
|
||||
initialQuantity,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"موجودی اولیه",
|
||||
null,
|
||||
ct);
|
||||
}
|
||||
|
||||
// همگامسازی با Product.RemainingCount
|
||||
await SyncRemainingCountAsync(inventoryItem, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Initialized inventory for {ProductType} Id={ProductId}, Quantity={Quantity}",
|
||||
productType, productId, initialQuantity);
|
||||
|
||||
return inventoryItem.Id;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Query Operations
|
||||
|
||||
public async Task<InventoryItem?> GetInventoryAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
long? warehouseId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var effectiveWarehouseId = warehouseId ?? DefaultWarehouseId;
|
||||
|
||||
return productType == ProductType.RegularProduct
|
||||
? await _context.InventoryItems.FirstOrDefaultAsync(
|
||||
x => x.ProductId == productId && x.WarehouseId == effectiveWarehouseId && !x.IsDeleted, ct)
|
||||
: await _context.InventoryItems.FirstOrDefaultAsync(
|
||||
x => x.DiscountProductId == productId && x.WarehouseId == effectiveWarehouseId && !x.IsDeleted, ct);
|
||||
}
|
||||
|
||||
public async Task<int> GetAvailableQuantityAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
long? warehouseId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var item = await GetInventoryAsync(productId, productType, warehouseId, ct);
|
||||
return item?.AvailableQuantity ?? 0;
|
||||
}
|
||||
|
||||
public async Task<bool> CheckAvailabilityAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int requiredQuantity,
|
||||
long? warehouseId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var available = await GetAvailableQuantityAsync(productId, productType, warehouseId, ct);
|
||||
return available >= requiredQuantity;
|
||||
}
|
||||
|
||||
public async Task<List<InventoryItem>> GetLowStockItemsAsync(
|
||||
ProductType? productType = null,
|
||||
long? warehouseId = null,
|
||||
int count = 50,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var query = _context.InventoryItems
|
||||
.Where(x => !x.IsDeleted)
|
||||
.Where(x => x.Quantity <= x.LowStockThreshold);
|
||||
|
||||
if (productType.HasValue)
|
||||
query = query.Where(x => x.ProductType == productType.Value);
|
||||
|
||||
if (warehouseId.HasValue)
|
||||
query = query.Where(x => x.WarehouseId == warehouseId.Value);
|
||||
|
||||
return await query
|
||||
.OrderBy(x => x.Quantity)
|
||||
.Take(count)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<List<StockMovement>> GetStockMovementsAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
DateTime? fromDate = null,
|
||||
DateTime? toDate = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var inventoryItem = await GetInventoryAsync(productId, productType, null, ct);
|
||||
if (inventoryItem == null)
|
||||
return new List<StockMovement>();
|
||||
|
||||
var query = _context.StockMovements
|
||||
.Where(x => x.InventoryItemId == inventoryItem.Id && !x.IsDeleted);
|
||||
|
||||
if (fromDate.HasValue)
|
||||
query = query.Where(x => x.Created >= fromDate.Value);
|
||||
|
||||
if (toDate.HasValue)
|
||||
query = query.Where(x => x.Created <= toDate.Value);
|
||||
|
||||
return await query
|
||||
.OrderByDescending(x => x.Created)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Order Flow Operations
|
||||
|
||||
public async Task<bool> ReserveStockAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int quantity,
|
||||
long? orderId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var item = await GetInventoryAsync(productId, productType, null, ct);
|
||||
if (item == null)
|
||||
{
|
||||
_logger.LogWarning("Cannot reserve: InventoryItem not found for {ProductType} Id={ProductId}",
|
||||
productType, productId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.AvailableQuantity < quantity)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Cannot reserve: Insufficient stock. Available={Available}, Requested={Requested}",
|
||||
item.AvailableQuantity, quantity);
|
||||
return false;
|
||||
}
|
||||
|
||||
var quantityBefore = item.ReservedQuantity;
|
||||
item.ReservedQuantity += quantity;
|
||||
|
||||
await _context.SaveChangesAsync(ct);
|
||||
|
||||
// ثبت StockMovement
|
||||
await LogMovementAsync(
|
||||
item.Id,
|
||||
StockMovementType.Reserved,
|
||||
quantity,
|
||||
quantityBefore,
|
||||
item.ReservedQuantity,
|
||||
orderId,
|
||||
productType == ProductType.DiscountProduct ? orderId : null,
|
||||
null,
|
||||
"رزرو برای سفارش",
|
||||
null,
|
||||
ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Reserved {Quantity} units for {ProductType} Id={ProductId}, OrderId={OrderId}",
|
||||
quantity, productType, productId, orderId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ReleaseReservationAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int quantity,
|
||||
long? orderId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var item = await GetInventoryAsync(productId, productType, null, ct);
|
||||
if (item == null)
|
||||
{
|
||||
_logger.LogWarning("Cannot release: InventoryItem not found for {ProductType} Id={ProductId}",
|
||||
productType, productId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var quantityBefore = item.ReservedQuantity;
|
||||
item.ReservedQuantity = Math.Max(0, item.ReservedQuantity - quantity);
|
||||
|
||||
await _context.SaveChangesAsync(ct);
|
||||
|
||||
// ثبت StockMovement
|
||||
await LogMovementAsync(
|
||||
item.Id,
|
||||
StockMovementType.Released,
|
||||
quantity,
|
||||
quantityBefore,
|
||||
item.ReservedQuantity,
|
||||
orderId,
|
||||
productType == ProductType.DiscountProduct ? orderId : null,
|
||||
null,
|
||||
"آزادسازی رزرو",
|
||||
null,
|
||||
ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Released {Quantity} reserved units for {ProductType} Id={ProductId}, OrderId={OrderId}",
|
||||
quantity, productType, productId, orderId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ConfirmSaleAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int quantity,
|
||||
long? orderId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var item = await GetInventoryAsync(productId, productType, null, ct);
|
||||
if (item == null)
|
||||
{
|
||||
_logger.LogWarning("Cannot confirm sale: InventoryItem not found for {ProductType} Id={ProductId}",
|
||||
productType, productId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var quantityBefore = item.Quantity;
|
||||
|
||||
// کاهش موجودی واقعی
|
||||
item.Quantity -= quantity;
|
||||
|
||||
// کاهش رزرو (اگر رزرو شده بود)
|
||||
item.ReservedQuantity = Math.Max(0, item.ReservedQuantity - quantity);
|
||||
|
||||
// آپدیت آخرین فروش
|
||||
item.LastSoldAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync(ct);
|
||||
|
||||
// ثبت StockMovement
|
||||
await LogMovementAsync(
|
||||
item.Id,
|
||||
StockMovementType.Sale,
|
||||
-quantity, // منفی برای خروج
|
||||
quantityBefore,
|
||||
item.Quantity,
|
||||
orderId,
|
||||
productType == ProductType.DiscountProduct ? orderId : null,
|
||||
null,
|
||||
"فروش",
|
||||
null,
|
||||
ct);
|
||||
|
||||
// همگامسازی با Product.RemainingCount
|
||||
await SyncRemainingCountAsync(item, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Confirmed sale of {Quantity} units for {ProductType} Id={ProductId}, OrderId={OrderId}. New Quantity={NewQuantity}",
|
||||
quantity, productType, productId, orderId, item.Quantity);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stock Management Operations
|
||||
|
||||
public async Task<bool> AddStockAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int quantity,
|
||||
string? referenceNumber = null,
|
||||
string? note = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var item = await GetInventoryAsync(productId, productType, null, ct);
|
||||
if (item == null)
|
||||
{
|
||||
_logger.LogWarning("Cannot add stock: InventoryItem not found for {ProductType} Id={ProductId}",
|
||||
productType, productId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var quantityBefore = item.Quantity;
|
||||
item.Quantity += quantity;
|
||||
item.LastRestockedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync(ct);
|
||||
|
||||
// ثبت StockMovement
|
||||
await LogMovementAsync(
|
||||
item.Id,
|
||||
StockMovementType.Restock,
|
||||
quantity,
|
||||
quantityBefore,
|
||||
item.Quantity,
|
||||
null,
|
||||
null,
|
||||
referenceNumber,
|
||||
note ?? "ورود کالا",
|
||||
performedByUserId,
|
||||
ct);
|
||||
|
||||
// همگامسازی با Product.RemainingCount
|
||||
await SyncRemainingCountAsync(item, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Added {Quantity} units to {ProductType} Id={ProductId}. New Quantity={NewQuantity}",
|
||||
quantity, productType, productId, item.Quantity);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> AdjustStockAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int newQuantity,
|
||||
string? note = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var item = await GetInventoryAsync(productId, productType, null, ct);
|
||||
if (item == null)
|
||||
{
|
||||
_logger.LogWarning("Cannot adjust stock: InventoryItem not found for {ProductType} Id={ProductId}",
|
||||
productType, productId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var quantityBefore = item.Quantity;
|
||||
var difference = newQuantity - quantityBefore;
|
||||
|
||||
item.Quantity = newQuantity;
|
||||
|
||||
await _context.SaveChangesAsync(ct);
|
||||
|
||||
// ثبت StockMovement
|
||||
var movementType = difference >= 0
|
||||
? StockMovementType.AdjustmentPlus
|
||||
: StockMovementType.AdjustmentMinus;
|
||||
|
||||
await LogMovementAsync(
|
||||
item.Id,
|
||||
movementType,
|
||||
difference,
|
||||
quantityBefore,
|
||||
newQuantity,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
note ?? "تعدیل موجودی",
|
||||
performedByUserId,
|
||||
ct);
|
||||
|
||||
// همگامسازی با Product.RemainingCount
|
||||
await SyncRemainingCountAsync(item, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Adjusted stock for {ProductType} Id={ProductId}. Before={Before}, After={After}",
|
||||
productType, productId, quantityBefore, newQuantity);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ProcessReturnAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int quantity,
|
||||
long? orderId = null,
|
||||
string? note = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var item = await GetInventoryAsync(productId, productType, null, ct);
|
||||
if (item == null)
|
||||
{
|
||||
_logger.LogWarning("Cannot process return: InventoryItem not found for {ProductType} Id={ProductId}",
|
||||
productType, productId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var quantityBefore = item.Quantity;
|
||||
item.Quantity += quantity;
|
||||
|
||||
await _context.SaveChangesAsync(ct);
|
||||
|
||||
// ثبت StockMovement
|
||||
await LogMovementAsync(
|
||||
item.Id,
|
||||
StockMovementType.Return,
|
||||
quantity,
|
||||
quantityBefore,
|
||||
item.Quantity,
|
||||
orderId,
|
||||
productType == ProductType.DiscountProduct ? orderId : null,
|
||||
null,
|
||||
note ?? "برگشت از مشتری",
|
||||
performedByUserId,
|
||||
ct);
|
||||
|
||||
// همگامسازی با Product.RemainingCount
|
||||
await SyncRemainingCountAsync(item, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Processed return of {Quantity} units for {ProductType} Id={ProductId}. New Quantity={NewQuantity}",
|
||||
quantity, productType, productId, item.Quantity);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> RecordLossAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int quantity,
|
||||
StockMovementType lossType,
|
||||
string? note = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (lossType != StockMovementType.Damaged && lossType != StockMovementType.Lost)
|
||||
{
|
||||
_logger.LogWarning("Invalid loss type: {LossType}. Must be Damaged or Lost.", lossType);
|
||||
return false;
|
||||
}
|
||||
|
||||
var item = await GetInventoryAsync(productId, productType, null, ct);
|
||||
if (item == null)
|
||||
{
|
||||
_logger.LogWarning("Cannot record loss: InventoryItem not found for {ProductType} Id={ProductId}",
|
||||
productType, productId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var quantityBefore = item.Quantity;
|
||||
item.Quantity = Math.Max(0, item.Quantity - quantity);
|
||||
|
||||
await _context.SaveChangesAsync(ct);
|
||||
|
||||
// ثبت StockMovement
|
||||
await LogMovementAsync(
|
||||
item.Id,
|
||||
lossType,
|
||||
-quantity,
|
||||
quantityBefore,
|
||||
item.Quantity,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
note ?? (lossType == StockMovementType.Damaged ? "ضایعات" : "مفقودی"),
|
||||
performedByUserId,
|
||||
ct);
|
||||
|
||||
// همگامسازی با Product.RemainingCount
|
||||
await SyncRemainingCountAsync(item, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Recorded {LossType} of {Quantity} units for {ProductType} Id={ProductId}. New Quantity={NewQuantity}",
|
||||
lossType, quantity, productType, productId, item.Quantity);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bulk Operations
|
||||
|
||||
public async Task<bool> BulkReserveStockAsync(
|
||||
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
|
||||
long? orderId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
foreach (var (productId, productType, quantity) in items)
|
||||
{
|
||||
var result = await ReserveStockAsync(productId, productType, quantity, orderId, ct);
|
||||
if (!result)
|
||||
{
|
||||
// در صورت خطا، رزروهای قبلی را آزاد کنید
|
||||
_logger.LogError(
|
||||
"Bulk reserve failed at {ProductType} Id={ProductId}. Rolling back...",
|
||||
productType, productId);
|
||||
// TODO: Implement rollback logic
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> BulkReleaseReservationAsync(
|
||||
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
|
||||
long? orderId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
foreach (var (productId, productType, quantity) in items)
|
||||
{
|
||||
await ReleaseReservationAsync(productId, productType, quantity, orderId, ct);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> BulkConfirmSaleAsync(
|
||||
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
|
||||
long? orderId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
foreach (var (productId, productType, quantity) in items)
|
||||
{
|
||||
var result = await ConfirmSaleAsync(productId, productType, quantity, orderId, ct);
|
||||
if (!result)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Bulk confirm sale failed at {ProductType} Id={ProductId}",
|
||||
productType, productId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// همگامسازی موجودی InventoryItem با Product.RemainingCount
|
||||
/// این متد اطمینان میدهد که دادههای قدیمی (RemainingCount) همیشه با سیستم جدید sync است
|
||||
/// </summary>
|
||||
private async Task SyncRemainingCountAsync(InventoryItem item, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (item.ProductType == ProductType.RegularProduct && item.ProductId.HasValue)
|
||||
{
|
||||
var product = await _context.Products.FindAsync(new object[] { item.ProductId.Value }, ct);
|
||||
if (product != null)
|
||||
{
|
||||
product.RemainingCount = item.Quantity;
|
||||
await _context.SaveChangesAsync(ct);
|
||||
_logger.LogDebug("Synced RemainingCount for Product Id={ProductId} to {Quantity}",
|
||||
item.ProductId, item.Quantity);
|
||||
}
|
||||
}
|
||||
else if (item.ProductType == ProductType.DiscountProduct && item.DiscountProductId.HasValue)
|
||||
{
|
||||
var discountProduct = await _context.DiscountProducts.FindAsync(
|
||||
new object[] { item.DiscountProductId.Value }, ct);
|
||||
if (discountProduct != null)
|
||||
{
|
||||
discountProduct.RemainingCount = item.Quantity;
|
||||
await _context.SaveChangesAsync(ct);
|
||||
_logger.LogDebug("Synced RemainingCount for DiscountProduct Id={ProductId} to {Quantity}",
|
||||
item.DiscountProductId, item.Quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to sync RemainingCount for InventoryItem Id={ItemId}", item.Id);
|
||||
// Don't throw - sync failure shouldn't break the main operation
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ثبت حرکت موجودی در StockMovements
|
||||
/// </summary>
|
||||
private async Task LogMovementAsync(
|
||||
long inventoryItemId,
|
||||
StockMovementType movementType,
|
||||
int quantity,
|
||||
int quantityBefore,
|
||||
int quantityAfter,
|
||||
long? orderId,
|
||||
long? discountOrderId,
|
||||
string? referenceNumber,
|
||||
string? note,
|
||||
long? performedByUserId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var movement = new StockMovement
|
||||
{
|
||||
InventoryItemId = inventoryItemId,
|
||||
MovementType = movementType,
|
||||
Quantity = quantity,
|
||||
QuantityBefore = quantityBefore,
|
||||
QuantityAfter = quantityAfter,
|
||||
OrderId = orderId,
|
||||
DiscountOrderId = discountOrderId,
|
||||
ReferenceNumber = referenceNumber,
|
||||
Note = note,
|
||||
PerformedByUserId = performedByUserId
|
||||
};
|
||||
|
||||
_context.StockMovements.Add(movement);
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user