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:
@@ -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
|
||||
services.AddHostedService<ExpirePendingOrdersService>();
|
||||
|
||||
// One-time: Initialize inventory records for existing products
|
||||
services.AddHostedService<InventoryInitializerService>();
|
||||
|
||||
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
|
||||
{
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
|
||||
Reference in New Issue
Block a user