< Summary

Information
Class: Services.OzonWebhookServices.Handlers.NewPostingNotificationHandler
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/OzonWebhookServices/Handlers/NewPostingNotificationHandler.cs
Line coverage
73%
Covered lines: 58
Uncovered lines: 21
Coverable lines: 79
Total lines: 170
Line coverage: 73.4%
Branch coverage
56%
Covered branches: 25
Total branches: 44
Branch coverage: 56.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
CanHandle(...)100%11100%
HandleAsync()40%362066.03%
RecordFailureAsync(...)75%44100%
IsTransientApiException(...)81.25%221671.42%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/OzonWebhookServices/Handlers/NewPostingNotificationHandler.cs

#LineLine coverage
 1using System;
 2using System.Linq;
 3using System.Net.Http;
 4using System.Net.Sockets;
 5using System.Threading;
 6using System.Threading.Tasks;
 7using Microsoft.Extensions.Logging;
 8using Newtonsoft.Json.Linq;
 9using OzonDomains.Models;
 10using OzonDomains.Models.OzonWebhook;
 11using Services.ApiServices.OzonApi;
 12using Services.DataServices;
 13using Services.OzonWebhookServices.Models;
 14using Services.ParserServices;
 15using Services.SystemMonitoringServices;
 16
 17namespace Services.OzonWebhookServices.Handlers
 18{
 19    /// <summary>
 20    /// Обработчик уведомления <c>TYPE_NEW_POSTING</c>.
 21    ///
 22    /// При получении уведомления о новом отправлении:
 23    /// 1. Определяет OzonClient по <c>seller_id</c> из уведомления.
 24    /// 2. Загружает полные данные отправления через Ozon API (<c>/v3/posting/fbs/get</c>).
 25    /// 3. Преобразует данные в формат, совместимый с <see cref="OrderCaster.JsonToOrders"/>.
 26    /// 4. Сохраняет заказы через <see cref="OrdersDataServices.AddOrders"/>.
 27    /// 5. Возвращает строку с кратким описанием выполненных действий (для <c>ActionDetails</c>).
 28    /// </summary>
 29    public sealed class NewPostingNotificationHandler : IWebhookNotificationHandler
 30    {
 31        private readonly OzonClientServices _ozonClientServices;
 32        private readonly OzonJsonDataBuilder _jsonDataBuilder;
 33        private readonly OrderCaster _orderCaster;
 34        private readonly OrdersDataServices _ordersDataServices;
 35        private readonly ILogger<NewPostingNotificationHandler> _logger;
 36        private readonly ISystemMonitoringSignalWriter? _signals;
 37
 538        public NewPostingNotificationHandler(
 539            OzonClientServices ozonClientServices,
 540            OzonJsonDataBuilder jsonDataBuilder,
 541            OrderCaster orderCaster,
 542            OrdersDataServices ordersDataServices,
 543            ILogger<NewPostingNotificationHandler> logger,
 544            ISystemMonitoringSignalWriter? signals = null)
 45        {
 546            _ozonClientServices = ozonClientServices;
 547            _jsonDataBuilder = jsonDataBuilder;
 548            _orderCaster = orderCaster;
 549            _ordersDataServices = ordersDataServices;
 550            _logger = logger;
 551            _signals = signals;
 552        }
 53
 54        /// <inheritdoc/>
 55        public bool CanHandle(OzonMessageType messageType) =>
 356            messageType == OzonMessageType.TYPE_NEW_POSTING;
 57
 58        /// <inheritdoc/>
 59        public async Task<WebhookHandlingResult> HandleAsync(IOzonWebhookMessage notification, CancellationToken ct)
 60        {
 461            if (notification is not OzonNewPostingNotification newPosting)
 062                throw new ArgumentException(
 063                    $"Ожидался тип {nameof(OzonNewPostingNotification)}, получен {notification.GetType().Name}");
 64
 465            var postingNumber = newPosting.PostingNumber;
 466            var sellerId = newPosting.SellerId;
 67
 468            _logger.LogInformation(
 469                "TYPE_NEW_POSTING: начинаем загрузку отправления '{PostingNumber}' (seller_id={SellerId})",
 470                postingNumber, sellerId);
 71
 72            // 1. Определяем OzonClient по seller_id
 473            var clients = await _ozonClientServices.GetOzonClients();
 474            var client = clients.FirstOrDefault(c =>
 775                c.DecryptClientId != null && c.DecryptClientId == sellerId.ToString());
 76
 477            if (client == null)
 78            {
 179                var msg = $"Клиент с seller_id={sellerId} не найден в системе. " +
 180                          $"Отправление '{postingNumber}' не загружено.";
 181                _logger.LogWarning("TYPE_NEW_POSTING: {Message}", msg);
 182                await RecordFailureAsync(ct);
 183                return msg;
 84            }
 85
 86            // 2. Загружаем данные отправления через Ozon API
 387            _jsonDataBuilder.SetClient(client.DecryptClientId ?? "", client.DecryptApiKey ?? "");
 88
 89            JArray jsonData;
 90            try
 91            {
 392                jsonData = await _jsonDataBuilder.BuildDataForPosting(postingNumber);
 193            }
 294            catch (Exception exception) when (IsTransientApiException(exception))
 95            {
 196                _logger.LogInformation(
 197                    "TYPE_NEW_POSTING: Временная ошибка Ozon API при загрузке отправления '{PostingNumber}' (client='{Cl
 198                    postingNumber, client.Name, exception.Message);
 199                await RecordFailureAsync(ct);
 1100                throw new InvalidOperationException(
 1101                    $"Временная ошибка Ozon API при загрузке отправления '{postingNumber}' (client='{client.Name}'): {ex
 102            }
 1103            catch (Exception exception)
 104            {
 1105                _logger.LogWarning(exception,
 1106                    "TYPE_NEW_POSTING: Ошибка Ozon API при загрузке отправления {PostingNumber}", postingNumber);
 1107                await RecordFailureAsync(ct);
 1108                throw;
 109            }
 110
 1111            if (jsonData == null || !jsonData.Any())
 112            {
 1113                var msg = $"Данные отправления '{postingNumber}' пока отсутствуют в Ozon API (client='{client.Name}'). Б
 1114                _logger.LogInformation("TYPE_NEW_POSTING: {Message}", msg);
 1115                await RecordFailureAsync(ct);
 1116                throw new InvalidOperationException(msg);
 117            }
 118
 119            // 3. Преобразуем в заказы через существующий pipeline
 0120            var orders = await _orderCaster.JsonToOrders(jsonData);
 121
 0122            if (!orders.Any())
 123            {
 0124                var msg = $"Отправление '{postingNumber}' получено от API, " +
 0125                          $"но OrderCaster не создал ни одного заказа (client='{client.Name}').";
 0126                _logger.LogWarning("TYPE_NEW_POSTING: {Message}", msg);
 0127                await RecordFailureAsync(ct);
 0128                return msg;
 129            }
 130
 131            // 4. Привязываем OzonClient и сохраняем
 0132            foreach (var order in orders)
 133            {
 0134                order.OzonClient = client;
 135            }
 136
 0137            var result = await _ordersDataServices.AddOrders(orders);
 0138            var addCount = result[0];
 0139            var updateCount = result[1];
 140
 0141            var resultMsg = $"Новое отправление {postingNumber}. {client.Name}: добавлено {addCount}, обновлено {updateC
 142
 0143            _logger.LogInformation("TYPE_NEW_POSTING: {Result}", resultMsg);
 0144            await (_signals?.RecordAsync("ozon-order-import", "success", cancellationToken: ct) ?? Task.CompletedTask);
 145
 146            // Картинка первого товара отправления — прикладывается к Telegram-уведомлению о новом заказе.
 0147            var imageUrl = orders.Select(o => o.ProductInfo?.ImageUrl).FirstOrDefault(url => !string.IsNullOrWhiteSpace(
 148
 0149            return new WebhookHandlingResult { ActionDetails = resultMsg, ImageUrl = imageUrl };
 1150        }
 151
 152        private Task RecordFailureAsync(CancellationToken ct) =>
 4153            _signals?.RecordAsync("ozon-order-import", "failure", message: "Не удалось загрузить новый заказ Ozon", canc
 4154            ?? Task.CompletedTask;
 155
 156        private static bool IsTransientApiException(Exception exception)
 157        {
 2158            if (exception is OperationCanceledException or TimeoutException or HttpRequestException or SocketException)
 1159                return true;
 160
 1161            if (exception.InnerException != null && IsTransientApiException(exception.InnerException))
 0162                return true;
 163
 1164            if (exception is AggregateException agg)
 0165                return agg.InnerExceptions.Any(IsTransientApiException);
 166
 1167            return false;
 168        }
 169    }
 170}