| | | 1 | | using System; |
| | | 2 | | using System.Linq; |
| | | 3 | | using System.Threading; |
| | | 4 | | using System.Threading.Tasks; |
| | | 5 | | using Microsoft.EntityFrameworkCore; |
| | | 6 | | using Microsoft.Extensions.Logging; |
| | | 7 | | using OzonDomains.Models; |
| | | 8 | | using OzonDomains.Models.OzonWebhook; |
| | | 9 | | using OzonRepositories.Context; |
| | | 10 | | using Services.ApiServices.OzonApi; |
| | | 11 | | using Services.DataServices; |
| | | 12 | | using Services.OzonWebhookServices.Models; |
| | | 13 | | using Services.ParserServices; |
| | | 14 | | using Services.ParserServices.HelpDictEnum; |
| | | 15 | | using Services.OrderControlServices; |
| | | 16 | | |
| | | 17 | | namespace 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; |
| | 16 | 40 | | public PostingCancelledNotificationHandler( |
| | 16 | 41 | | ApplicationDbContext context, |
| | 16 | 42 | | OzonClientServices ozonClientServices, |
| | 16 | 43 | | OzonJsonDataBuilder jsonDataBuilder, |
| | 16 | 44 | | OrderCaster orderCaster, |
| | 16 | 45 | | OrdersDataServices ordersDataServices, |
| | 16 | 46 | | ILogger<PostingCancelledNotificationHandler> logger, |
| | 16 | 47 | | IOrderDeliveryStatusSynchronizer deliveryStatusSynchronizer, |
| | 16 | 48 | | IOzonPostingCancellationService cancellationService) |
| | | 49 | | { |
| | 16 | 50 | | _context = context; |
| | 16 | 51 | | _ozonClientServices = ozonClientServices; |
| | 16 | 52 | | _jsonDataBuilder = jsonDataBuilder; |
| | 16 | 53 | | _orderCaster = orderCaster; |
| | 16 | 54 | | _ordersDataServices = ordersDataServices; |
| | 16 | 55 | | _logger = logger; |
| | 16 | 56 | | _deliveryStatusSynchronizer = deliveryStatusSynchronizer; |
| | 16 | 57 | | _cancellationService = cancellationService; |
| | 16 | 58 | | } |
| | | 59 | | |
| | | 60 | | /// <inheritdoc/> |
| | | 61 | | public bool CanHandle(OzonMessageType messageType) => |
| | 8 | 62 | | messageType == OzonMessageType.TYPE_POSTING_CANCELLED; |
| | | 63 | | |
| | | 64 | | /// <inheritdoc/> |
| | | 65 | | public async Task<WebhookHandlingResult> HandleAsync(IOzonWebhookMessage notification, CancellationToken ct) |
| | | 66 | | { |
| | 15 | 67 | | if (notification is not OzonPostingCancelledNotification cancelled) |
| | 1 | 68 | | throw new ArgumentException( |
| | 1 | 69 | | $"Ожидался тип {nameof(OzonPostingCancelledNotification)}, получен {notification.GetType().Name}"); |
| | | 70 | | |
| | 14 | 71 | | var postingNumber = cancelled.PostingNumber; |
| | 14 | 72 | | var sellerId = cancelled.SellerId; |
| | 14 | 73 | | var reasonText = cancelled.Reason != null |
| | 14 | 74 | | ? $"{cancelled.Reason.Message} (id={cancelled.Reason.Id})" |
| | 14 | 75 | | : "причина не указана"; |
| | | 76 | | |
| | 14 | 77 | | _logger.LogInformation( |
| | 14 | 78 | | "TYPE_POSTING_CANCELLED: начинаем обработку отмены '{PostingNumber}' " + |
| | 14 | 79 | | "(seller_id={SellerId}, reason={Reason})", |
| | 14 | 80 | | postingNumber, sellerId, reasonText); |
| | | 81 | | |
| | | 82 | | // ── 1. Определяем OzonClient по seller_id ──────────────────────── |
| | 14 | 83 | | var clients = await _ozonClientServices.GetOzonClients(); |
| | 14 | 84 | | var client = clients.FirstOrDefault(c => |
| | 27 | 85 | | c.DecryptClientId != null && c.DecryptClientId == sellerId.ToString()); |
| | | 86 | | |
| | 14 | 87 | | if (client == null) |
| | | 88 | | { |
| | 1 | 89 | | var msg = $"Клиент с seller_id={sellerId} не найден. " + |
| | 1 | 90 | | $"Отмена '{postingNumber}' не обработана."; |
| | 1 | 91 | | _logger.LogWarning("TYPE_POSTING_CANCELLED: {Message}", msg); |
| | 1 | 92 | | return msg; |
| | | 93 | | } |
| | | 94 | | |
| | | 95 | | // ── 2. Загружаем данные через Ozon API и делаем upsert ─────────── |
| | | 96 | | // Гарантирует наличие заказа в БД даже если TYPE_NEW_POSTING был пропущен. |
| | 13 | 97 | | _jsonDataBuilder.SetClient(client.DecryptClientId ?? "", client.DecryptApiKey ?? ""); |
| | | 98 | | |
| | | 99 | | try |
| | | 100 | | { |
| | 13 | 101 | | var jsonData = await _jsonDataBuilder.BuildDataForPosting(postingNumber); |
| | 12 | 102 | | if (jsonData != null && jsonData.Any()) |
| | | 103 | | { |
| | 0 | 104 | | var orders = await _orderCaster.JsonToOrders(jsonData); |
| | 0 | 105 | | foreach (var order in orders) |
| | 0 | 106 | | order.OzonClient = client; |
| | 0 | 107 | | await _ordersDataServices.AddOrders(orders); |
| | | 108 | | |
| | 0 | 109 | | _logger.LogInformation( |
| | 0 | 110 | | "TYPE_POSTING_CANCELLED: upsert {Count} заказ(а/ов) для '{PostingNumber}'", |
| | 0 | 111 | | orders.Count, postingNumber); |
| | 0 | 112 | | } |
| | 12 | 113 | | } |
| | 1 | 114 | | catch (Exception ex) |
| | | 115 | | { |
| | | 116 | | // Не критично — основная цель (обновление статуса) продолжается. |
| | 1 | 117 | | _logger.LogInformation( |
| | 1 | 118 | | "TYPE_POSTING_CANCELLED: не удалось предварительно загрузить данные отправления '{PostingNumber}' от |
| | 1 | 119 | | postingNumber, ex.Message); |
| | 1 | 120 | | } |
| | | 121 | | |
| | | 122 | | // ── 3. Получаем/создаём ClientStatus = "Отменено" ──────────────── |
| | | 123 | | const string cancelledState = "posting_canceled"; |
| | 13 | 124 | | var humanStatus = OzonStatus.PostingSubStatuses.TryGetValue(cancelledState, out var sub) |
| | 13 | 125 | | ? sub |
| | 13 | 126 | | : "Отменён"; |
| | | 127 | | |
| | 13 | 128 | | var clientStatus = await _context.ClientStatuses |
| | 13 | 129 | | .FirstOrDefaultAsync(cs => cs.Name == humanStatus, ct); |
| | | 130 | | |
| | 12 | 131 | | if (clientStatus == null) |
| | | 132 | | { |
| | 10 | 133 | | clientStatus = new ClientStatus { Name = humanStatus }; |
| | 10 | 134 | | await _context.ClientStatuses.AddAsync(clientStatus, ct); |
| | 10 | 135 | | await _context.SaveChangesAsync(ct); |
| | | 136 | | } |
| | | 137 | | |
| | | 138 | | // ── 4. Находим заказы в БД по номеру отправления ───────────────── |
| | 12 | 139 | | var dbOrders = await _context.Orders |
| | 12 | 140 | | .Include(o => o.AppStatus) |
| | 12 | 141 | | .Include(o => o.ClientStatus) |
| | 12 | 142 | | .Where(o => o.ShipmentNumber != null |
| | 12 | 143 | | && (o.ShipmentNumber == postingNumber |
| | 12 | 144 | | || o.ShipmentNumber.StartsWith(postingNumber + "/"))) |
| | 12 | 145 | | .ToListAsync(ct); |
| | | 146 | | |
| | 12 | 147 | | if (!dbOrders.Any()) |
| | | 148 | | { |
| | 2 | 149 | | var msg = $"Заказы '{postingNumber}' не найдены в БД после upsert. " + |
| | 2 | 150 | | $"Причина отмены: {reasonText}."; |
| | 2 | 151 | | _logger.LogWarning("TYPE_POSTING_CANCELLED: {Message}", msg); |
| | 2 | 152 | | return msg; |
| | | 153 | | } |
| | | 154 | | |
| | 10 | 155 | | string? reasonTitle = null; |
| | 10 | 156 | | string? reasonTypeId = null; |
| | 10 | 157 | | if (cancelled.Reason?.Id > 0) |
| | | 158 | | { |
| | | 159 | | try |
| | | 160 | | { |
| | 4 | 161 | | var catalog = await _jsonDataBuilder.GetFbsCancelReasonCatalogAsync(); |
| | 5 | 162 | | var catalogReason = catalog.Result?.FirstOrDefault(x => x.Id == cancelled.Reason.Id); |
| | 3 | 163 | | reasonTitle = catalogReason?.Title; |
| | 3 | 164 | | reasonTypeId = catalogReason?.TypeId; |
| | 3 | 165 | | } |
| | 1 | 166 | | catch (Exception ex) |
| | | 167 | | { |
| | 1 | 168 | | _logger.LogWarning(ex, "TYPE_POSTING_CANCELLED: не удалось расшифровать причину {ReasonId}", cancell |
| | 1 | 169 | | } |
| | | 170 | | } |
| | | 171 | | |
| | 10 | 172 | | await _cancellationService.RecordAsync(client.Id, postingNumber, cancelled.Reason?.Id, |
| | 10 | 173 | | reasonTitle, cancelled.Reason?.Message, reasonTypeId, cancelled.ChangedStateDate, "Webhook", |
| | 21 | 174 | | dbOrders.Select(x => x.Id).ToArray(), ct); |
| | | 175 | | |
| | | 176 | | // ── 5. Обновляем ClientStatus заказов ──────────────────────────── |
| | 10 | 177 | | var now = DateTime.Now; |
| | 10 | 178 | | var clientStatusUpdated = 0; |
| | | 179 | | |
| | 42 | 180 | | foreach (var order in dbOrders) |
| | | 181 | | { |
| | 11 | 182 | | if (order.ClientStatusId != clientStatus.Id) |
| | | 183 | | { |
| | 10 | 184 | | order.ClientStatusId = clientStatus.Id; |
| | 10 | 185 | | order.LastStatusChangeDate = now; |
| | 10 | 186 | | clientStatusUpdated++; |
| | | 187 | | } |
| | | 188 | | } |
| | | 189 | | |
| | 10 | 190 | | await _context.SaveChangesAsync(ct); |
| | 21 | 191 | | await _deliveryStatusSynchronizer.SynchronizeCancelledAsync(dbOrders.Select(x => x.Id).ToArray(), cancellati |
| | | 192 | | |
| | 10 | 193 | | var updatePart = clientStatusUpdated > 0 |
| | 10 | 194 | | ? $"Статус обновлён у {clientStatusUpdated} заказ(ов)." |
| | 10 | 195 | | : "В Studio2 изменений нет."; |
| | 10 | 196 | | var reasonPart = string.IsNullOrWhiteSpace(reasonText) ? string.Empty : $" Причина: {reasonText}."; |
| | 10 | 197 | | var result = $"Отправление {postingNumber} отменено. {updatePart}{reasonPart}"; |
| | | 198 | | |
| | 10 | 199 | | _logger.LogInformation("TYPE_POSTING_CANCELLED: {Result}", result); |
| | 10 | 200 | | return result; |
| | 13 | 201 | | } |
| | | 202 | | } |
| | | 203 | | } |