feat: auto-create inventory records for new products + migration worker
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m52s
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m52s
- CreateNewProductsCommandHandler: inject IInventoryService, auto-init inventory record with qty=0 when a new regular product is created (DiscountProduct handler already had this) - InventoryInitializerService: one-time BackgroundService that runs on startup, finds existing products without InventoryItem records, and creates them (migration for legacy products) - Register InventoryInitializerService in DI
This commit is contained in:
+18
@@ -1,6 +1,7 @@
|
|||||||
using CMSMicroservice.Application.Common.FileManager;
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
||||||
@@ -9,15 +10,18 @@ public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProducts
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IFileManager _fileManager;
|
private readonly IFileManager _fileManager;
|
||||||
|
private readonly IInventoryService _inventoryService;
|
||||||
private readonly ILogger<CreateNewProductsCommandHandler> _logger;
|
private readonly ILogger<CreateNewProductsCommandHandler> _logger;
|
||||||
|
|
||||||
public CreateNewProductsCommandHandler(
|
public CreateNewProductsCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IFileManager fileManager,
|
IFileManager fileManager,
|
||||||
|
IInventoryService inventoryService,
|
||||||
ILogger<CreateNewProductsCommandHandler> logger)
|
ILogger<CreateNewProductsCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_fileManager = fileManager;
|
_fileManager = fileManager;
|
||||||
|
_inventoryService = inventoryService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +74,20 @@ public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProducts
|
|||||||
await _context.Products.AddAsync(entity, cancellationToken);
|
await _context.Products.AddAsync(entity, cancellationToken);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// ایجاد رکورد موجودی در سیستم انبارداری
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _inventoryService.InitializeInventoryAsync(
|
||||||
|
entity.Id,
|
||||||
|
ProductType.RegularProduct,
|
||||||
|
0, // موجودی اولیه صفر — باید از طریق Inventory اضافه شود
|
||||||
|
ct: cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to initialize inventory for Product Id={ProductId}", entity.Id);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle category assignments
|
// Handle category assignments
|
||||||
if (request.CategoryIds is { Count: > 0 })
|
if (request.CategoryIds is { Count: > 0 })
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.BackgroundServices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس یکباره (Migration) برای مقداردهی اولیه انبارداری از محصولات قدیمی.
|
||||||
|
/// محصولاتی که قبل از فعالسازی سیستم انبارداری ایجاد شدهاند رکورد InventoryItem ندارند.
|
||||||
|
/// این Worker در استارتاپ اجرا شده، برای آنها رکورد میسازد و سپس متوقف میشود.
|
||||||
|
/// توجه: محصولات جدید از طریق CreateProductCommandHandler خودکار رکورد انبار دریافت میکنند.
|
||||||
|
/// این سرویس را میتوانید بعد از اجرای اولیه از DI حذف کنید.
|
||||||
|
/// </summary>
|
||||||
|
public class InventoryInitializerService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ILogger<InventoryInitializerService> _logger;
|
||||||
|
|
||||||
|
public InventoryInitializerService(
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
ILogger<InventoryInitializerService> logger)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
// صبر کوتاه برای اطمینان از آماده شدن دیتابیس
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("InventoryInitializerService started — scanning products for missing inventory records...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>();
|
||||||
|
var inventoryService = scope.ServiceProvider.GetRequiredService<IInventoryService>();
|
||||||
|
|
||||||
|
var (regularCount, discountCount) = await InitializeAllProducts(context, inventoryService, stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"InventoryInitializerService completed — initialized {RegularCount} regular products and {DiscountCount} discount products",
|
||||||
|
regularCount, discountCount);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "InventoryInitializerService encountered an error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// این Worker فقط یکبار اجرا میشود
|
||||||
|
_logger.LogInformation("InventoryInitializerService finished — shutting down (one-time execution)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(int regularCount, int discountCount)> InitializeAllProducts(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IInventoryService inventoryService,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var regularCount = 0;
|
||||||
|
var discountCount = 0;
|
||||||
|
|
||||||
|
// ── محصولات عادی ──
|
||||||
|
// محصولاتی که هنوز InventoryItem برایشان ایجاد نشده
|
||||||
|
var existingRegularProductIds = await context.InventoryItems
|
||||||
|
.Where(i => !i.IsDeleted && i.ProductId != null)
|
||||||
|
.Select(i => i.ProductId!.Value)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var regularProducts = await context.Products
|
||||||
|
.Where(p => !p.IsDeleted && !existingRegularProductIds.Contains(p.Id))
|
||||||
|
.Select(p => new { p.Id, p.Title, p.RemainingCount })
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
_logger.LogInformation("Found {Count} regular products without inventory records", regularProducts.Count);
|
||||||
|
|
||||||
|
foreach (var product in regularProducts)
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var initialQty = Math.Max(0, product.RemainingCount);
|
||||||
|
|
||||||
|
await inventoryService.InitializeInventoryAsync(
|
||||||
|
product.Id,
|
||||||
|
ProductType.RegularProduct,
|
||||||
|
initialQty,
|
||||||
|
warehouseId: null, // انبار پیشفرض (ID=1)
|
||||||
|
lowStockThreshold: 10,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
regularCount++;
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Initialized inventory for Regular Product Id={ProductId} ({Title}), Qty={Qty}",
|
||||||
|
product.Id, product.Title, initialQty);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex,
|
||||||
|
"Failed to initialize inventory for Regular Product Id={ProductId} ({Title})",
|
||||||
|
product.Id, product.Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── محصولات تخفیفی ──
|
||||||
|
var existingDiscountProductIds = await context.InventoryItems
|
||||||
|
.Where(i => !i.IsDeleted && i.DiscountProductId != null)
|
||||||
|
.Select(i => i.DiscountProductId!.Value)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var discountProducts = await context.DiscountProducts
|
||||||
|
.Where(p => !p.IsDeleted && !existingDiscountProductIds.Contains(p.Id))
|
||||||
|
.Select(p => new { p.Id, p.Title, p.RemainingCount })
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
_logger.LogInformation("Found {Count} discount products without inventory records", discountProducts.Count);
|
||||||
|
|
||||||
|
foreach (var product in discountProducts)
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var initialQty = Math.Max(0, product.RemainingCount);
|
||||||
|
|
||||||
|
await inventoryService.InitializeInventoryAsync(
|
||||||
|
product.Id,
|
||||||
|
ProductType.DiscountProduct,
|
||||||
|
initialQty,
|
||||||
|
warehouseId: null,
|
||||||
|
lowStockThreshold: 10,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
discountCount++;
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Initialized inventory for Discount Product Id={ProductId} ({Title}), Qty={Qty}",
|
||||||
|
product.Id, product.Title, initialQty);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex,
|
||||||
|
"Failed to initialize inventory for Discount Product Id={ProductId} ({Title})",
|
||||||
|
product.Id, product.Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (regularCount, discountCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -123,6 +123,9 @@ public static class ConfigureServices
|
|||||||
// Expire pending discount orders after 30 minutes
|
// Expire pending discount orders after 30 minutes
|
||||||
services.AddHostedService<ExpirePendingOrdersService>();
|
services.AddHostedService<ExpirePendingOrdersService>();
|
||||||
|
|
||||||
|
// One-time: Initialize inventory records for existing products
|
||||||
|
services.AddHostedService<InventoryInitializerService>();
|
||||||
|
|
||||||
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
|
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
|
||||||
{
|
{
|
||||||
services.AddDbContext<ApplicationDbContext>(options =>
|
services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
|
|||||||
Reference in New Issue
Block a user