using AyCode.Core.Loggers; using FruitBank.Common.Dtos; using FruitBank.Common.Interfaces; using Mango.Nop.Core.Loggers; using Nop.Core; using Nop.Core.Domain.Catalog; using Nop.Core.Domain.Customers; using Nop.Core.Domain.Orders; using Nop.Core.Domain.Stores; using Nop.Core.Domain.Tax; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Models.Orders; using Nop.Services.Catalog; using Nop.Services.Localization; using Nop.Services.Orders; using Nop.Services.Tax; namespace Nop.Plugin.Misc.FruitBankPlugin.Services; /// /// Shared service for creating, adding and removing order items. /// Extracted from CustomOrderController so the same logic can be reused /// by PreorderConversionService without duplication. /// public class FruitBankOrderItemService { private readonly FruitBankDbContext _dbContext; private readonly IProductService _productService; private readonly IPriceCalculationService _priceCalculationService; private readonly ITaxService _taxService; private readonly IOrderService _orderService; private readonly IStoreContext _storeContext; private readonly ILocalizationService _localizationService; private readonly ILogger _logger; public FruitBankOrderItemService( FruitBankDbContext dbContext, IProductService productService, IPriceCalculationService priceCalculationService, ITaxService taxService, IOrderService orderService, IStoreContext storeContext, ILocalizationService localizationService, IEnumerable logWriters) { _dbContext = dbContext; _productService = productService; _priceCalculationService = priceCalculationService; _taxService = taxService; _orderService = orderService; _storeContext = storeContext; _localizationService = localizationService; _logger = new Logger(logWriters.ToArray()); } // ── Create ──────────────────────────────────────────────────────────────── /// /// Builds an OrderItem entity — no DB writes. /// unitPricesIncludeDiscounts=true recalculates price from product; /// false uses the supplied price as-is (manual override). /// public async Task CreateOrderItemAsync( Product product, Order order, int productId, int quantity, decimal price, bool isMeasurable, bool unitPricesIncludeDiscounts, Customer? customer = null, Store? store = null) { store ??= await _storeContext.GetCurrentStoreAsync(); customer ??= new Customer { Id = order.CustomerId }; decimal unitPriceInclTax; if (unitPricesIncludeDiscounts) { var calc = await _priceCalculationService.GetFinalPriceAsync( product, customer, store, includeDiscounts: true); unitPriceInclTax = calc.finalPrice; } else { unitPriceInclTax = price; } var (unitPriceExclTax, _) = await _taxService.GetProductPriceAsync( product, unitPriceInclTax, includingTax: false, customer); return new OrderItem { OrderId = order.Id, ProductId = productId, Quantity = quantity, OrderItemGuid = Guid.NewGuid(), UnitPriceInclTax = unitPriceInclTax, UnitPriceExclTax = unitPriceExclTax, PriceInclTax = isMeasurable ? 0m : unitPriceInclTax * quantity, PriceExclTax = isMeasurable ? 0m : unitPriceExclTax * quantity, OriginalProductCost = await _priceCalculationService.GetProductCostAsync(product, null), AttributeDescription = string.Empty, AttributesXml = string.Empty, DiscountAmountInclTax = decimal.Zero, DiscountAmountExclTax = decimal.Zero, DownloadCount = 0, IsDownloadActivated = false, LicenseDownloadId = 0, ItemWeight = product.Weight * quantity }; } // ── Add ─────────────────────────────────────────────────────────────────── /// /// Inserts a single OrderItem, deducts inventory, and updates order totals. /// Does NOT check availability — caller is responsible for that guard. /// Does NOT call UpdateOrderAsync — caller must do that after all items are added. /// public async Task AddOrderItemToOrderAsync( Order order, OrderItem orderItem, string inventoryMessage) { await _orderService.InsertOrderItemAsync(orderItem); var product = await _productService.GetProductByIdAsync(orderItem.ProductId); if (product != null) await _productService.AdjustInventoryAsync( product, -orderItem.Quantity, orderItem.AttributesXml, inventoryMessage); order.OrderSubtotalInclTax += orderItem.UnitPriceInclTax * orderItem.Quantity; order.OrderSubtotalExclTax += orderItem.UnitPriceExclTax * orderItem.Quantity; order.OrderTotal += orderItem.PriceInclTax; } /// /// Batch add — mirrors the original CustomOrderController.AddOrderItemsThenUpdateOrder. /// Checks availability, creates each OrderItem, adjusts inventory, /// updates order totals, then persists the order and writes an order note. /// public async Task AddOrderItemsThenUpdateOrderAsync( Order order, IReadOnlyList items, Customer? customer = null, Store? store = null, Customer? admin = null) { store ??= await _storeContext.GetCurrentStoreAsync(); customer ??= new Customer { Id = order.CustomerId }; admin ??= customer; var productDtosById = await _dbContext.ProductDtos .GetAllByIds(items.Select(x => x.Id).ToArray()) .ToDictionaryAsync(k => k.Id, v => v); foreach (var item in items) { var product = await _productService.GetProductByIdAsync(item.Id); if (product == null) { _logger.Warning($"AddOrderItemsThenUpdateOrderAsync: product #{item.Id} not found, skipped"); continue; } productDtosById.TryGetValue(item.Id, out var dto); var isMeasurable = dto?.IsMeasurable ?? false; var available = product.StockQuantity + (dto?.IncomingQuantity ?? 0); if (available - item.Quantity < 0) throw new Exception( $"Insufficient stock for product #{product.Id} " + $"(available: {available}, requested: {item.Quantity})"); bool useDiscountPrice = item.Price == product.Price; var orderItem = await CreateOrderItemAsync( product, order, item.Id, item.Quantity, item.Price, isMeasurable, useDiscountPrice, customer, store); var msg = string.Format( await _localizationService.GetResourceAsync( "Admin.StockQuantityHistory.Messages.PlaceOrder"), order.Id); await AddOrderItemToOrderAsync(order, orderItem, msg); } await _orderService.UpdateOrderAsync(order); await InsertOrderNoteAsync(order.Id, false, $"Products added ({items.Count} item(s)) by " + $"{admin.FirstName} {admin.LastName} (Id: {admin.Id})"); } // ── Remove ──────────────────────────────────────────────────────────────── /// /// Removes an OrderItem: deletes pallets/GAs/weight constraints, /// restores inventory, deletes the item, adjusts order totals. /// Caller must call UpdateOrderAsync after. /// public async Task RemoveOrderItemFromOrderAsync( Order order, OrderItem orderItem, string inventoryMessage) { await _dbContext.DeleteOrderItemConstraintsAsync(orderItem); var product = await _productService.GetProductByIdAsync(orderItem.ProductId); if (product != null) await _productService.AdjustInventoryAsync( product, +orderItem.Quantity, orderItem.AttributesXml, inventoryMessage); await _orderService.DeleteOrderItemAsync(orderItem); order.OrderSubtotalInclTax -= orderItem.UnitPriceInclTax * orderItem.Quantity; order.OrderSubtotalExclTax -= orderItem.UnitPriceExclTax * orderItem.Quantity; order.OrderTotal -= orderItem.PriceInclTax; } // ── Helpers ─────────────────────────────────────────────────────────────── public Task InsertOrderNoteAsync(int orderId, bool displayToCustomer, string note) => _orderService.InsertOrderNoteAsync(new OrderNote { OrderId = orderId, Note = note, DisplayToCustomer = displayToCustomer, CreatedOnUtc = DateTime.UtcNow }); }