| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using Microsoft.Extensions.Options; |
| | | 3 | | using OzonDomains.Models; |
| | | 4 | | using OzonOrdersWeb.Observability; |
| | | 5 | | using OzonOrdersWeb.WebServices.SystemAlerts; |
| | | 6 | | using OzonRepositories.Context; |
| | | 7 | | |
| | | 8 | | namespace OzonOrdersWeb.WebServices.Orders; |
| | | 9 | | |
| | | 10 | | public interface IOrderProblemInfoService |
| | | 11 | | { |
| | | 12 | | /// <summary> |
| | | 13 | | /// Собирает проблемы заказов страницы для значка и строки в сетках: активные алерты (из памяти), |
| | | 14 | | /// непроведённые документы 1С и упавшие транзакции (по одному запросу на страницу). |
| | | 15 | | /// </summary> |
| | | 16 | | Task PopulateForOrdersAsync(IReadOnlyList<Order> ordersForPage, CancellationToken cancellationToken = default); |
| | | 17 | | } |
| | | 18 | | |
| | 5 | 19 | | public sealed class OrderProblemInfoService( |
| | 5 | 20 | | ApplicationDbContext context, |
| | 5 | 21 | | SystemAlertOrderIndex alertIndex, |
| | 5 | 22 | | SystemAlertIncidentStore alertStore, |
| | 5 | 23 | | SensitiveDataRedactor redactor, |
| | 5 | 24 | | IOptions<SystemAlertsOptions> options) : IOrderProblemInfoService |
| | | 25 | | { |
| | | 26 | | private const int MaxProblemsPerOrder = 5; |
| | | 27 | | private const string FailedStatus = "Failed"; |
| | | 28 | | private const string SuccessStatus = "Success"; |
| | | 29 | | |
| | | 30 | | public async Task PopulateForOrdersAsync(IReadOnlyList<Order> ordersForPage, CancellationToken cancellationToken = d |
| | | 31 | | { |
| | 5 | 32 | | if (ordersForPage.Count == 0) return; |
| | | 33 | | |
| | 5 | 34 | | var settings = options.Value; |
| | 15 | 35 | | var orderIds = ordersForPage.Select(o => o.Id).Distinct().ToList(); |
| | 5 | 36 | | var since = DateTime.UtcNow.AddHours(-Math.Max(1, settings.OrderProblemWindowHours)); |
| | 5 | 37 | | var problems = new Dictionary<int, List<OrderProblem>>(); |
| | | 38 | | |
| | 5 | 39 | | await alertStore.EnsureOrderIndexAsync(cancellationToken); |
| | 30 | 40 | | foreach (var id in orderIds) |
| | | 41 | | { |
| | 30 | 42 | | foreach (var entry in alertIndex.Get(id)) |
| | | 43 | | { |
| | | 44 | | // Непроведённые документы 1С показываем из первоисточника ниже: там учтено, что позже заказ мог пройти. |
| | 5 | 45 | | if (entry.Key == "onec-posting") continue; |
| | 4 | 46 | | Add(problems, id, entry.Severity, OrderProblemKind.Alert, string.IsNullOrWhiteSpace(entry.Reason) ? entr |
| | | 47 | | } |
| | | 48 | | } |
| | | 49 | | |
| | | 50 | | // Ошибка проведения неактуальна, если позже по тому же заказу проведение прошло успешно. |
| | 5 | 51 | | var failedPostings = await context.OrderHistoryOneCPostings.AsNoTracking() |
| | 5 | 52 | | .Where(x => orderIds.Contains(x.OrderId) && !x.IsPosted && !string.IsNullOrEmpty(x.ErrorSummary) && x.Posted |
| | 5 | 53 | | .Where(f => !context.OrderHistoryOneCPostings.Any(s => s.OrderId == f.OrderId && s.IsPosted && s.PostedAt > |
| | 5 | 54 | | .OrderByDescending(x => x.PostedAt) |
| | 5 | 55 | | .Select(x => new { x.OrderId, x.ErrorSummary }) |
| | 5 | 56 | | .ToListAsync(cancellationToken); |
| | 18 | 57 | | foreach (var posting in failedPostings.GroupBy(x => x.OrderId).Select(g => g.First())) |
| | 2 | 58 | | Add(problems, posting.OrderId, "danger", OrderProblemKind.OneC, SystemAlertOrderRef.OneCReason(posting.Error |
| | | 59 | | |
| | | 60 | | // Транзакция неактуальна после более позднего успеха: той же автоматизации или ручного проведения в 1С. |
| | 5 | 61 | | var failedTransactions = await context.BackgroundTransactionResults.AsNoTracking() |
| | 5 | 62 | | .Where(x => orderIds.Contains(x.OrderId) && x.Status == FailedStatus && x.CreatedAt >= since) |
| | 5 | 63 | | .Where(f => !context.BackgroundTransactionResults.Any(s => s.OrderId == f.OrderId && s.CreatedAt > f.Created |
| | 5 | 64 | | && (s.Status == SuccessStatus || s.IsPosted)) |
| | 5 | 65 | | && !context.OrderHistoryOneCPostings.Any(p => p.OrderId == f.OrderId && p.IsPosted && p.PostedAt |
| | 5 | 66 | | .OrderByDescending(x => x.CreatedAt) |
| | 5 | 67 | | .Select(x => new { x.OrderId, x.ErrorMessage }) |
| | 5 | 68 | | .ToListAsync(cancellationToken); |
| | 14 | 69 | | foreach (var transaction in failedTransactions.GroupBy(x => x.OrderId).Select(g => g.First())) |
| | 1 | 70 | | Add(problems, transaction.OrderId, "danger", OrderProblemKind.Transaction, |
| | 1 | 71 | | "Транзакция не выполнена" + (string.IsNullOrWhiteSpace(transaction.ErrorMessage) ? string.Empty : " — " |
| | | 72 | | |
| | 30 | 73 | | foreach (var order in ordersForPage) |
| | | 74 | | { |
| | 10 | 75 | | if (!problems.TryGetValue(order.Id, out var list)) continue; |
| | 5 | 76 | | order.Problems = list |
| | 7 | 77 | | .OrderBy(x => OrderProblem.SeverityRank(x.Severity)) |
| | 5 | 78 | | .Take(MaxProblemsPerOrder) |
| | 7 | 79 | | .Select(x => x with { Text = SystemAlertOrderRef.Trim(redactor.Redact(x.Text, SystemAlertOrderRef.MaxRea |
| | 5 | 80 | | .ToList(); |
| | | 81 | | } |
| | 5 | 82 | | } |
| | | 83 | | |
| | | 84 | | private static void Add(Dictionary<int, List<OrderProblem>> map, int orderId, string severity, string kind, string t |
| | | 85 | | { |
| | 12 | 86 | | if (!map.TryGetValue(orderId, out var list)) map[orderId] = list = []; |
| | 9 | 87 | | if (list.Any(x => x.Text == text)) return; |
| | 7 | 88 | | list.Add(new OrderProblem(OrderProblem.NormalizeSeverity(severity), kind, text)); |
| | 7 | 89 | | } |
| | | 90 | | } |