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 Index() { var customer = await _workContext.GetCurrentCustomerAsync(); if (await _customerService.IsGuestAsync(customer)) return Challenge(); return View("~/Plugins/Misc.FruitBankPlugin/Views/QuickOrder/Index.cshtml"); } /// /// Return all available products with prices (for initial page load) /// [HttpGet] public async Task 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(); 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}" }); } } /// /// Parse a manually typed product list and return matching products with prices /// [HttpPost] public async Task 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}" }); } } /// /// Transcribe voice audio (Hungarian) then parse and match products /// [HttpPost] public async Task 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}" }); } } /// /// Add a product to the current customer's shopping cart and return the updated cart /// [HttpPost] public async Task 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}" }); } } /// /// Return the current customer's cart as JSON (for cart panel refresh) /// [HttpGet] public async Task 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 /// Shorthand: get a localized QuickOrder resource string private Task L(string keySuffix) => _localizationService.GetResourceAsync(Prefix + keySuffix); private async Task 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> 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(); try { return System.Text.Json.JsonSerializer.Deserialize>(jsonMatch.Value) ?? new List(); } catch (Exception ex) { Console.WriteLine($"[QuickOrder] JSON parse error: {ex.Message}"); return new List(); } } private async Task> EnrichProductData( List parsedProducts, Nop.Core.Domain.Customers.Customer customer, Nop.Core.Domain.Stores.Store store) { var enrichedProducts = new List(); 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> 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(); 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 } }