Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/Services/FruitBankOrderItemService.cs

232 lines
9.5 KiB
C#

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;
/// <summary>
/// Shared service for creating, adding and removing order items.
/// Extracted from CustomOrderController so the same logic can be reused
/// by PreorderConversionService without duplication.
/// </summary>
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<IAcLogWriterBase> logWriters)
{
_dbContext = dbContext;
_productService = productService;
_priceCalculationService = priceCalculationService;
_taxService = taxService;
_orderService = orderService;
_storeContext = storeContext;
_localizationService = localizationService;
_logger = new Logger<FruitBankOrderItemService>(logWriters.ToArray());
}
// ── Create ────────────────────────────────────────────────────────────────
/// <summary>
/// Builds an OrderItem entity — no DB writes.
/// unitPricesIncludeDiscounts=true recalculates price from product;
/// false uses the supplied price as-is (manual override).
/// </summary>
public async Task<OrderItem> 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 ───────────────────────────────────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
public async Task AddOrderItemsThenUpdateOrderAsync(
Order order,
IReadOnlyList<IOrderProductItemBase> 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 ────────────────────────────────────────────────────────────────
/// <summary>
/// Removes an OrderItem: deletes pallets/GAs/weight constraints,
/// restores inventory, deletes the item, adjusts order totals.
/// Caller must call UpdateOrderAsync after.
/// </summary>
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
});
}