diff --git a/src/CMSMicroservice.Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommandHandler.cs b/src/CMSMicroservice.Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommandHandler.cs index 8bd4783..915bd4e 100644 --- a/src/CMSMicroservice.Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommandHandler.cs +++ b/src/CMSMicroservice.Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommandHandler.cs @@ -1,6 +1,7 @@ using CMSMicroservice.Application.Common.FileManager; using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Enums; using Microsoft.Extensions.Logging; namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts; @@ -9,15 +10,18 @@ public class CreateNewProductsCommandHandler : IRequestHandler _logger; public CreateNewProductsCommandHandler( IApplicationDbContext context, IFileManager fileManager, + IInventoryService inventoryService, ILogger logger) { _context = context; _fileManager = fileManager; + _inventoryService = inventoryService; _logger = logger; } @@ -70,6 +74,20 @@ public class CreateNewProductsCommandHandler : IRequestHandler 0 }) { diff --git a/src/CMSMicroservice.Infrastructure/BackgroundServices/InventoryInitializerService.cs b/src/CMSMicroservice.Infrastructure/BackgroundServices/InventoryInitializerService.cs new file mode 100644 index 0000000..85fa3e0 --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/BackgroundServices/InventoryInitializerService.cs @@ -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; + +/// +/// سرویس یکباره (Migration) برای مقداردهی اولیه انبارداری از محصولات قدیمی. +/// محصولاتی که قبل از فعال‌سازی سیستم انبارداری ایجاد شده‌اند رکورد InventoryItem ندارند. +/// این Worker در استارتاپ اجرا شده، برای آنها رکورد می‌سازد و سپس متوقف می‌شود. +/// توجه: محصولات جدید از طریق CreateProductCommandHandler خودکار رکورد انبار دریافت می‌کنند. +/// این سرویس را می‌توانید بعد از اجرای اولیه از DI حذف کنید. +/// +public class InventoryInitializerService : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public InventoryInitializerService( + IServiceScopeFactory scopeFactory, + ILogger 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(); + var inventoryService = scope.ServiceProvider.GetRequiredService(); + + 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); + } +} diff --git a/src/CMSMicroservice.Infrastructure/ConfigureServices.cs b/src/CMSMicroservice.Infrastructure/ConfigureServices.cs index 0503b4c..bb47e93 100644 --- a/src/CMSMicroservice.Infrastructure/ConfigureServices.cs +++ b/src/CMSMicroservice.Infrastructure/ConfigureServices.cs @@ -123,6 +123,9 @@ public static class ConfigureServices // Expire pending discount orders after 30 minutes services.AddHostedService(); + // One-time: Initialize inventory records for existing products + services.AddHostedService(); + if (configuration.GetValue("UseInMemoryDatabase")) { services.AddDbContext(options =>