using FruitBank.Common.Entities; using FruitBank.Common.Enums; using LinqToDB; using Nop.Core.Domain.Catalog; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Services.Catalog; using System.Threading; namespace Nop.Plugin.Misc.FruitBankPlugin.Services; public class OrderDraftService : IOrderDraftService { private readonly FruitBankDbContext _dbContext; private readonly PreOrderDbContext _preOrderDbContext; private readonly OrderDraftParserService _parserService; private readonly IProductService _productService; private readonly PreOrderConversionService _preOrderConversionService; public OrderDraftService( FruitBankDbContext dbContext, PreOrderDbContext preOrderDbContext, OrderDraftParserService parserService, IProductService productService, PreOrderConversionService preOrderConversionService) { _dbContext = dbContext; _preOrderDbContext = preOrderDbContext; _parserService = parserService; _productService = productService; _preOrderConversionService = preOrderConversionService; } // ── Létrehozás ──────────────────────────────────────────────────────────── public async Task> CreateDraftsFromMessageAsync(string externalUsername, string rawMessageText) { // 1. Partner azonosítás var mapping = await _dbContext.ExternalUserPartnerMaps.GetByUsernameAsync(externalUsername); // 2. AI parse: dátumok + termékek kinyerése var parsedGroups = await _parserService.ParseMessageAsync(rawMessageText); if (!parsedGroups.Any()) return []; var now = DateTime.UtcNow; var createdDrafts = new List(); foreach (var group in parsedGroups) { var deliveryDate = group.GetDeliveryDateTime(); var isPreorder = deliveryDate.Date > now.Date.AddDays(OrderDraftParserService.PreorderThresholdDays); var draft = new OrderDraft { ExternalUsername = externalUsername, CustomerId = mapping?.CustomerId, RawMessageText = rawMessageText, ParsedDeliveryDate = deliveryDate, Status = OrderDraftStatus.Pending, CreatedOnUtc = now }; await _dbContext.OrderDrafts.InsertAsync(draft); // 3. Tételek feldolgozása var sortOrder = 0; var itemTasks = group.Items.Select(parsedItem => ProcessDraftItemAsync( draft.Id, parsedItem, isPreorder, sortOrder++)).ToList(); var items = await Task.WhenAll(itemTasks); draft.Items = items.ToList(); createdDrafts.Add(draft); Console.WriteLine($"[OrderDraftService] Created draft #{draft.Id} for '{externalUsername}', " + $"deliveryDate={deliveryDate:yyyy-MM-dd}, isPreorder={isPreorder}, " + $"items={items.Length}, customerId={mapping?.CustomerId.ToString() ?? "null"}"); } return createdDrafts; } private async Task ProcessDraftItemAsync( int draftId, ParsedDraftItem parsed, bool isPreorder, int sortOrder) { // 1. Historikus súlyozás: utolsó 5 admin döntés ugyanerre a szövegre var historicMappings = await _dbContext.ProductTextMappings .GetRecentByRawText(parsed.RawProductText, isPreorder, 5) .ToListAsync(); var historicProductIds = historicMappings.Select(m => m.ResolvedProductId).Distinct().ToList(); // 2. AI: kanonikus név + kulcsszavak var hint = await _parserService.ResolveProductNameAsync(parsed.RawProductText); // 3. Termékkeresés az összes kulcsszóra — párhuzamosan var searchTasks = hint.SearchKeywords .Select(kw => _productService.SearchProductsAsync(keywords: kw, visibleIndividuallyOnly: true, pageSize: 10)) .ToList(); var searchResults = await Task.WhenAll(searchTasks); var allFoundProducts = searchResults .SelectMany(r => r) .DistinctBy(p => p.Id) .ToList(); // 4. Sorrendezés: historikus találatok elől, majd AI ranking List rankedIds; if (allFoundProducts.Count == 0) { rankedIds = historicProductIds; } else { // Historikus ID-k kerülnek előre ha az aktuális keresésben is szerepelnek var foundIds = allFoundProducts.Select(p => p.Id).ToList(); var historicAndFound = historicProductIds.Where(id => foundIds.Contains(id)).ToList(); var restFound = foundIds.Except(historicAndFound).ToList(); // Ha több mint 1 nem-historikus találat van, AI rangsorolja if (restFound.Count > 1) { var candidates = allFoundProducts .Where(p => restFound.Contains(p.Id)) .Select(p => (p.Id, p.Name)) .ToList(); var aiRanked = await _parserService.RankCandidatesAsync( parsed.RawProductText, hint.CanonicalName, candidates); rankedIds = [.. historicAndFound, .. aiRanked]; } else { rankedIds = [.. historicAndFound, .. restFound]; } } var item = new OrderDraftItem { DraftId = draftId, RawProductText = parsed.RawProductText, AiResolvedProductName = hint.CanonicalName, SuggestedProductIdsJson = System.Text.Json.JsonSerializer.Serialize(rankedIds), SelectedProductId = rankedIds.Count > 0 ? rankedIds[0] : null, // első javaslat előre kiválasztva RequestedQuantity = parsed.RequestedQuantity > 0 ? parsed.RequestedQuantity : 1, IsPreorder = isPreorder, SortOrder = sortOrder }; await _dbContext.OrderDraftItems.InsertAsync(item); return item; } // ── Lekérdezések ────────────────────────────────────────────────────────── public Task GetByIdAsync(int id, bool loadRelations = true) => _dbContext.OrderDrafts.GetByIdAsync(id, loadRelations); public async Task> GetPendingAsync(int? customerId = null) { var query = _dbContext.OrderDrafts.GetPending(); if (customerId.HasValue) query = query.Where(d => d.CustomerId == customerId.Value); return await query.OrderByDescending(d => d.CreatedOnUtc).ToListAsync(); } public async Task<(List Items, int TotalCount)> GetPagedAsync( int pageIndex, int pageSize, OrderDraftStatus? status = null, int? customerId = null) { var query = _dbContext.OrderDrafts.GetAll(false); if (status.HasValue) query = query.Where(d => d.Status == status.Value); if (customerId.HasValue) query = query.Where(d => d.CustomerId == customerId.Value); query = query.OrderByDescending(d => d.CreatedOnUtc); var totalCount = await query.CountAsync(CancellationToken.None); var items = await query .Skip(pageIndex * pageSize) .Take(pageSize) .ToListAsync(); return (items, totalCount); } // ── Admin műveletek ─────────────────────────────────────────────────────── public async Task ApproveAsync(int draftId, int adminId) { var result = new OrderDraftApproveResult(); var draft = await GetByIdAsync(draftId, true); if (draft == null) { result.ErrorMessage = $"Draft #{draftId} nem található."; return result; } if (draft.Status != OrderDraftStatus.Pending) { result.ErrorMessage = $"Draft #{draftId} már le van zárva (státusz: {draft.Status})."; return result; } if (!draft.CustomerId.HasValue) { result.ErrorMessage = "Jóváhagyás előtt rendelj hozzá partnert a drafthoz."; return result; } var unresolved = draft.Items.Where(i => i.SelectedProductId == null).ToList(); if (unresolved.Any()) { result.ErrorMessage = $"{unresolved.Count} tétel nincs kitöltve. Minden sorban ki kell választani a terméket."; return result; } try { // Tételek szétválasztása: preorder vs. azonnali var preorderItems = draft.Items.Where(i => i.IsPreorder).ToList(); var orderItems = draft.Items.Where(i => !i.IsPreorder).ToList(); // Tanulási rekordok mentése (párhuzamosan) var mappingTasks = draft.Items.Select(item => SaveProductTextMappingAsync(item, adminId)); await Task.WhenAll(mappingTasks); // Előrendelések létrehozása if (preorderItems.Any()) { var preorderNopItems = preorderItems.Select(i => new PreOrderItem { ProductId = i.SelectedProductId!.Value, RequestedQuantity = i.RequestedQuantity, FulfilledQuantity = 0, UnitPriceInclTax = 0m, // Árat a konverziókor számítja a service Status = PreOrderItemStatus.Pending }).ToList(); var preorder = new PreOrder { CustomerId = draft.CustomerId!.Value, StoreId = 1, DateOfReceipt = draft.ParsedDeliveryDate, CustomerNote = $"OrderDraft #{draft.Id} alapján létrehozva" }; var saved = await _preOrderDbContext.InsertPreOrderAsync(preorder, preorderNopItems); result.PreOrderIds.Add(saved.Id); // Azonnali konverzió ha van raktáron await _preOrderConversionService.ConvertPreOrdersForProductsAsync( preorderNopItems.Select(i => i.ProductId).ToList(), 0); } // Azonnali rendelések: a PreOrderConversionService kezeli ha létrehozunk egy // preordert aminek dátuma <= 4 nap — itt is ugyanazt a flow-t használjuk if (orderItems.Any()) { var orderNopItems = orderItems.Select(i => new PreOrderItem { ProductId = i.SelectedProductId!.Value, RequestedQuantity = i.RequestedQuantity, FulfilledQuantity = 0, UnitPriceInclTax = 0m, Status = PreOrderItemStatus.Pending }).ToList(); var immediatePreorder = new PreOrder { CustomerId = draft.CustomerId!.Value, StoreId = 1, DateOfReceipt = draft.ParsedDeliveryDate, CustomerNote = $"OrderDraft #{draft.Id} alapján létrehozva (azonnali)" }; var saved = await _preOrderDbContext.InsertPreOrderAsync(immediatePreorder, orderNopItems); // Az azonnali rendeléseknél inline futtatjuk a konverziót await _preOrderConversionService.ConvertPreOrdersForProductsAsync( orderNopItems.Select(i => i.ProductId).ToList(), 0); // Ha lett belőle rendelés, azt is visszaadjuk var converted = await _preOrderDbContext.PreOrders.GetByIdAsync(saved.Id); if (converted?.OrderId.HasValue == true) result.OrderIds.Add(converted.OrderId.Value); else result.PreOrderIds.Add(saved.Id); } // Draft lezárása draft.Status = OrderDraftStatus.Approved; draft.ResolvedByAdminId = adminId; draft.ResolvedOnUtc = DateTime.UtcNow; await _dbContext.OrderDrafts.UpdateAsync(draft); result.Success = true; Console.WriteLine($"[OrderDraftService] Draft #{draftId} approved by admin #{adminId}. " + $"PreOrders: [{string.Join(",", result.PreOrderIds)}], " + $"Orders: [{string.Join(",", result.OrderIds)}]"); } catch (Exception ex) { result.ErrorMessage = $"Hiba a jóváhagyás során: {ex.Message}"; Console.Error.WriteLine($"[OrderDraftService] ApproveAsync error for draft #{draftId}: {ex}"); } return result; } public async Task RejectAsync(int draftId, int adminId, string? adminNote = null) { var draft = await GetByIdAsync(draftId, false); if (draft == null || draft.Status != OrderDraftStatus.Pending) return; draft.Status = OrderDraftStatus.Rejected; draft.ResolvedByAdminId = adminId; draft.ResolvedOnUtc = DateTime.UtcNow; draft.AdminNote = adminNote; await _dbContext.OrderDrafts.UpdateAsync(draft); Console.WriteLine($"[OrderDraftService] Draft #{draftId} rejected by admin #{adminId}."); } public async Task AssignCustomerAsync(int draftId, int customerId, int adminId) { var draft = await GetByIdAsync(draftId, false); if (draft == null) return; // Mapping mentése a jövőre var existingMapping = await _dbContext.ExternalUserPartnerMaps.GetByUsernameAsync(draft.ExternalUsername); if (existingMapping == null || existingMapping.CustomerId != customerId) { await _dbContext.ExternalUserPartnerMaps.InsertAsync(new ExternalUserPartnerMapping { ExternalUsername = draft.ExternalUsername, CustomerId = customerId, CreatedByAdminId = adminId, CreatedOnUtc = DateTime.UtcNow }); } draft.CustomerId = customerId; await _dbContext.OrderDrafts.UpdateAsync(draft); } public async Task UpdateDraftItemAsync(int draftItemId, int selectedProductId, int quantity) { var item = await _dbContext.OrderDraftItems.GetByIdAsync(draftItemId); if (item == null) return; item.SelectedProductId = selectedProductId; if (quantity > 0) item.RequestedQuantity = quantity; await _dbContext.OrderDraftItems.UpdateAsync(item); } public async Task ExpireOldDraftsAsync() { var candidates = await _dbContext.OrderDrafts.GetExpiredCandidates().ToListAsync(); foreach (var draft in candidates) { draft.Status = OrderDraftStatus.Expired; await _dbContext.OrderDrafts.UpdateAsync(draft); } if (candidates.Any()) Console.WriteLine($"[OrderDraftService] Expired {candidates.Count} old drafts."); } // ── Privát segédek ──────────────────────────────────────────────────────── private async Task SaveProductTextMappingAsync(OrderDraftItem item, int adminId) { if (item.SelectedProductId == null) return; await _dbContext.ProductTextMappings.InsertAsync(new ProductTextMapping { RawText = item.RawProductText.ToLower().Trim(), ResolvedProductId = item.SelectedProductId.Value, IsPreorder = item.IsPreorder, SelectedByAdminId = adminId, CreatedOnUtc = DateTime.UtcNow }); } }