using FruitBank.Common.Dtos; using FruitBank.Common.Entities; using FruitBank.Common.Enums; using FruitBank.Common.Interfaces; using Nop.Core.Domain.Catalog; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Services.Catalog; using Nop.Services.Orders; namespace Nop.Plugin.Misc.FruitBankPlugin.Services; /// /// Extension methods for product replacement logic on PreorderConversionService. /// /// SETUP REQUIRED: Add this as a partial class by: /// 1. Add `partial` keyword to PreorderConversionService class declaration /// 2. This file uses the same injected fields — no extra DI needed /// public partial class PreorderConversionService { // ── Product replacement ─────────────────────────────────────────────────── /// /// Called from FruitBankDataController.UpdateShippingItem when ProductId changes. /// oldItem must be loaded BEFORE the DB update to capture the previous state. /// public async Task ReplaceShippingItemProductAsync( int shippingItemId, int newProductId, ShippingItem oldItem) { if (oldItem.ProductId == null || oldItem.ProductId.Value == newProductId) return; var oldProductId = oldItem.ProductId.Value; var qty = oldItem.QuantityOnDocument; var isMeasured = oldItem.IsMeasured; Console.WriteLine($"[ReplaceShippingItemProduct] #{shippingItemId}: {oldProductId}→{newProductId}, qty={qty}, measured={isMeasured}"); // ── Guard: reject if any linked order is being measured ─────────────── var affectedOrders = await GetAffectedOpenOrdersAsync(oldProductId); var startedOrders = affectedOrders .Where(o => o.MeasuringStatus > MeasuringStatus.NotStarted) .ToList(); if (startedOrders.Any()) throw new InvalidOperationException( $"Mérés folyamatban a következő rendeléseken: " + $"#{string.Join(", #", startedOrders.Select(o => o.Id))}. " + $"Termékcsere nem lehetséges."); // ── Stock / IncomingQuantity swap ───────────────────────────────────── if (!isMeasured) { // Item not yet on the truck — only IncomingQuantity moves await SyncIncomingQuantityAsync(oldProductId, qty, 0); await SyncIncomingQuantityAsync(newProductId, 0, qty); } else { // Item already measured/received into stock — adjust actual stock await _dbContext.UpdateStockQuantityAndWeightAsync( oldProductId, -oldItem.MeasuredQuantity, $"Termék csere: shippingItem #{shippingItemId} — {oldProductId}→{newProductId}", oldItem.IsMeasurable ? -oldItem.MeasuredNetWeight : 0d); await _dbContext.UpdateStockQuantityAndWeightAsync( newProductId, +oldItem.MeasuredQuantity, $"Termék csere: shippingItem #{shippingItemId} — {oldProductId}→{newProductId}", oldItem.IsMeasurable ? +oldItem.MeasuredNetWeight : 0d); } // ── Swap order items ────────────────────────────────────────────────── var openOrders = affectedOrders .Where(o => o.MeasuringStatus == MeasuringStatus.NotStarted) .OrderBy(o => o.Id) .ToList(); var replacementBudget = qty; var affectedOrderIds = new List(); foreach (var orderDto in openOrders) { if (replacementBudget <= 0) break; var order = await _dbContext.Orders.GetByIdAsync(orderDto.Id); if (order == null) continue; var allItems = await _orderService.GetOrderItemsAsync(order.Id); var oldOrderItem = allItems.FirstOrDefault(oi => oi.ProductId == oldProductId); if (oldOrderItem == null) continue; var swapQty = Math.Min(oldOrderItem.Quantity, replacementBudget); // Remove old item — restores stock, deletes pallets/GAs, deletes item await _orderItemService.RemoveOrderItemFromOrderAsync( order, oldOrderItem, $"Termék csere: #{oldProductId}→#{newProductId}, rendelés #{order.Id}"); // Add new item — direct insert without availability check var newProduct = await _productService.GetProductByIdAsync(newProductId); if (newProduct != null) { var newProductDto = await _dbContext.ProductDtos.GetByIdAsync(newProductId, true); var newOrderItem = await _orderItemService.CreateOrderItemAsync( newProduct, order, productId : newProductId, quantity : swapQty, price : oldOrderItem.UnitPriceInclTax, isMeasurable : newProductDto?.IsMeasurable ?? false, unitPricesIncludeDiscounts : false); await _orderItemService.AddOrderItemToOrderAsync( order, newOrderItem, $"Termék csere felvitel: #{newProductId}, rendelés #{order.Id}"); } await _orderService.UpdateOrderAsync(order); await _orderItemService.InsertOrderNoteAsync(order.Id, false, $"Termék cserélve: #{oldProductId}→#{newProductId} ({swapQty} db), " + $"szállítói dok. #{oldItem.ShippingDocumentId}"); replacementBudget -= swapQty; affectedOrderIds.Add(order.Id); } // ── Swap preorder items ─────────────────────────────────────────────── if (affectedOrderIds.Any()) { var preorders = (await _preorderDbContext.Preorders.GetAll(false).ToListAsync()) .Where(p => p.OrderId.HasValue && affectedOrderIds.Contains(p.OrderId.Value)) .ToList(); foreach (var preorder in preorders) { var piList = await _preorderDbContext.PreorderItems .GetAllByPreorderIdAsync(preorder.Id) .ToListAsync(); foreach (var pi in piList.Where(i => i.ProductId == oldProductId)) { pi.ProductId = newProductId; await _preorderDbContext.PreorderItems.UpdateAsync(pi); } } } // ── Trigger conversion for new product ──────────────────────────────── // New product may now have pending preorders that can be fulfilled await ConvertPreordersForProductsAsync( new List { newProductId }, oldItem.ShippingDocumentId); // TODO: SignalR notification to admin hub // TODO: SendPreorderProductReplacedNotificationAsync per affected customer Console.WriteLine($"[ReplaceShippingItemProduct] Complete: " + $"{affectedOrderIds.Count} orders swapped, budget remaining={replacementBudget}"); } private async Task> GetAffectedOpenOrdersAsync(int oldProductId) { // Orders that are referenced by a preorder AND contain the old product var preorderOrderIds = (await _preorderDbContext.Preorders.GetAll(false).ToListAsync()) .Where(p => p.OrderId.HasValue) .Select(p => p.OrderId!.Value) .ToHashSet(); if (!preorderOrderIds.Any()) return new(); var matchingOrderIds = await _dbContext.OrderItems.Table .Where(oi => oi.ProductId == oldProductId && preorderOrderIds.Contains(oi.OrderId)) .Select(oi => oi.OrderId) .Distinct() .ToListAsync(); if (!matchingOrderIds.Any()) return new(); return await _dbContext.OrderDtos .GetAll(false) .Where(o => matchingOrderIds.Contains(o.Id) && !o.Deleted) .ToListAsync(); } }