< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.OrdersClientStatusSyncService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/OrdersClientStatusSyncService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 155
Coverable lines: 155
Total lines: 275
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 60
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
SyncOrderStatusesAsync()0%2550500%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/OrdersClientStatusSyncService.cs

#LineLine coverage
 1using System.Diagnostics;
 2using System.Text;
 3using Microsoft.EntityFrameworkCore;
 4using OzonDomains;
 5using OzonDomains.Models;
 6using OzonRepositories.Context;
 7using Services.ApiServices.OzonApi;
 8using Services.DataServices;
 9using Services.LabelServices;
 10using Services.OrderControlServices;
 11using Services.ParserServices.HelpDictEnum;
 12using Services.OzonWebhookServices;
 13
 14namespace OzonOrdersWeb.WebServices.Orders;
 15
 16public sealed class OrdersClientStatusSyncResult
 17{
 18    public string? Message { get; init; }
 19
 20    public string? Error { get; init; }
 21
 22    /// <summary>
 23    /// Заказы, для которых запрос статуса к Ozon не удался в этом проходе (таймаут, ошибка API и т.п.).
 24    /// Их локальный ClientStatus остался прежним — вызывающий код (SLA-очередь) не должен принимать
 25    /// по ним решения в этом цикле, как будто статус свежий.
 26    /// </summary>
 27    public IReadOnlySet<int> FailedOrderIds { get; init; } = new HashSet<int>();
 28}
 29
 30public interface IOrdersClientStatusSyncService
 31{
 32    Task<OrdersClientStatusSyncResult> SyncOrderStatusesAsync(
 33        IReadOnlyList<int> orderIds,
 34        CancellationToken cancellationToken = default);
 35}
 36
 37/// <summary>
 38/// Синхронизация ClientStatus заказов с Ozon API (логика бывшего ProcessOrdersStatusUpdate).
 39/// </summary>
 40public sealed class OrdersClientStatusSyncService : IOrdersClientStatusSyncService
 41{
 42    private readonly ApplicationDbContext _context;
 43    private readonly OrdersDataServices _orderServices;
 44    private readonly OzonClientServices _ozonClientServices;
 45    private readonly OzonJsonDataBuilder _jsonDataBuilder;
 46    private readonly ClientStatusDataServices _clientStatusServices;
 47    private readonly IOrderDeliveryStatusSynchronizer _deliveryStatusSynchronizer;
 48    private readonly IOzonPostingCancellationService _postingCancellationService;
 49
 050    public OrdersClientStatusSyncService(
 051        ApplicationDbContext context,
 052        OrdersDataServices orderServices,
 053        OzonClientServices ozonClientServices,
 054        OzonJsonDataBuilder jsonDataBuilder,
 055        ClientStatusDataServices clientStatusServices,
 056        IOrderDeliveryStatusSynchronizer deliveryStatusSynchronizer,
 057        IOzonPostingCancellationService postingCancellationService)
 58    {
 059        _context = context;
 060        _orderServices = orderServices;
 061        _ozonClientServices = ozonClientServices;
 062        _jsonDataBuilder = jsonDataBuilder;
 063        _clientStatusServices = clientStatusServices;
 064        _deliveryStatusSynchronizer = deliveryStatusSynchronizer;
 065        _postingCancellationService = postingCancellationService;
 066    }
 67
 68    public async Task<OrdersClientStatusSyncResult> SyncOrderStatusesAsync(
 69        IReadOnlyList<int> orderIds,
 70        CancellationToken cancellationToken = default)
 71    {
 072        if (orderIds.Count == 0)
 73        {
 074            return new OrdersClientStatusSyncResult { Error = "Нет доступных заказов для обновления" };
 75        }
 76
 077        var idArray = orderIds.Distinct().ToArray();
 078        var totalStopwatch = Stopwatch.StartNew();
 079        var dbOrdersStopwatch = Stopwatch.StartNew();
 80
 81        try
 82        {
 083            var ordersLight = await _context.Orders
 084                .AsNoTracking()
 085                .Where(o => idArray.Contains(o.Id))
 086                .Select(o => new
 087                {
 088                    o.Id,
 089                    o.ShipmentNumber,
 090                    o.OzonClientId,
 091                    o.ClientStatusId,
 092                    ClientStatusName = o.ClientStatus != null ? o.ClientStatus.Name : null
 093                })
 094                .ToListAsync(cancellationToken);
 95
 096            dbOrdersStopwatch.Stop();
 097            var dbOrdersTime = dbOrdersStopwatch.Elapsed;
 98
 099            if (ordersLight.Count == 0)
 100            {
 0101                return new OrdersClientStatusSyncResult { Error = "Нет доступных заказов для обновления" };
 102            }
 103
 104            // Статус на момент чтения — условие для UPDATE: если за время опроса пришёл свежий пуш, не перезаписываем.
 0105            var expectedStatusIds = ordersLight.ToDictionary(o => o.Id, o => o.ClientStatusId);
 106
 0107            var ordersToUpdate = ordersLight
 0108                .Select(o => new Order
 0109                {
 0110                    Id = o.Id,
 0111                    ShipmentNumber = o.ShipmentNumber,
 0112                    OzonClientId = o.OzonClientId,
 0113                    ClientStatus = string.IsNullOrEmpty(o.ClientStatusName)
 0114                        ? null
 0115                        : new ClientStatus { Name = o.ClientStatusName }
 0116                })
 0117                .ToList();
 118
 0119            int updatingCount = 0;
 0120            var updateInfoBuilder = new StringBuilder("");
 0121            var updatedOrderIds = new List<int>();
 0122            var failedOrderIds = new List<int>();
 123
 0124            var clientsLoadStopwatch = Stopwatch.StartNew();
 0125            TimeSpan ozonApiTime = TimeSpan.Zero;
 0126            TimeSpan clientStatusDbTime = TimeSpan.Zero;
 127
 0128            var distinctClientIds = ordersToUpdate
 0129                .Where(o => o.OzonClientId.HasValue)
 0130                .Select(o => o.OzonClientId!.Value)
 0131                .Distinct()
 0132                .ToList();
 133
 0134            var allClients = await _ozonClientServices.GetOzonClients();
 0135            var clientsDict = allClients
 0136                .Where(c => distinctClientIds.Contains(c.Id))
 0137                .ToDictionary(c => c.Id, c => c);
 138
 0139            clientsLoadStopwatch.Stop();
 0140            var clientsLoadTime = clientsLoadStopwatch.Elapsed;
 141
 0142            foreach (var order in ordersToUpdate)
 143            {
 0144                if (order.OzonClientId.HasValue &&
 0145                    clientsDict.TryGetValue(order.OzonClientId.Value, out var client))
 146                {
 0147                    order.OzonClient = client;
 148                }
 149            }
 150
 151            // Группируем по клиенту Ozon и внутри группы запрашиваем статусы параллельно (батчем) —
 152            // раньше каждый заказ ждал свой HTTP-запрос последовательно, что на 50-100 заказов давало
 153            // столько же round-trip'ов подряд внутри одного прохода SLA-джобы. SetClient меняет креды
 154            // на общем _jsonDataBuilder, поэтому между клиентами группы идут строго последовательно,
 155            // а параллелится только пачка shipment-номеров ОДНОГО клиента (безопасно, креды не меняются).
 0156            var ozonApiSw = Stopwatch.StartNew();
 0157            var clientGroups = ordersToUpdate
 0158                .Where(o => o.OzonClient is { ClientType: ClientType.OZON } &&
 0159                            !string.IsNullOrEmpty(o.OzonClient.DecryptClientId) &&
 0160                            !string.IsNullOrEmpty(o.OzonClient.DecryptApiKey) &&
 0161                            !string.IsNullOrEmpty(o.ShipmentNumber))
 0162                .GroupBy(o => o.OzonClient!.Id);
 163
 0164            foreach (var group in clientGroups)
 165            {
 0166                var client = group.First().OzonClient!;
 0167                _jsonDataBuilder.SetClient(client.DecryptClientId!, client.DecryptApiKey!);
 168
 0169                var shipmentNumbers = group
 0170                    .Select(o => o.ShipmentNumber!)
 0171                    .Where(s => !string.IsNullOrWhiteSpace(s))
 0172                    .Distinct()
 0173                    .ToArray();
 0174                var statusResults = await _jsonDataBuilder.GetProductStatusBatch(shipmentNumbers);
 175
 0176                foreach (var order in group)
 177                {
 178                    // Заказ не считается актуализированным, если Ozon не ответил — иначе SLA-очередь
 179                    // принимала бы решение по устаревшему локальному статусу, думая, что он свежий.
 0180                    var apiPosting = OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber);
 0181                    var hasStatus = statusResults.TryGetValue(order.ShipmentNumber!, out var statusResult) ||
 0182                                   (!string.IsNullOrEmpty(apiPosting) && statusResults.TryGetValue(apiPosting, out statu
 183
 0184                    if (!hasStatus || !statusResult.Success)
 185                    {
 0186                        failedOrderIds.Add(order.Id);
 0187                        updateInfoBuilder.AppendLine($"• <b>{order.ShipmentNumber}</b> ошибка запроса статуса.");
 0188                        continue;
 189                    }
 190
 0191                    var newStatus = statusResult.Status;
 192                    string newStatusName;
 0193                    if (string.IsNullOrWhiteSpace(newStatus) ||
 0194                        !OzonStatus.OrderStatuses.TryGetValue(newStatus, out var mappedStatusName))
 195                    {
 0196                        newStatusName = string.IsNullOrWhiteSpace(newStatus)
 0197                            ? order.ClientStatus?.Name ?? string.Empty
 0198                            : newStatus;
 199                    }
 200                    else
 201                    {
 0202                        newStatusName = mappedStatusName;
 203                    }
 204
 0205                    updateInfoBuilder.AppendLine(
 0206                        $"• <b>{order.ShipmentNumber}</b> {order.ClientStatus?.Name ?? ""} → {newStatusName}");
 207
 0208                    if (!string.IsNullOrEmpty(newStatusName) &&
 0209                        (order.ClientStatus?.Name == null ||
 0210                         !order.ClientStatus.Name.Equals(newStatusName, StringComparison.OrdinalIgnoreCase)))
 211                    {
 0212                        var statusSw = Stopwatch.StartNew();
 0213                        order.ClientStatus = await _clientStatusServices.GetClientStatusByNameAsync(newStatusName);
 214
 0215                        if (order.ClientStatus == null)
 216                        {
 0217                            order.ClientStatus = new ClientStatus { Name = newStatusName };
 0218                            await _clientStatusServices.AddClientStatus(order.ClientStatus);
 219                        }
 220
 0221                        statusSw.Stop();
 0222                        clientStatusDbTime += statusSw.Elapsed;
 0223                        updatingCount++;
 0224                        updatedOrderIds.Add(order.Id);
 0225                    }
 0226                }
 0227            }
 0228            ozonApiSw.Stop();
 0229            ozonApiTime += ozonApiSw.Elapsed;
 230
 0231            var saveOrdersSw = Stopwatch.StartNew();
 0232            var idsToUpdate = updatedOrderIds.Distinct().ToList();
 0233            var savedCount = await _orderServices.SaveClientStatusUpdatesAsync(idsToUpdate, ordersToUpdate, expectedStat
 0234            updatingCount = savedCount;
 0235            await _postingCancellationService.LinkOrdersAsync(ordersToUpdate, cancellationToken);
 0236            await _deliveryStatusSynchronizer.SynchronizeCancelledAsync(idArray, cancellationToken: cancellationToken);
 0237            await _deliveryStatusSynchronizer.SynchronizeDeliveredAsync(idArray, cancellationToken: cancellationToken);
 0238            saveOrdersSw.Stop();
 0239            var saveOrdersTime = saveOrdersSw.Elapsed;
 240
 0241            totalStopwatch.Stop();
 0242            var totalTime = totalStopwatch.Elapsed;
 243
 0244            var timingBuilder = new StringBuilder();
 0245            timingBuilder.AppendLine("<br/><b>Время выполнения (сек):</b>");
 0246            timingBuilder.AppendLine($"<br/>- Загрузка заказов из БД: {dbOrdersTime.TotalSeconds:F2}");
 0247            timingBuilder.AppendLine($"<br/>- Загрузка клиентов Ozon: {clientsLoadTime.TotalSeconds:F2}");
 0248            timingBuilder.AppendLine($"<br/>- Запросы в Ozon API: {ozonApiTime.TotalSeconds:F2}");
 0249            timingBuilder.AppendLine($"<br/>- Работа с БД статусов клиентов: {clientStatusDbTime.TotalSeconds:F2}");
 0250            timingBuilder.AppendLine($"<br/>- Сохранение заказов: {saveOrdersTime.TotalSeconds:F2}");
 0251            timingBuilder.AppendLine($"<br/>- Итого: {totalTime.TotalSeconds:F2}");
 252
 0253            var failedIdsSet = (IReadOnlySet<int>)failedOrderIds.Distinct().ToHashSet();
 254
 0255            if (updatingCount > 0)
 256            {
 0257                var messageBuilder = new StringBuilder();
 0258                messageBuilder.AppendLine($"Обновлено <b>{updatingCount}</b> статусов.<br/>{updateInfoBuilder}");
 0259                messageBuilder.AppendLine(timingBuilder.ToString());
 0260                return new OrdersClientStatusSyncResult { Message = messageBuilder.ToString(), FailedOrderIds = failedId
 261            }
 262
 0263            var noChangesMessageBuilder = new StringBuilder();
 0264            noChangesMessageBuilder.AppendLine("Нет изменений в статусах заказов");
 0265            noChangesMessageBuilder.AppendLine(timingBuilder.ToString());
 0266            return new OrdersClientStatusSyncResult { Message = noChangesMessageBuilder.ToString(), FailedOrderIds = fai
 267        }
 0268        catch (Exception ex)
 269        {
 0270            totalStopwatch.Stop();
 0271            var error = $"Ошибка при обновлении статусов заказов: {ex.Message}";
 0272            return new OrdersClientStatusSyncResult { Message = error, Error = error, FailedOrderIds = idArray.ToHashSet
 273        }
 0274    }
 275}