Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/Controllers/QuickOrderController.cs

442 lines
18 KiB
C#

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Nop.Core;
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 QuickOrderController : BasePluginController
{
private readonly IWorkContext _workContext;
private readonly IStoreContext _storeContext;
private readonly IProductService _productService;
private readonly IShoppingCartService _shoppingCartService;
private readonly ICustomerService _customerService;
private readonly ILocalizationService _localizationService;
private readonly CustomPriceCalculationService _customPriceCalculationService;
private readonly OpenAIApiService _aiApiService;
private readonly CerebrasAPIService _cerebrasApiService;
private readonly FruitBankDbContext _dbContext;
// Resource key prefix
private const string Prefix = "Plugins.Misc.FruitBankPlugin.QuickOrder.";
public QuickOrderController(
IWorkContext workContext,
IStoreContext storeContext,
IProductService productService,
IShoppingCartService shoppingCartService,
ICustomerService customerService,
ILocalizationService localizationService,
IPriceCalculationService priceCalculationService,
OpenAIApiService aiApiService,
CerebrasAPIService cerebrasApiService,
FruitBankDbContext dbContext)
{
_workContext = workContext;
_storeContext = storeContext;
_productService = productService;
_shoppingCartService = shoppingCartService;
_customerService = customerService;
_localizationService = localizationService;
_customPriceCalculationService = priceCalculationService as CustomPriceCalculationService;
_aiApiService = aiApiService;
_cerebrasApiService = cerebrasApiService;
_dbContext = dbContext;
}
[HttpGet]
public async Task<IActionResult> Index()
{
var customer = await _workContext.GetCurrentCustomerAsync();
if (await _customerService.IsGuestAsync(customer))
return Challenge();
return View("~/Plugins/Misc.FruitBankPlugin/Views/QuickOrder/Index.cshtml");
}
/// <summary>
/// Return all available products with prices (for initial page load)
/// </summary>
[HttpGet]
public async Task<IActionResult> GetAllProducts()
{
var customer = await _workContext.GetCurrentCustomerAsync();
if (await _customerService.IsGuestAsync(customer))
return Json(new { success = false, message = await L("NotLoggedIn") });
try
{
var store = await _storeContext.GetCurrentStoreAsync();
var allProductDtos = (await _dbContext.ProductDtos.GetAll(true).ToListAsync()).Where(pd => pd.AvailableQuantity > 0);
//var dbProducts = await _productService.SearchProductsAsync(
// pageIndex: 0,
// pageSize: 500,
// orderBy: );
var result = new List<object>();
foreach (var product in allProductDtos)
{
var productDto = allProductDtos.FirstOrDefault(x => x.Id == product.Id);
if (productDto == null) continue;
var availableQty = product.StockQuantity + productDto.IncomingQuantity;
if (availableQty <= 0) continue;
decimal? unitPrice = null;
if (!productDto.IsMeasurable)
{
var tproduct = await _productService.GetProductByIdAsync(productDto.Id);
var priceResult = await _customPriceCalculationService.GetFinalPriceAsync(
tproduct, customer, store, null, 0, true, 1, null, null);
unitPrice = priceResult.finalPrice;
}
result.Add(new
{
id = product.Id,
name = product.Name,
quantity = 1,
requestedQuantity = 1,
unitPrice,
stockQuantity = availableQty,
searchTerm = (string)null,
isQuantityReduced = false,
isMeasurable = productDto.IsMeasurable
});
}
return Json(new { success = true, products = result });
}
catch (Exception ex)
{
Console.WriteLine($"[QuickOrder] GetAllProducts error: {ex.Message}");
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
}
}
/// <summary>
/// Parse a manually typed product list and return matching products with prices
/// </summary>
[HttpPost]
public async Task<IActionResult> SearchProducts(string text)
{
var customer = await _workContext.GetCurrentCustomerAsync();
if (await _customerService.IsGuestAsync(customer))
return Json(new { success = false, message = await L("NotLoggedIn") });
if (string.IsNullOrWhiteSpace(text))
return Json(new { success = false, message = await L("NoTextProvided") });
try
{
var parsedProducts = await ParseProductsFromText(text);
if (parsedProducts == null || parsedProducts.Count == 0)
return Json(new { success = false, message = await L("NoProductsIdentified"), transcription = text });
var store = await _storeContext.GetCurrentStoreAsync();
var enrichedProducts = await EnrichProductData(parsedProducts, customer, store);
return Json(new { success = true, transcription = text, products = enrichedProducts });
}
catch (Exception ex)
{
Console.WriteLine($"[QuickOrder] SearchProducts error: {ex.Message}");
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
}
}
/// <summary>
/// Transcribe voice audio (Hungarian) then parse and match products
/// </summary>
[HttpPost]
public async Task<IActionResult> TranscribeAndSearch(IFormFile audioFile)
{
var customer = await _workContext.GetCurrentCustomerAsync();
if (await _customerService.IsGuestAsync(customer))
return Json(new { success = false, message = await L("NotLoggedIn") });
if (audioFile == null || audioFile.Length == 0)
return Json(new { success = false, message = await L("NoAudioReceived") });
try
{
var transcribedText = await TranscribeAudioFile(audioFile, "hu");
if (string.IsNullOrEmpty(transcribedText))
return Json(new { success = false, message = await L("TranscriptionFailed") });
Console.WriteLine($"[QuickOrder] Transcription: {transcribedText}");
var parsedProducts = await ParseProductsFromText(transcribedText);
if (parsedProducts == null || parsedProducts.Count == 0)
return Json(new { success = false, message = await L("NoProductsIdentified"), transcription = transcribedText });
var store = await _storeContext.GetCurrentStoreAsync();
var enrichedProducts = await EnrichProductData(parsedProducts, customer, store);
return Json(new { success = true, transcription = transcribedText, products = enrichedProducts });
}
catch (Exception ex)
{
Console.WriteLine($"[QuickOrder] TranscribeAndSearch error: {ex.Message}");
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
}
}
/// <summary>
/// Add a product to the current customer's shopping cart and return the updated cart
/// </summary>
[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 = await L("NotLoggedIn") });
if (productId <= 0 || quantity <= 0)
return Json(new { success = false, message = await L("InvalidProductOrQuantity") });
try
{
var product = await _productService.GetProductByIdAsync(productId);
if (product == null || product.Deleted || !product.Published)
return Json(new { success = false, message = await L("ProductNotAvailable") });
var store = await _storeContext.GetCurrentStoreAsync();
var warnings = await _shoppingCartService.AddToCartAsync(
customer: customer,
product: product,
shoppingCartType: ShoppingCartType.ShoppingCart,
storeId: 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)
{
Console.WriteLine($"[QuickOrder] AddToCart error: {ex.Message}");
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
}
}
/// <summary>
/// Return the current customer's cart as JSON (for cart panel refresh)
/// </summary>
[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();
var cartItems = await GetCartItemsJson(customer, store);
return Json(new { success = true, cartItems });
}
#region Private helpers
/// <summary>Shorthand: get a localized QuickOrder resource string</summary>
private Task<string> L(string keySuffix)
=> _localizationService.GetResourceAsync(Prefix + keySuffix);
private async Task<string> TranscribeAudioFile(IFormFile audioFile, string language)
{
var fileName = $"quick_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 transcribedText;
using (var audioStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
transcribedText = await _aiApiService.TranscribeAudioAsync(audioStream, fileName, language, null);
if (!string.IsNullOrEmpty(transcribedText) &&
(transcribedText.EndsWith(".") || transcribedText.EndsWith("!") || transcribedText.EndsWith("?")))
transcribedText = transcribedText[..^1];
try { System.IO.File.Delete(filePath); } catch { /* ignore cleanup errors */ }
return transcribedText;
}
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. Extract product names and quantities from ANY produce item
2. Normalize product names to singular, lowercase (e.g., 'narancsok' → 'narancs')
3. Handle Hungarian number words ('száz' = 100, 'ötven' = 50, 'húsz' = 20, 'tíz' = 10, 'öt' = 5, 'egy' = 1)
4. Fix common transcription/typing errors (e.g., 'datója' → 'datolya', 'szűlő' → 'szőlő', 'mondarin' → 'mandarin')
5. Return ONLY valid JSON array, no explanations
6. DO NOT include units - only product name and quantity as a number
7. ALWAYS return at least one product if you can parse anything from the input
OUTPUT FORMAT (JSON only):
[
{""product"": ""narancs"", ""quantity"": 100},
{""product"": ""alma"", ""quantity"": 50}
]";
var aiResponse = await _cerebrasApiService.GetSimpleResponseAsync(systemPrompt, $"Parse this: {text}");
Console.WriteLine($"[QuickOrder] AI parse response: {aiResponse}");
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 (Exception ex)
{
Console.WriteLine($"[QuickOrder] JSON parse error: {ex.Message}");
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 enrichedProducts = new List<object>();
var allProductDtos = await _dbContext.ProductDtos.GetAll(true).ToListAsync();
foreach (var parsed in parsedProducts)
{
var dbProducts = await _productService.SearchProductsAsync(
keywords: parsed.Product,
pageIndex: 0,
pageSize: 20);
if (!dbProducts.Any())
{
Console.WriteLine($"[QuickOrder] No products found for: {parsed.Product}");
continue;
}
foreach (var product in dbProducts)
{
var productDto = allProductDtos.FirstOrDefault(x => x.Id == product.Id);
if (productDto == null) continue;
var availableQty = product.StockQuantity + productDto.IncomingQuantity;
if (availableQty <= 0) continue;
var requestedQty = parsed.Quantity;
var finalQty = Math.Min(requestedQty, availableQty);
var isReduced = finalQty < requestedQty;
decimal? unitPrice = null;
if (!productDto.IsMeasurable)
{
var priceResult = await _customPriceCalculationService.GetFinalPriceAsync(
product, customer, store, null, 0, true, finalQty, null, null);
unitPrice = priceResult.finalPrice;
}
enrichedProducts.Add(new
{
id = product.Id,
name = product.Name,
quantity = finalQty,
requestedQuantity = requestedQty,
unitPrice,
stockQuantity = availableQty,
searchTerm = parsed.Product,
isQuantityReduced = isReduced,
isMeasurable = productDto.IsMeasurable
});
}
}
Console.WriteLine($"[QuickOrder] Enriched product count: {enrichedProducts.Count}");
return enrichedProducts;
}
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 allProductDtos = 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 productDto = allProductDtos.FirstOrDefault(x => x.Id == product.Id);
var isMeasurable = productDto?.IsMeasurable ?? false;
decimal? unitPrice = null;
if (!isMeasurable)
{
var priceResult = await _customPriceCalculationService.GetFinalPriceAsync(
product, customer, store, null, 0, true, item.Quantity, null, null);
unitPrice = priceResult.finalPrice;
}
result.Add(new
{
id = item.Id,
productId = item.ProductId,
name = product.Name,
quantity = item.Quantity,
unitPrice,
isMeasurable
});
}
return result;
}
#endregion
#region Inner models
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; }
}
#endregion
}
}