232 lines
9.5 KiB
C#
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
|
|
});
|
|
}
|