| | | 1 | | using System.Text; |
| | | 2 | | using Microsoft.EntityFrameworkCore; |
| | | 3 | | using OzonDomains; |
| | | 4 | | using OzonDomains.Models; |
| | | 5 | | using OzonRepositories.Data; |
| | | 6 | | using Services.BitrixStockServices; |
| | | 7 | | using Services.DataServices; |
| | | 8 | | |
| | | 9 | | namespace Services.AiAssistant; |
| | | 10 | | |
| | | 11 | | /// <summary> |
| | | 12 | | /// Те же данные, что на странице OrderHistory/GetOrderHistory — для AI-контекста. |
| | | 13 | | /// </summary> |
| | | 14 | | public sealed class OrderHistoryPageContextLoader |
| | | 15 | | { |
| | | 16 | | private readonly IOrderHistoryDataService _historyService; |
| | | 17 | | private readonly OrdersDataServices _ordersData; |
| | | 18 | | private readonly TransactionDataServices _transactionData; |
| | | 19 | | private readonly OrderRepository _orderRepository; |
| | | 20 | | private readonly AiOrderScopeApplier _scopeApplier; |
| | | 21 | | private readonly IBitrixStockSyncArticleHistoryService _bitrixArticleHistory; |
| | | 22 | | |
| | 0 | 23 | | public OrderHistoryPageContextLoader( |
| | 0 | 24 | | IOrderHistoryDataService historyService, |
| | 0 | 25 | | OrdersDataServices ordersData, |
| | 0 | 26 | | TransactionDataServices transactionData, |
| | 0 | 27 | | OrderRepository orderRepository, |
| | 0 | 28 | | AiOrderScopeApplier scopeApplier, |
| | 0 | 29 | | IBitrixStockSyncArticleHistoryService bitrixArticleHistory) |
| | | 30 | | { |
| | 0 | 31 | | _historyService = historyService; |
| | 0 | 32 | | _ordersData = ordersData; |
| | 0 | 33 | | _transactionData = transactionData; |
| | 0 | 34 | | _orderRepository = orderRepository; |
| | 0 | 35 | | _scopeApplier = scopeApplier; |
| | 0 | 36 | | _bitrixArticleHistory = bitrixArticleHistory; |
| | 0 | 37 | | } |
| | | 38 | | |
| | | 39 | | public async Task<bool> IsOrderAccessibleAsync(int orderId, string? userId, CancellationToken ct = default) |
| | | 40 | | { |
| | 0 | 41 | | if (orderId <= 0) return false; |
| | 0 | 42 | | var query = _orderRepository.GetQueryable(includeArchived: true).Where(o => o.Id == orderId); |
| | 0 | 43 | | query = await _scopeApplier.ApplyAsync(query, userId, ct); |
| | 0 | 44 | | return await query.AnyAsync(ct); |
| | 0 | 45 | | } |
| | | 46 | | |
| | | 47 | | public async Task<string?> LoadFormattedContextAsync(int orderId, string? userId, CancellationToken ct = default) |
| | | 48 | | { |
| | 0 | 49 | | if (!await IsOrderAccessibleAsync(orderId, userId, ct)) |
| | 0 | 50 | | return null; |
| | | 51 | | |
| | 0 | 52 | | var histories = await _historyService.GetByOrderIdAsync(orderId) ?? []; |
| | | 53 | | |
| | 0 | 54 | | var order = histories.FirstOrDefault()?.Order; |
| | 0 | 55 | | if (order == null) |
| | | 56 | | { |
| | 0 | 57 | | order = await _ordersData.GetAsyncNoTracking(orderId).ConfigureAwait(false); |
| | 0 | 58 | | if (order == null) |
| | 0 | 59 | | return null; |
| | | 60 | | } |
| | | 61 | | |
| | 0 | 62 | | var transactionIds = AiOrderTransactionIdCollector.Collect(order, histories).ToList(); |
| | 0 | 63 | | var transactions = await _transactionData.GetTransactionsByIdsAsync(transactionIds); |
| | | 64 | | |
| | 0 | 65 | | var sb = new StringBuilder(FormatPageContext(order, histories, transactions ?? [])); |
| | | 66 | | |
| | 0 | 67 | | if (!string.IsNullOrWhiteSpace(order.Article)) |
| | | 68 | | { |
| | 0 | 69 | | var bitrixPage = await _bitrixArticleHistory.GetAllAsync( |
| | 0 | 70 | | order.Article.Trim(), maxReports: 15, ct); |
| | 0 | 71 | | sb.AppendLine(); |
| | 0 | 72 | | sb.AppendLine(BitrixStockArticleHistoryAiFormatter.FormatForAiAnalysis(bitrixPage)); |
| | | 73 | | } |
| | | 74 | | |
| | 0 | 75 | | return sb.ToString().TrimEnd(); |
| | 0 | 76 | | } |
| | | 77 | | |
| | | 78 | | private static string FormatPageContext( |
| | | 79 | | Order order, |
| | | 80 | | List<OrderHistory> histories, |
| | | 81 | | List<Transaction> transactions) |
| | | 82 | | { |
| | 0 | 83 | | var sb = new StringBuilder(); |
| | 0 | 84 | | sb.AppendLine($"=== OrderHistory/GetOrderHistory — заказ #{order.Id} ==="); |
| | 0 | 85 | | OrderRussianFieldFormatter.AppendKeyPartiesBlock(sb, order); |
| | 0 | 86 | | sb.AppendLine($"Номер отправления: {order.ShipmentNumber}"); |
| | 0 | 87 | | sb.AppendLine($"Товар: {order.ProductName}"); |
| | 0 | 88 | | sb.AppendLine($"Артикул: {order.Article}"); |
| | | 89 | | |
| | 0 | 90 | | var txSummaries = transactions.Select(t => new TransactionSummary |
| | 0 | 91 | | { |
| | 0 | 92 | | Id = t.Id, |
| | 0 | 93 | | TypeName = t.Type?.GetDisplayName() ?? "—", |
| | 0 | 94 | | CreatedAt = t.CreatedDateTime, |
| | 0 | 95 | | CreatedBy = t.CreateBy, |
| | 0 | 96 | | Comment = t.Comment |
| | 0 | 97 | | }).ToList(); |
| | 0 | 98 | | OrderRussianFieldFormatter.AppendTransactionsBlock(sb, txSummaries); |
| | | 99 | | |
| | 0 | 100 | | if (histories.Count == 0) |
| | | 101 | | { |
| | 0 | 102 | | sb.AppendLine("История изменений (orders_history): записей нет."); |
| | | 103 | | } |
| | | 104 | | else |
| | | 105 | | { |
| | 0 | 106 | | sb.AppendLine($"История изменений ({histories.Count} записей), по дням:"); |
| | 0 | 107 | | foreach (var dayGroup in histories |
| | 0 | 108 | | .GroupBy(h => h.ChangedAt.Date) |
| | 0 | 109 | | .OrderByDescending(g => g.Key)) |
| | | 110 | | { |
| | 0 | 111 | | sb.AppendLine($"--- {dayGroup.Key:dd.MM.yyyy} ---"); |
| | 0 | 112 | | foreach (var h in dayGroup.OrderByDescending(x => x.ChangedAt)) |
| | | 113 | | { |
| | 0 | 114 | | var field = !string.IsNullOrWhiteSpace(h.ColumnDisplayName) |
| | 0 | 115 | | ? h.ColumnDisplayName |
| | 0 | 116 | | : h.ColumnName ?? "поле"; |
| | 0 | 117 | | sb.AppendLine( |
| | 0 | 118 | | $" {h.ChangedAt:HH:mm:ss} | {h.ChangedBy ?? "—"} | {field}: " + |
| | 0 | 119 | | $"«{h.FormattedOldValue}» → «{h.FormattedNewValue}»"); |
| | | 120 | | } |
| | | 121 | | } |
| | | 122 | | } |
| | | 123 | | |
| | 0 | 124 | | return sb.ToString().TrimEnd(); |
| | | 125 | | } |
| | | 126 | | } |