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 Index() { var customer = await _workContext.GetCurrentCustomerAsync(); if (await _customerService.IsGuestAsync(customer)) return Challenge(); return View("~/Plugins/Misc.FruitBankPlugin/Views/Order/Index.cshtml"); } // ── FLOW TYPE ───────────────────────────────────────────────────────────── /// /// 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. /// 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 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( 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 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( 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 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(); 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 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() }); var productDtos = await _dbContext.ProductDtos .GetAll(true) .Where(p => availableIds.Contains(p.Id)) .ToListAsync(); var result = new List(); 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 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 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 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 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 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(); 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( 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 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> 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(); try { return System.Text.Json.JsonSerializer.Deserialize>(jsonMatch.Value) ?? new List(); } catch { return new List(); } } private async Task> EnrichProductData( List parsedProducts, Nop.Core.Domain.Customers.Customer customer, Nop.Core.Domain.Stores.Store store) { var enriched = new List(); 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> 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(); 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 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; } } }