< Summary

Information
Class: Services.OzonWebhookServices.Handlers.StateChangedNotificationHandler
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/OzonWebhookServices/Handlers/StateChangedNotificationHandler.cs
Line coverage
97%
Covered lines: 70
Uncovered lines: 2
Coverable lines: 72
Total lines: 158
Line coverage: 97.2%
Branch coverage
88%
Covered branches: 30
Total branches: 34
Branch coverage: 88.2%
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()90%303096.42%
ToLocalEventTime(...)75%44100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/OzonWebhookServices/Handlers/StateChangedNotificationHandler.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.OzonWebhookServices.Models;
 11using Services.OrderControlServices;
 12using Services.ParserServices.HelpDictEnum;
 13
 14namespace Services.OzonWebhookServices.Handlers
 15{
 16    /// <summary>
 17    /// Обработчик уведомления <c>TYPE_STATE_CHANGED</c>.
 18    ///
 19    /// При получении уведомления об изменении статуса отправления:
 20    /// 1. Переводит Ozon-статус из нотации <c>new_state</c> ("posting_delivered" и т.д.)
 21    ///    в читаемое название через <see cref="OzonStatus.PostingSubStatuses"/>
 22    ///    (с fallback на <see cref="OzonStatus.OrderStatuses"/>).
 23    /// 2. Находит или создаёт запись <see cref="ClientStatus"/> с этим названием.
 24    /// 3. Обновляет <see cref="Order.ClientStatusId"/> и <see cref="Order.LastStatusChangeDate"/>
 25    ///    у всех заказов с совпадающим <see cref="Order.ShipmentNumber"/>.
 26    /// 4. Возвращает строку с кратким описанием выполненных действий (для <c>ActionDetails</c>).
 27    ///
 28    /// Для добавления обработчика нового типа уведомления создайте аналогичный класс,
 29    /// реализующий <see cref="IWebhookNotificationHandler"/>, и зарегистрируйте его в DI.
 30    /// </summary>
 31    public sealed class StateChangedNotificationHandler : IWebhookNotificationHandler
 32    {
 33        private readonly ApplicationDbContext _context;
 34        private readonly ILogger<StateChangedNotificationHandler> _logger;
 35        private readonly IOrderDeliveryStatusSynchronizer _deliveryStatusSynchronizer;
 36
 537        public StateChangedNotificationHandler(
 538            ApplicationDbContext context,
 539            ILogger<StateChangedNotificationHandler> logger,
 540            IOrderDeliveryStatusSynchronizer deliveryStatusSynchronizer)
 41        {
 542            _context = context;
 543            _logger = logger;
 544            _deliveryStatusSynchronizer = deliveryStatusSynchronizer;
 545        }
 46
 47        /// <inheritdoc/>
 48        public bool CanHandle(OzonMessageType messageType) =>
 349            messageType == OzonMessageType.TYPE_STATE_CHANGED;
 50
 51        /// <inheritdoc/>
 52        public async Task<WebhookHandlingResult> HandleAsync(IOzonWebhookMessage notification, CancellationToken ct)
 53        {
 454            if (notification is not OzonStateChangedNotification stateChanged)
 055                throw new ArgumentException(
 056                    $"Ожидался тип {nameof(OzonStateChangedNotification)}, получен {notification.GetType().Name}");
 57
 458            var postingNumber = stateChanged.PostingNumber;
 459            var newState = stateChanged.NewState;
 60
 61            // Переводим Ozon-статус в читаемое название.
 62            // new_state приходит в формате "posting_*" → ищем в PostingSubStatuses,
 63            // затем в OrderStatuses (для совместимости), иначе оставляем raw-значение.
 464            var humanStatus = OzonStatus.PostingSubStatuses.TryGetValue(newState, out var sub)
 465                ? sub
 466                : OzonStatus.OrderStatuses.TryGetValue(newState, out var plain)
 467                    ? plain
 468                    : newState;
 69
 70            // Находим или создаём запись ClientStatus с данным именем (по аналогии с OrderCaster).
 471            var clientStatus = await _context.ClientStatuses
 472                .FirstOrDefaultAsync(cs => cs.Name == humanStatus, ct);
 73
 474            if (clientStatus == null)
 75            {
 476                clientStatus = new ClientStatus { Name = humanStatus };
 477                await _context.ClientStatuses.AddAsync(clientStatus, ct);
 478                await _context.SaveChangesAsync(ct);
 79
 480                _logger.LogInformation(
 481                    "TYPE_STATE_CHANGED: создан новый ClientStatus '{Name}' (из Ozon new_state='{NewState}')",
 482                    humanStatus, newState);
 83            }
 84
 85            // Ищем заказы по номеру отправления (с суффиксом /N и без него).
 486            var orders = await _context.Orders
 487                .Where(o => o.ShipmentNumber != null
 488                         && (o.ShipmentNumber == postingNumber
 489                             || o.ShipmentNumber.StartsWith(postingNumber + "/")))
 490                .ToListAsync(ct);
 91
 492            if (!orders.Any())
 93            {
 194                _logger.LogInformation(
 195                    "TYPE_STATE_CHANGED: заказы с номером отправления '{PostingNumber}' не найдены в БД (статус Ozon: '{
 196                    postingNumber, humanStatus);
 197                return $"Отправление {postingNumber} не найдено в Studio2. Статус Ozon: «{humanStatus}».";
 98            }
 99
 100            // Ozon не гарантирует порядок доставки пушей (и ретраит их): уведомление, событие которого старше
 101            // последней известной смены статуса, не должно откатывать более свежий статус.
 3102            var eventTime = ToLocalEventTime(stateChanged.ChangedStateDate);
 103
 3104            var updatedCount = 0;
 3105            var staleCount = 0;
 12106            foreach (var order in orders)
 107            {
 3108                if (order.ClientStatusId == clientStatus.Id)
 109                    continue;
 110
 3111                if (eventTime.HasValue && order.LastStatusChangeDate.HasValue && eventTime.Value < order.LastStatusChang
 112                {
 1113                    staleCount++;
 1114                    continue;
 115                }
 116
 2117                order.ClientStatusId = clientStatus.Id;
 2118                order.LastStatusChangeDate = eventTime ?? DateTime.Now;
 2119                updatedCount++;
 120            }
 121
 3122            if (staleCount > 0)
 123            {
 1124                _logger.LogInformation(
 1125                    "TYPE_STATE_CHANGED: {PostingNumber} — пропущено устаревшее уведомление '{HumanStatus}' от {EventTim
 1126                    postingNumber, humanStatus, eventTime, staleCount);
 127            }
 128
 3129            if (updatedCount > 0)
 2130                await _context.SaveChangesAsync(ct);
 131
 6132            var orderIds = orders.Select(x => x.Id).ToArray();
 3133            await _deliveryStatusSynchronizer.SynchronizeCancelledAsync(orderIds, cancellationToken: ct);
 3134            await _deliveryStatusSynchronizer.SynchronizeDeliveredAsync(orderIds, cancellationToken: ct);
 135
 3136            var result = updatedCount > 0
 3137                ? $"Отправление {postingNumber}. Статус: «{humanStatus}». Обновлено заказов: {updatedCount}."
 3138                : staleCount > 0
 3139                    ? $"Отправление {postingNumber}. Уведомление «{humanStatus}» устарело (статус изменён позже) — пропу
 3140                    : $"Отправление {postingNumber}. Статус уже «{humanStatus}» — изменений нет.";
 141
 3142            _logger.LogInformation("TYPE_STATE_CHANGED: {Result}", result);
 3143            return result;
 4144        }
 145
 146        /// <summary>changed_state_date приходит в UTC; LastStatusChangeDate хранится в локальном времени сервера.</summ
 147        private static DateTime? ToLocalEventTime(DateTime changedStateDate)
 148        {
 3149            if (changedStateDate == default)
 1150                return null;
 151
 2152            var utc = changedStateDate.Kind == DateTimeKind.Local
 2153                ? changedStateDate.ToUniversalTime()
 2154                : DateTime.SpecifyKind(changedStateDate, DateTimeKind.Utc);
 2155            return utc.ToLocalTime();
 156        }
 157    }
 158}