Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/Services/OrderDraftService.cs

393 lines
16 KiB
C#

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<List<OrderDraft>> 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<OrderDraft>();
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<OrderDraftItem> 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<int> 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<OrderDraft?> GetByIdAsync(int id, bool loadRelations = true)
=> _dbContext.OrderDrafts.GetByIdAsync(id, loadRelations);
public async Task<List<OrderDraft>> 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<OrderDraft> 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<OrderDraftApproveResult> 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
});
}
}