631 lines
27 KiB
C#
631 lines
27 KiB
C#
using FruitBank.Common.Entities;
|
|
using FruitBank.Common.Server;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Nop.Core;
|
|
using Nop.Core.Domain.Catalog;
|
|
using Nop.Core.Domain.Orders;
|
|
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
|
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
|
using Nop.Services.Catalog;
|
|
using Nop.Services.Customers;
|
|
using Nop.Services.Localization;
|
|
using Nop.Services.Orders;
|
|
using Nop.Web.Framework.Controllers;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers;
|
|
|
|
[AutoValidateAntiforgeryToken]
|
|
public class OrderController : BasePluginController
|
|
{
|
|
private readonly IWorkContext _workContext;
|
|
private readonly IStoreContext _storeContext;
|
|
private readonly ICustomerService _customerService;
|
|
private readonly ILocalizationService _localizationService;
|
|
private readonly FruitBankDbContext _dbContext;
|
|
private readonly PreorderDbContext _preorderDbContext;
|
|
private readonly FruitBankAttributeService _fruitBankAttributeService;
|
|
private readonly CustomPriceCalculationService _customPriceCalculationService;
|
|
private readonly IShoppingCartService _shoppingCartService;
|
|
private readonly IProductService _productService;
|
|
private readonly OpenAIApiService _aiApiService;
|
|
private readonly CerebrasAPIService _cerebrasApiService;
|
|
private readonly PreorderConversionService _preorderConversionService;
|
|
|
|
private const string PendingDeliveryKey = "OrderFlowPendingDeliveryDateTime";
|
|
|
|
public OrderController(
|
|
IWorkContext workContext,
|
|
IStoreContext storeContext,
|
|
ICustomerService customerService,
|
|
ILocalizationService localizationService,
|
|
FruitBankDbContext dbContext,
|
|
PreorderDbContext preorderDbContext,
|
|
FruitBankAttributeService fruitBankAttributeService,
|
|
IPriceCalculationService priceCalculationService,
|
|
IShoppingCartService shoppingCartService,
|
|
IProductService productService,
|
|
OpenAIApiService aiApiService,
|
|
CerebrasAPIService cerebrasApiService,
|
|
PreorderConversionService preorderConversionService)
|
|
{
|
|
_workContext = workContext;
|
|
_storeContext = storeContext;
|
|
_customerService = customerService;
|
|
_localizationService = localizationService;
|
|
_dbContext = dbContext;
|
|
_preorderDbContext = preorderDbContext;
|
|
_fruitBankAttributeService = fruitBankAttributeService;
|
|
_customPriceCalculationService = priceCalculationService as CustomPriceCalculationService;
|
|
_shoppingCartService = shoppingCartService;
|
|
_productService = productService;
|
|
_aiApiService = aiApiService;
|
|
_cerebrasApiService = cerebrasApiService;
|
|
_preorderConversionService = preorderConversionService;
|
|
}
|
|
|
|
// ── INDEX ─────────────────────────────────────────────────────────────────
|
|
|
|
[HttpGet]
|
|
public async Task<IActionResult> Index()
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Challenge();
|
|
|
|
return View("~/Plugins/Misc.FruitBankPlugin/Views/Order/Index.cshtml");
|
|
}
|
|
|
|
// ── FLOW TYPE ─────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Mon/Tue/Wed → preorder regardless of delivery date.
|
|
/// Thu/Fri/Sat/Sun + delivery this week → quickorder.
|
|
/// Thu/Fri/Sat/Sun + delivery next week or later → preorder.
|
|
/// </summary>
|
|
public static string ComputeFlowType(DateTime deliveryDate)
|
|
{
|
|
var today = DateTime.Today;
|
|
var todayDow = (int)today.DayOfWeek; // 0=Sun 1=Mon … 6=Sat
|
|
|
|
// This week's Thursday
|
|
int daysSinceMon = todayDow == 0 ? 6 : todayDow - 1;
|
|
var weekStart = today.AddDays(-daysSinceMon); // Monday
|
|
var thisThursday = weekStart.AddDays(3); // Thursday
|
|
var weekEnd = weekStart.AddDays(6); // Sunday
|
|
|
|
bool deliveryBeforeThursday = deliveryDate.Date < thisThursday;
|
|
bool isLateWeek = todayDow == 0 || todayDow >= 4; // Thu-Sun
|
|
bool deliveryThisWeek = deliveryDate.Date >= weekStart && deliveryDate.Date <= weekEnd;
|
|
|
|
// Quick Order: delivery needs current stock (before Thursday)
|
|
// OR goods already arrived (Thu-Sun) and delivery still this week
|
|
// Preorder: delivery is Thursday+ but today is still Mon/Tue/Wed (goods not yet here)
|
|
return (deliveryBeforeThursday || (isLateWeek && deliveryThisWeek))
|
|
? "quickorder"
|
|
: "preorder";
|
|
}
|
|
|
|
// ── GET / SET DELIVERY DATETIME ───────────────────────────────────────────
|
|
|
|
[HttpGet]
|
|
public async Task<IActionResult> GetDeliveryDateTime()
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false });
|
|
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var saved = await _fruitBankAttributeService
|
|
.GetGenericAttributeValueAsync<Nop.Core.Domain.Customers.Customer, DateTime?>(
|
|
customer.Id, PendingDeliveryKey, store.Id);
|
|
|
|
if (!saved.HasValue)
|
|
return Json(new { success = true, hasValue = false });
|
|
|
|
var flowType = ComputeFlowType(saved.Value);
|
|
|
|
return Json(new
|
|
{
|
|
success = true,
|
|
hasValue = true,
|
|
date = saved.Value.ToString("yyyy-MM-dd"),
|
|
time = saved.Value.ToString("HH:mm"),
|
|
iso = saved.Value.ToString("yyyy-MM-ddTHH:mm"),
|
|
flowType
|
|
});
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> SetDeliveryDateTime(string deliveryDateTime)
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false, message = "Nincs bejelentkezve" });
|
|
|
|
if (string.IsNullOrWhiteSpace(deliveryDateTime) ||
|
|
!DateTime.TryParse(deliveryDateTime, out var parsed))
|
|
return Json(new { success = false, message = "Érvénytelen dátum/idő formátum" });
|
|
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Nop.Core.Domain.Customers.Customer, DateTime>(
|
|
customer.Id, PendingDeliveryKey, parsed, store.Id);
|
|
|
|
var flowType = ComputeFlowType(parsed);
|
|
|
|
Console.WriteLine($"[OrderFlow] SetDeliveryDateTime — customer #{customer.Id}, {parsed:u}, flowType={flowType}");
|
|
return Json(new { success = true, flowType });
|
|
}
|
|
|
|
// ── PRODUCTS — Quick Order flow (all available stock) ─────────────────────
|
|
|
|
[HttpGet]
|
|
public async Task<IActionResult> GetAllProducts(string deliveryDate = null, string deliveryTime = null)
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false, message = "Nincs bejelentkezve" });
|
|
|
|
try
|
|
{
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var allProductDtos = (await _dbContext.ProductDtos.GetAll(true).ToListAsync())
|
|
.Where(pd => pd.AvailableQuantity > 0);
|
|
|
|
var result = new List<object>();
|
|
foreach (var product in allProductDtos)
|
|
{
|
|
var availableQty = product.StockQuantity + product.IncomingQuantity;
|
|
if (availableQty <= 0) continue;
|
|
|
|
decimal? unitPrice = null;
|
|
if (!product.IsMeasurable && _customPriceCalculationService != null)
|
|
{
|
|
var tproduct = await _productService.GetProductByIdAsync(product.Id);
|
|
if (tproduct != null)
|
|
{
|
|
var pr = await _customPriceCalculationService.GetFinalPriceAsync(
|
|
tproduct, customer, store, null, 0, true, 1, null, null);
|
|
unitPrice = pr.finalPrice;
|
|
}
|
|
}
|
|
|
|
result.Add(new
|
|
{
|
|
id = product.Id,
|
|
name = product.Name,
|
|
quantity = 1,
|
|
unitPrice,
|
|
stockQuantity = availableQty,
|
|
searchTerm = (string)null,
|
|
isQuantityReduced = false,
|
|
isMeasurable = product.IsMeasurable
|
|
});
|
|
}
|
|
|
|
return Json(new { success = true, products = result });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
|
|
}
|
|
}
|
|
|
|
// ── PRODUCTS — Preorder flow (curated window list) ────────────────────────
|
|
|
|
[HttpGet]
|
|
public async Task<IActionResult> GetPreorderProducts()
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false, message = "Nincs bejelentkezve" });
|
|
|
|
try
|
|
{
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var today = DateTime.UtcNow.Date;
|
|
|
|
var gaStart = await _dbContext.GenericAttributes.Table
|
|
.Where(ga => ga.KeyGroup == nameof(Product)
|
|
&& ga.Key == FruitBankConst.PreorderWindowStart
|
|
&& ga.StoreId == store.Id)
|
|
.ToListAsync();
|
|
var gaEnd = await _dbContext.GenericAttributes.Table
|
|
.Where(ga => ga.KeyGroup == nameof(Product)
|
|
&& ga.Key == FruitBankConst.PreorderWindowEnd
|
|
&& ga.StoreId == store.Id)
|
|
.ToListAsync();
|
|
|
|
var startById = gaStart.ToDictionary(g => g.EntityId, g => g.Value);
|
|
var endById = gaEnd.ToDictionary(g => g.EntityId, g => g.Value);
|
|
|
|
var availableIds = startById.Keys.Intersect(endById.Keys)
|
|
.Where(id =>
|
|
{
|
|
DateTime.TryParse(startById[id], out var ws);
|
|
DateTime.TryParse(endById[id], out var we);
|
|
return ws.Date <= today && today <= we.Date;
|
|
})
|
|
.ToHashSet();
|
|
|
|
if (!availableIds.Any())
|
|
return Json(new { success = true, products = Array.Empty<object>() });
|
|
|
|
var productDtos = await _dbContext.ProductDtos
|
|
.GetAll(true)
|
|
.Where(p => availableIds.Contains(p.Id))
|
|
.ToListAsync();
|
|
|
|
var result = new List<object>();
|
|
foreach (var dto in productDtos.OrderBy(p => p.Name))
|
|
{
|
|
decimal? unitPrice = null;
|
|
if (!dto.IsMeasurable && _customPriceCalculationService != null)
|
|
{
|
|
var product = await _dbContext.Products.GetByIdAsync(dto.Id);
|
|
if (product != null)
|
|
{
|
|
var pr = await _customPriceCalculationService.GetFinalPriceAsync(
|
|
product, customer, store, null, 0, true, 1, null, null);
|
|
unitPrice = pr.finalPrice;
|
|
}
|
|
}
|
|
|
|
result.Add(new
|
|
{
|
|
id = dto.Id,
|
|
name = dto.Name,
|
|
isMeasurable = dto.IsMeasurable,
|
|
unitPrice,
|
|
stockQuantity = dto.AvailableQuantity
|
|
});
|
|
}
|
|
|
|
return Json(new { success = true, products = result });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
|
|
}
|
|
}
|
|
|
|
// ── SEARCH (Quick Order flow) ─────────────────────────────────────────────
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> SearchProducts(string text, string deliveryDate = null, string deliveryTime = null)
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false, message = "Nincs bejelentkezve" });
|
|
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
return Json(new { success = false, message = "Nincs szöveg megadva" });
|
|
|
|
try
|
|
{
|
|
var parsedProducts = await ParseProductsFromText(text);
|
|
if (parsedProducts == null || !parsedProducts.Any())
|
|
return Json(new { success = false, message = "Nem sikerült termékeket azonosítani", transcription = text });
|
|
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var enriched = await EnrichProductData(parsedProducts, customer, store);
|
|
return Json(new { success = true, transcription = text, products = enriched });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
|
|
}
|
|
}
|
|
|
|
// ── VOICE (Quick Order flow) ──────────────────────────────────────────────
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> TranscribeAndSearch(
|
|
Microsoft.AspNetCore.Http.IFormFile audioFile,
|
|
string deliveryDate = null, string deliveryTime = null)
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false, message = "Nincs bejelentkezve" });
|
|
|
|
if (audioFile == null || audioFile.Length == 0)
|
|
return Json(new { success = false, message = "Nem érkezett hangfájl" });
|
|
|
|
try
|
|
{
|
|
var text = await TranscribeAudioFile(audioFile, "hu");
|
|
if (string.IsNullOrEmpty(text))
|
|
return Json(new { success = false, message = "Nem sikerült a hangfelismerés" });
|
|
|
|
var parsedProducts = await ParseProductsFromText(text);
|
|
if (parsedProducts == null || !parsedProducts.Any())
|
|
return Json(new { success = false, message = "Nem sikerült termékeket azonosítani", transcription = text });
|
|
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var enriched = await EnrichProductData(parsedProducts, customer, store);
|
|
return Json(new { success = true, transcription = text, products = enriched });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
|
|
}
|
|
}
|
|
|
|
// ── ADD TO CART (Quick Order flow) ────────────────────────────────────────
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> AddToCart(int productId, int quantity)
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false, message = "Nincs bejelentkezve" });
|
|
|
|
if (productId <= 0 || quantity <= 0)
|
|
return Json(new { success = false, message = "Érvénytelen termék vagy mennyiség" });
|
|
|
|
try
|
|
{
|
|
var product = await _productService.GetProductByIdAsync(productId);
|
|
if (product == null || product.Deleted || !product.Published)
|
|
return Json(new { success = false, message = "A termék nem elérhető" });
|
|
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var warnings = await _shoppingCartService.AddToCartAsync(
|
|
customer, product, ShoppingCartType.ShoppingCart, store.Id, quantity: quantity);
|
|
|
|
if (warnings.Any())
|
|
return Json(new { success = false, message = string.Join("; ", warnings) });
|
|
|
|
var cartItems = await GetCartItemsJson(customer, store);
|
|
return Json(new { success = true, cartItems });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
|
|
}
|
|
}
|
|
|
|
// ── GET CART (Quick Order flow) ───────────────────────────────────────────
|
|
|
|
[HttpGet]
|
|
public async Task<IActionResult> GetCartItems()
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false });
|
|
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
return Json(new { success = true, cartItems = await GetCartItemsJson(customer, store) });
|
|
}
|
|
|
|
// ── PLACE PREORDER (Preorder flow) ────────────────────────────────────────
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> PlacePreorder([FromBody] PlacePreorderRequest request)
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false, message = "Nincs bejelentkezve" });
|
|
|
|
if (request?.Items == null || !request.Items.Any())
|
|
return Json(new { success = false, message = "Nincs kiválasztott termék" });
|
|
|
|
if (!DateTime.TryParse(request.DeliveryDateTime, out var deliveryDateTime))
|
|
return Json(new { success = false, message = "Érvénytelen szállítási dátum/idő" });
|
|
|
|
try
|
|
{
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
|
|
var preorder = new Preorder
|
|
{
|
|
CustomerId = customer.Id,
|
|
StoreId = store.Id,
|
|
DateOfReceipt = deliveryDateTime,
|
|
CustomerNote = request.CustomerNote?.Trim()
|
|
};
|
|
|
|
var items = new List<PreorderItem>();
|
|
foreach (var req in request.Items.Where(i => i.Quantity > 0))
|
|
{
|
|
var product = await _dbContext.Products.GetByIdAsync(req.ProductId);
|
|
if (product == null || product.Deleted || !product.Published) continue;
|
|
|
|
decimal unitPrice = 0;
|
|
if (_customPriceCalculationService != null)
|
|
{
|
|
var pr = await _customPriceCalculationService.GetFinalPriceAsync(
|
|
product, customer, store, null, 0, true, req.Quantity, null, null);
|
|
unitPrice = pr.finalPrice;
|
|
}
|
|
|
|
items.Add(new PreorderItem
|
|
{
|
|
ProductId = req.ProductId,
|
|
RequestedQuantity = req.Quantity,
|
|
UnitPriceInclTax = unitPrice
|
|
});
|
|
}
|
|
|
|
if (!items.Any())
|
|
return Json(new { success = false, message = "Nincs érvényes termék az előrendelésben" });
|
|
|
|
var saved = await _preorderDbContext.InsertPreorderAsync(preorder, items);
|
|
|
|
// Clean up the pending datetime attribute
|
|
await _fruitBankAttributeService
|
|
.DeleteGenericAttributeAsync<Nop.Core.Domain.Customers.Customer>(
|
|
customer.Id, PendingDeliveryKey, store.Id);
|
|
|
|
// Immediately check if any items can be fulfilled from current available stock.
|
|
// Awaited inline (not fire-and-forget) so we can return the order ID if one is created.
|
|
// shippingDocumentId = 0 signals this was triggered at preorder placement, not by a document.
|
|
var productIds = items.Select(i => i.ProductId).Distinct().ToList();
|
|
await _preorderConversionService.ConvertPreordersForProductsAsync(productIds, 0);
|
|
|
|
// Re-read to pick up OrderId if conversion created a real order
|
|
var refreshed = await _preorderDbContext.Preorders.GetByIdAsync(saved.Id);
|
|
|
|
Console.WriteLine($"[OrderFlow] PlacePreorder #{saved.Id} — orderId={refreshed?.OrderId}");
|
|
|
|
return Json(new
|
|
{
|
|
success = true,
|
|
preorderId = saved.Id,
|
|
orderId = refreshed?.OrderId
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
|
|
}
|
|
}
|
|
|
|
// ── PRIVATE HELPERS ───────────────────────────────────────────────────────
|
|
|
|
private async Task<string> TranscribeAudioFile(Microsoft.AspNetCore.Http.IFormFile audioFile, string language)
|
|
{
|
|
var fileName = $"order_{DateTime.Now:yyyyMMdd_HHmmss}.webm";
|
|
var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "voice");
|
|
if (!Directory.Exists(uploadsFolder)) Directory.CreateDirectory(uploadsFolder);
|
|
|
|
var filePath = Path.Combine(uploadsFolder, fileName);
|
|
using (var stream = new FileStream(filePath, FileMode.Create))
|
|
await audioFile.CopyToAsync(stream);
|
|
|
|
string text;
|
|
using (var audioStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
|
text = await _aiApiService.TranscribeAudioAsync(audioStream, fileName, language, null);
|
|
|
|
if (!string.IsNullOrEmpty(text) && (text.EndsWith(".") || text.EndsWith("!") || text.EndsWith("?")))
|
|
text = text[..^1];
|
|
|
|
try { System.IO.File.Delete(filePath); } catch { }
|
|
return text;
|
|
}
|
|
|
|
private async Task<List<ParsedProduct>> ParseProductsFromText(string text)
|
|
{
|
|
var systemPrompt = @"You are a product parser for a Hungarian fruit and vegetable wholesale company.
|
|
Parse the product names and quantities from the user's input.
|
|
CRITICAL RULES:
|
|
1. Normalize product names to singular, lowercase
|
|
2. Handle Hungarian number words
|
|
3. Fix common transcription/typing errors
|
|
4. Return ONLY valid JSON array
|
|
OUTPUT FORMAT: [{""product"": ""narancs"", ""quantity"": 100}]";
|
|
|
|
var aiResponse = await _cerebrasApiService.GetSimpleResponseAsync(systemPrompt, $"Parse this: {text}");
|
|
var jsonMatch = Regex.Match(aiResponse, @"\[.*\]", RegexOptions.Singleline);
|
|
if (!jsonMatch.Success) return new List<ParsedProduct>();
|
|
try
|
|
{
|
|
return System.Text.Json.JsonSerializer.Deserialize<List<ParsedProduct>>(jsonMatch.Value)
|
|
?? new List<ParsedProduct>();
|
|
}
|
|
catch { return new List<ParsedProduct>(); }
|
|
}
|
|
|
|
private async Task<List<object>> EnrichProductData(
|
|
List<ParsedProduct> parsedProducts,
|
|
Nop.Core.Domain.Customers.Customer customer,
|
|
Nop.Core.Domain.Stores.Store store)
|
|
{
|
|
var enriched = new List<object>();
|
|
var allDtos = await _dbContext.ProductDtos.GetAll(true).ToListAsync();
|
|
|
|
foreach (var parsed in parsedProducts)
|
|
{
|
|
var dbProducts = await _productService.SearchProductsAsync(
|
|
keywords: parsed.Product, pageIndex: 0, pageSize: 20);
|
|
|
|
foreach (var product in dbProducts)
|
|
{
|
|
var dto = allDtos.FirstOrDefault(x => x.Id == product.Id);
|
|
if (dto == null) continue;
|
|
var available = product.StockQuantity + dto.IncomingQuantity;
|
|
if (available <= 0) continue;
|
|
|
|
var finalQty = Math.Min(parsed.Quantity, available);
|
|
var isReduced = finalQty < parsed.Quantity;
|
|
decimal? price = null;
|
|
if (!dto.IsMeasurable && _customPriceCalculationService != null)
|
|
{
|
|
var pr = await _customPriceCalculationService.GetFinalPriceAsync(
|
|
product, customer, store, null, 0, true, finalQty, null, null);
|
|
price = pr.finalPrice;
|
|
}
|
|
|
|
enriched.Add(new
|
|
{
|
|
id = product.Id,
|
|
name = product.Name,
|
|
quantity = finalQty,
|
|
requestedQuantity = parsed.Quantity,
|
|
unitPrice = price,
|
|
stockQuantity = available,
|
|
searchTerm = parsed.Product,
|
|
isQuantityReduced = isReduced,
|
|
isMeasurable = dto.IsMeasurable
|
|
});
|
|
}
|
|
}
|
|
return enriched;
|
|
}
|
|
|
|
private async Task<List<object>> GetCartItemsJson(
|
|
Nop.Core.Domain.Customers.Customer customer,
|
|
Nop.Core.Domain.Stores.Store store)
|
|
{
|
|
var cart = await _shoppingCartService.GetShoppingCartAsync(customer, ShoppingCartType.ShoppingCart, store.Id);
|
|
var allDtos = await _dbContext.ProductDtos.GetAll(true).ToListAsync();
|
|
var result = new List<object>();
|
|
|
|
foreach (var item in cart)
|
|
{
|
|
var product = await _productService.GetProductByIdAsync(item.ProductId);
|
|
if (product == null) continue;
|
|
var dto = allDtos.FirstOrDefault(x => x.Id == product.Id);
|
|
var isMeasurable = dto?.IsMeasurable ?? false;
|
|
decimal? price = null;
|
|
if (!isMeasurable && _customPriceCalculationService != null)
|
|
{
|
|
var pr = await _customPriceCalculationService.GetFinalPriceAsync(
|
|
product, customer, store, null, 0, true, item.Quantity, null, null);
|
|
price = pr.finalPrice;
|
|
}
|
|
result.Add(new { id = item.Id, productId = item.ProductId, name = product.Name,
|
|
quantity = item.Quantity, unitPrice = price, isMeasurable });
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ── Inner models ──────────────────────────────────────────────────────────
|
|
|
|
public class PlacePreorderRequest
|
|
{
|
|
public string? DeliveryDateTime { get; set; }
|
|
public string? CustomerNote { get; set; }
|
|
public List<PreorderItemRequest> Items { get; set; } = new();
|
|
}
|
|
|
|
public class PreorderItemRequest
|
|
{
|
|
public int ProductId { get; set; }
|
|
public int Quantity { get; set; }
|
|
}
|
|
|
|
private class ParsedProduct
|
|
{
|
|
[System.Text.Json.Serialization.JsonPropertyName("product")]
|
|
public string Product { get; set; }
|
|
[System.Text.Json.Serialization.JsonPropertyName("quantity")]
|
|
public int Quantity { get; set; }
|
|
}
|
|
}
|