< Summary

Information
Class: Services.OzonWebhookServices.Handlers.PostingCancelledNotificationHandler
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/OzonWebhookServices/Handlers/PostingCancelledNotificationHandler.cs
Line coverage
92%
Covered lines: 100
Uncovered lines: 8
Coverable lines: 108
Total lines: 203
Line coverage: 92.5%
Branch coverage
81%
Covered branches: 36
Total branches: 44
Branch coverage: 81.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()81.81%464490.58%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Linq;
 3using System.Threading;
 4using System.Threading.Tasks;
 5using Microsoft.EntityFrameworkCore;
 6using Microsoft.Extensions.Logging;
 7using OzonDomains.Models;
 8using OzonDomains.Models.OzonWebhook;
 9using OzonRepositories.Context;
 10using Services.ApiServices.OzonApi;
 11using Services.DataServices;
 12using Services.OzonWebhookServices.Models;
 13using Services.ParserServices;
 14using Services.ParserServices.HelpDictEnum;
 15using Services.OrderControlServices;
 16
 17namespace Services.OzonWebhookServices.Handlers
 18{
 19    /// <summary>
 20    /// Обработчик уведомления <c>TYPE_POSTING_CANCELLED</c>.
 21    ///
 22    /// Алгоритм:
 23    /// 1. Определяет OzonClient по <c>seller_id</c>.
 24    /// 2. Загружает полные данные отправления через Ozon API (<c>/v3/posting/fbs/get</c>)
 25    ///    и выполняет upsert через <see cref="OrdersDataServices.AddOrders"/>
 26    ///    (на случай, если <c>TYPE_NEW_POSTING</c> ранее не был обработан).
 27    /// 3. Устанавливает <see cref="Order.ClientStatus"/> = "Отменено" (из <c>posting_canceled</c>).
 28    /// 4. Устанавливает <see cref="Order.AppStatus"/> = "Отменен" если заказ в допустимом состоянии.
 29    /// </summary>
 30    public sealed class PostingCancelledNotificationHandler : IWebhookNotificationHandler
 31    {
 32        private readonly ApplicationDbContext _context;
 33        private readonly OzonClientServices _ozonClientServices;
 34        private readonly OzonJsonDataBuilder _jsonDataBuilder;
 35        private readonly OrderCaster _orderCaster;
 36        private readonly OrdersDataServices _ordersDataServices;
 37        private readonly ILogger<PostingCancelledNotificationHandler> _logger;
 38        private readonly IOrderDeliveryStatusSynchronizer _deliveryStatusSynchronizer;
 39        private readonly IOzonPostingCancellationService _cancellationService;
 1640        public PostingCancelledNotificationHandler(
 1641            ApplicationDbContext context,
 1642            OzonClientServices ozonClientServices,
 1643            OzonJsonDataBuilder jsonDataBuilder,
 1644            OrderCaster orderCaster,
 1645            OrdersDataServices ordersDataServices,
 1646            ILogger<PostingCancelledNotificationHandler> logger,
 1647            IOrderDeliveryStatusSynchronizer deliveryStatusSynchronizer,
 1648            IOzonPostingCancellationService cancellationService)
 49        {
 1650            _context = context;
 1651            _ozonClientServices = ozonClientServices;
 1652            _jsonDataBuilder = jsonDataBuilder;
 1653            _orderCaster = orderCaster;
 1654            _ordersDataServices = ordersDataServices;
 1655            _logger = logger;
 1656            _deliveryStatusSynchronizer = deliveryStatusSynchronizer;
 1657            _cancellationService = cancellationService;
 1658        }
 59
 60        /// <inheritdoc/>
 61        public bool CanHandle(OzonMessageType messageType) =>
 862            messageType == OzonMessageType.TYPE_POSTING_CANCELLED;
 63
 64        /// <inheritdoc/>
 65        public async Task<WebhookHandlingResult> HandleAsync(IOzonWebhookMessage notification, CancellationToken ct)
 66        {
 1567            if (notification is not OzonPostingCancelledNotification cancelled)
 168                throw new ArgumentException(
 169                    $"Ожидался тип {nameof(OzonPostingCancelledNotification)}, получен {notification.GetType().Name}");
 70
 1471            var postingNumber = cancelled.PostingNumber;
 1472            var sellerId = cancelled.SellerId;
 1473            var reasonText = cancelled.Reason != null
 1474                ? $"{cancelled.Reason.Message} (id={cancelled.Reason.Id})"
 1475                : "причина не указана";
 76
 1477            _logger.LogInformation(
 1478                "TYPE_POSTING_CANCELLED: начинаем обработку отмены '{PostingNumber}' " +
 1479                "(seller_id={SellerId}, reason={Reason})",
 1480                postingNumber, sellerId, reasonText);
 81
 82            // ── 1. Определяем OzonClient по seller_id ────────────────────────
 1483            var clients = await _ozonClientServices.GetOzonClients();
 1484            var client = clients.FirstOrDefault(c =>
 2785                c.DecryptClientId != null && c.DecryptClientId == sellerId.ToString());
 86
 1487            if (client == null)
 88            {
 189                var msg = $"Клиент с seller_id={sellerId} не найден. " +
 190                          $"Отмена '{postingNumber}' не обработана.";
 191                _logger.LogWarning("TYPE_POSTING_CANCELLED: {Message}", msg);
 192                return msg;
 93            }
 94
 95            // ── 2. Загружаем данные через Ozon API и делаем upsert ───────────
 96            // Гарантирует наличие заказа в БД даже если TYPE_NEW_POSTING был пропущен.
 1397            _jsonDataBuilder.SetClient(client.DecryptClientId ?? "", client.DecryptApiKey ?? "");
 98
 99            try
 100            {
 13101                var jsonData = await _jsonDataBuilder.BuildDataForPosting(postingNumber);
 12102                if (jsonData != null && jsonData.Any())
 103                {
 0104                    var orders = await _orderCaster.JsonToOrders(jsonData);
 0105                    foreach (var order in orders)
 0106                        order.OzonClient = client;
 0107                    await _ordersDataServices.AddOrders(orders);
 108
 0109                    _logger.LogInformation(
 0110                        "TYPE_POSTING_CANCELLED: upsert {Count} заказ(а/ов) для '{PostingNumber}'",
 0111                        orders.Count, postingNumber);
 0112                }
 12113            }
 1114            catch (Exception ex)
 115            {
 116                // Не критично — основная цель (обновление статуса) продолжается.
 1117                _logger.LogInformation(
 1118                    "TYPE_POSTING_CANCELLED: не удалось предварительно загрузить данные отправления '{PostingNumber}' от
 1119                    postingNumber, ex.Message);
 1120            }
 121
 122            // ── 3. Получаем/создаём ClientStatus = "Отменено" ────────────────
 123            const string cancelledState = "posting_canceled";
 13124            var humanStatus = OzonStatus.PostingSubStatuses.TryGetValue(cancelledState, out var sub)
 13125                ? sub
 13126                : "Отменён";
 127
 13128            var clientStatus = await _context.ClientStatuses
 13129                .FirstOrDefaultAsync(cs => cs.Name == humanStatus, ct);
 130
 12131            if (clientStatus == null)
 132            {
 10133                clientStatus = new ClientStatus { Name = humanStatus };
 10134                await _context.ClientStatuses.AddAsync(clientStatus, ct);
 10135                await _context.SaveChangesAsync(ct);
 136            }
 137
 138            // ── 4. Находим заказы в БД по номеру отправления ─────────────────
 12139            var dbOrders = await _context.Orders
 12140                .Include(o => o.AppStatus)
 12141                .Include(o => o.ClientStatus)
 12142                .Where(o => o.ShipmentNumber != null
 12143                         && (o.ShipmentNumber == postingNumber
 12144                             || o.ShipmentNumber.StartsWith(postingNumber + "/")))
 12145                .ToListAsync(ct);
 146
 12147            if (!dbOrders.Any())
 148            {
 2149                var msg = $"Заказы '{postingNumber}' не найдены в БД после upsert. " +
 2150                          $"Причина отмены: {reasonText}.";
 2151                _logger.LogWarning("TYPE_POSTING_CANCELLED: {Message}", msg);
 2152                return msg;
 153            }
 154
 10155            string? reasonTitle = null;
 10156            string? reasonTypeId = null;
 10157            if (cancelled.Reason?.Id > 0)
 158            {
 159                try
 160                {
 4161                    var catalog = await _jsonDataBuilder.GetFbsCancelReasonCatalogAsync();
 5162                    var catalogReason = catalog.Result?.FirstOrDefault(x => x.Id == cancelled.Reason.Id);
 3163                    reasonTitle = catalogReason?.Title;
 3164                    reasonTypeId = catalogReason?.TypeId;
 3165                }
 1166                catch (Exception ex)
 167                {
 1168                    _logger.LogWarning(ex, "TYPE_POSTING_CANCELLED: не удалось расшифровать причину {ReasonId}", cancell
 1169                }
 170            }
 171
 10172            await _cancellationService.RecordAsync(client.Id, postingNumber, cancelled.Reason?.Id,
 10173                reasonTitle, cancelled.Reason?.Message, reasonTypeId, cancelled.ChangedStateDate, "Webhook",
 21174                dbOrders.Select(x => x.Id).ToArray(), ct);
 175
 176            // ── 5. Обновляем ClientStatus заказов ────────────────────────────
 10177            var now = DateTime.Now;
 10178            var clientStatusUpdated = 0;
 179
 42180            foreach (var order in dbOrders)
 181            {
 11182                if (order.ClientStatusId != clientStatus.Id)
 183                {
 10184                    order.ClientStatusId = clientStatus.Id;
 10185                    order.LastStatusChangeDate = now;
 10186                    clientStatusUpdated++;
 187                }
 188            }
 189
 10190            await _context.SaveChangesAsync(ct);
 21191            await _deliveryStatusSynchronizer.SynchronizeCancelledAsync(dbOrders.Select(x => x.Id).ToArray(), cancellati
 192
 10193            var updatePart = clientStatusUpdated > 0
 10194                ? $"Статус обновлён у {clientStatusUpdated} заказ(ов)."
 10195                : "В Studio2 изменений нет.";
 10196            var reasonPart = string.IsNullOrWhiteSpace(reasonText) ? string.Empty : $" Причина: {reasonText}.";
 10197            var result = $"Отправление {postingNumber} отменено. {updatePart}{reasonPart}";
 198
 10199            _logger.LogInformation("TYPE_POSTING_CANCELLED: {Result}", result);
 10200            return result;
 13201        }
 202    }
 203}