393 lines
16 KiB
C#
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
|
|
});
|
|
}
|
|
}
|