Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/Services/PreorderConversionService.R...

185 lines
8.3 KiB
C#

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;
/// <summary>
/// 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
/// </summary>
public partial class PreorderConversionService
{
// ── Product replacement ───────────────────────────────────────────────────
/// <summary>
/// Called from FruitBankDataController.UpdateShippingItem when ProductId changes.
/// oldItem must be loaded BEFORE the DB update to capture the previous state.
/// </summary>
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<int>();
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<int> { 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<List<OrderDto>> 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();
}
}