< Summary

Information
Class: Services.ApiServices.OzonApi.OzonOrderCancellationService
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/OzonApi/OzonOrderCancellationService.cs
Line coverage
13%
Covered lines: 44
Uncovered lines: 275
Coverable lines: 319
Total lines: 588
Line coverage: 13.7%
Branch coverage
15%
Covered branches: 30
Total branches: 194
Branch coverage: 15.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
LoadCancelReasonsAsync()0%1482380%
RefreshClientStatusesAsync()0%702260%
CancelOrdersAsync()0%1056320%
ParseFormRequests(...)100%2020100%
BuildSuccessHtml(...)100%1010100%
IsOzonMarketplaceOrderAsync()0%110100%
ResolveOzonClientId(...)0%110100%
EnsureOzonClientsResolvedAsync()0%930300%
ApplyOzonClientFromSource(...)0%210140%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/OzonApi/OzonOrderCancellationService.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Http;
 2using Microsoft.EntityFrameworkCore;
 3using System.Net;
 4using System.Text;
 5using OzonDomains;
 6using OzonDomains.Models;
 7using OzonRepositories.Context;
 8using Services.ApiServices.OzonApi.Filters;
 9using Services.DataServices;
 10using Services.LabelServices;
 11using SlqStudio.Application.Services.EmailService;
 12using Services.OzonWebhookServices;
 13
 14namespace Services.ApiServices.OzonApi
 15{
 16    /// <summary>
 17    /// Данные для UI отмены отправления в Ozon (используется на страницах транзакций,
 18    /// где заказу можно назначить статус «Отменен»).
 19    /// </summary>
 20    public class OzonOrderCancelReasonsInfo
 21    {
 22        public const long ReasonRequiresMessageId = 402;
 23
 24        public string? PostingNumber { get; set; }
 25
 26        public string? ErrorMessage { get; set; }
 27
 28        public List<OzonCancelReasonOption> Reasons { get; set; } = new();
 29
 30        public bool CanCancelInOzon => string.IsNullOrEmpty(ErrorMessage) && Reasons.Count > 0;
 31    }
 32
 33    public class OzonCancelReasonOption
 34    {
 35        public long Id { get; set; }
 36
 37        public string Title { get; set; } = string.Empty;
 38
 39        public string? TypeId { get; set; }
 40    }
 41
 42    public sealed record OzonCancelFormRequest(int OrderId, long ReasonId, string? ReasonMessage, string? ReasonTitle);
 43
 44    public sealed record OzonCancelSuccessEntry(string ShipmentNumber, string? ReasonTitle, string? ReasonMessage);
 45
 46    public sealed record OzonCancelExecutionResult(List<string> Errors, List<OzonCancelSuccessEntry> SuccessEntries);
 47
 48    /// <summary>
 49    /// Общая логика отмены отправлений Ozon для страниц транзакций: загрузка причин отмены,
 50    /// разбор формы и вызов Ozon API отмены. Используется в транзакциях, где заказу можно
 51    /// назначить статус «Отменен».
 52    /// </summary>
 53    public class OzonOrderCancellationService
 54    {
 55        private readonly ApplicationDbContext _context;
 56        private readonly IHttpClientFactory _httpClientFactory;
 57        private readonly IEmailService? _emailService;
 58        private readonly OzonClientServices _ozonClientServices;
 59        private readonly IOzonPostingCancellationService _postingCancellationService;
 60
 061        public OzonOrderCancellationService(
 062            ApplicationDbContext context,
 063            IHttpClientFactory httpClientFactory,
 064            IEmailService? emailService,
 065            OzonClientServices ozonClientServices,
 066            IOzonPostingCancellationService postingCancellationService)
 67        {
 068            _context = context;
 069            _httpClientFactory = httpClientFactory;
 070            _emailService = emailService;
 071            _ozonClientServices = ozonClientServices;
 072            _postingCancellationService = postingCancellationService;
 073        }
 74
 75        public async Task<Dictionary<int, OzonOrderCancelReasonsInfo>> LoadCancelReasonsAsync(
 76            IReadOnlyList<Order> orders)
 77        {
 078            var reasonsByOrderId = new Dictionary<int, OzonOrderCancelReasonsInfo>();
 79
 080            if (orders.Count == 0)
 081                return reasonsByOrderId;
 82
 083            await EnsureOzonClientsResolvedAsync(orders);
 84
 085            foreach (var order in orders)
 86            {
 087                if (!await IsOzonMarketplaceOrderAsync(order))
 88                    continue;
 89
 090                if (string.IsNullOrWhiteSpace(order.ShipmentNumber))
 91                {
 092                    reasonsByOrderId[order.Id] = new OzonOrderCancelReasonsInfo
 093                    {
 094                        ErrorMessage = "Нет номера отправления"
 095                    };
 096                    continue;
 97                }
 98
 099                if (!ResolveOzonClientId(order).HasValue)
 100                {
 0101                    reasonsByOrderId[order.Id] = new OzonOrderCancelReasonsInfo
 0102                    {
 0103                        PostingNumber = order.ShipmentNumber.Trim(),
 0104                        ErrorMessage = "Не указан клиент Ozon"
 0105                    };
 0106                    continue;
 107                }
 108
 0109                reasonsByOrderId[order.Id] = new OzonOrderCancelReasonsInfo
 0110                {
 0111                    PostingNumber = order.ShipmentNumber.Trim()
 0112                };
 0113            }
 114
 0115            var clientGroups = orders
 0116                .Where(o => reasonsByOrderId.ContainsKey(o.Id))
 0117                .Select(o => new { Order = o, ClientId = ResolveOzonClientId(o) })
 0118                .Where(x => x.ClientId.HasValue && !string.IsNullOrWhiteSpace(x.Order.ShipmentNumber))
 0119                .GroupBy(x => x.ClientId!.Value, x => x.Order);
 120
 0121            foreach (var clientGroup in clientGroups)
 122            {
 123                try
 124                {
 0125                    var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientGroup.Key);
 0126                    if (ozonClient == null)
 127                    {
 0128                        foreach (var order in clientGroup)
 0129                            reasonsByOrderId[order.Id].ErrorMessage = "Клиент Ozon не найден";
 0130                        continue;
 131                    }
 132
 0133                    var clientId = ozonClient.DecryptClientId;
 0134                    var apiKey = ozonClient.DecryptApiKey;
 135
 0136                    if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(apiKey))
 137                    {
 0138                        foreach (var order in clientGroup)
 139                        {
 0140                            reasonsByOrderId[order.Id].ErrorMessage = "Не настроены Client-Id или Api-Key Ozon";
 141                        }
 142
 0143                        continue;
 144                    }
 145
 0146                    var apiManager = new OzonApiDataManager(_httpClientFactory, _emailService, clientId, apiKey);
 0147                    var postingNumbers = clientGroup
 0148                        .Select(o => o.ShipmentNumber!.Trim())
 0149                        .Distinct(StringComparer.OrdinalIgnoreCase)
 0150                        .ToList();
 151
 0152                    var response = await apiManager.GetFbsCancelReasonsAsync(postingNumbers);
 0153                    var reasonsByPosting = (response?.Result ?? new List<FbsCancelReasonPostingResult>())
 0154                        .Where(r => !string.IsNullOrWhiteSpace(r.PostingNumber))
 0155                        .ToDictionary(
 0156                            r => r.PostingNumber!,
 0157                            r => r.Reasons ?? new List<FbsCancelReasonItem>(),
 0158                            StringComparer.OrdinalIgnoreCase);
 159
 0160                    var withoutReasons = new List<Order>();
 0161                    foreach (var order in clientGroup)
 162                    {
 0163                        var posting = order.ShipmentNumber!.Trim();
 0164                        if (!reasonsByPosting.TryGetValue(posting, out var cancelReasons) || cancelReasons.Count == 0)
 165                        {
 0166                            reasonsByOrderId[order.Id].ErrorMessage = NoReasonsMessage;
 0167                            withoutReasons.Add(order);
 0168                            continue;
 169                        }
 170
 0171                        reasonsByOrderId[order.Id].Reasons = cancelReasons
 0172                            .Select(r => new OzonCancelReasonOption
 0173                            {
 0174                                Id = r.Id,
 0175                                Title = r.Title ?? string.Empty,
 0176                                TypeId = r.TypeId
 0177                            })
 0178                            .ToList();
 179                    }
 180
 0181                    if (withoutReasons.Count > 0)
 0182                        await RefreshClientStatusesAsync(apiManager, withoutReasons, reasonsByOrderId);
 0183                }
 0184                catch (Exception ex)
 185                {
 0186                    foreach (var order in clientGroup)
 0187                        reasonsByOrderId[order.Id].ErrorMessage = ex.Message;
 0188                }
 0189            }
 190
 0191            return reasonsByOrderId;
 0192        }
 193
 194        private const string NoReasonsMessage =
 195            "Ozon не вернул причины отмены: отправление, вероятно, уже отменено или отгружено в Ozon";
 196
 197        /// <summary>
 198        /// Ozon не отдаёт причины отмены, когда отправление уже не отменить, а клиентский статус в базе мог устареть
 199        /// (например, «Ожидает упаковки», хотя в Ozon оно давно «Отменён»). Спрашиваем актуальный статус, обновляем
 200        /// ClientStatus заказа и пишем его в подсказку. Статус приложения не трогаем: на странице отмены заказ ещё
 201        /// должен пройти проведение. Сбой проверки не мешает странице — остаётся общая подсказка.
 202        /// </summary>
 203        private async Task RefreshClientStatusesAsync(
 204            OzonApiDataManager apiManager,
 205            IReadOnlyCollection<Order> orders,
 206            Dictionary<int, OzonOrderCancelReasonsInfo> reasonsByOrderId)
 207        {
 208            try
 209            {
 0210                var numbers = orders.Select(o => o.ShipmentNumber!.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).To
 0211                var statuses = await apiManager.GetProductStatusBatch(numbers);
 212
 0213                var updates = new Dictionary<int, string>();
 0214                foreach (var order in orders)
 215                {
 0216                    var posting = order.ShipmentNumber!.Trim();
 0217                    var apiPosting = OzonPostingNumberHelper.GetOzonApiPostingNumber(posting);
 0218                    var found = statuses.TryGetValue(posting, out var api) ||
 0219                                (!string.IsNullOrEmpty(apiPosting) && statuses.TryGetValue(apiPosting, out api));
 0220                    if (!found || !api.Success || string.IsNullOrWhiteSpace(api.Status))
 221                        continue;
 222
 0223                    var humanStatus = Services.ParserServices.HelpDictEnum.OzonStatus.OrderStatuses.TryGetValue(api.Stat
 0224                        ? mapped
 0225                        : api.Status;
 0226                    reasonsByOrderId[order.Id].ErrorMessage =
 0227                        $"Отправление в Ozon уже в статусе «{humanStatus}» — отмена через API недоступна";
 228
 0229                    if (!string.Equals(order.ClientStatus?.Name, humanStatus, StringComparison.OrdinalIgnoreCase))
 0230                        updates[order.Id] = humanStatus;
 231                }
 232
 0233                if (updates.Count == 0)
 0234                    return;
 235
 0236                var ids = updates.Keys.ToList();
 0237                var tracked = await _context.Orders.Where(o => ids.Contains(o.Id)).ToListAsync();
 0238                var statusCache = new Dictionary<string, ClientStatus>(StringComparer.OrdinalIgnoreCase);
 0239                var now = DateTime.Now;
 0240                foreach (var order in tracked)
 241                {
 0242                    var name = updates[order.Id];
 0243                    if (!statusCache.TryGetValue(name, out var clientStatus))
 244                    {
 0245                        clientStatus = await _context.ClientStatuses.FirstOrDefaultAsync(cs => cs.Name == name);
 0246                        if (clientStatus == null)
 247                        {
 0248                            clientStatus = new ClientStatus { Name = name };
 0249                            _context.ClientStatuses.Add(clientStatus);
 0250                            await _context.SaveChangesAsync();
 251                        }
 252
 0253                        statusCache[name] = clientStatus;
 254                    }
 255
 0256                    order.ClientStatusId = clientStatus.Id;
 0257                    order.LastStatusChangeDate = now;
 0258                }
 259
 0260                await _context.SaveChangesAsync();
 0261            }
 0262            catch
 263            {
 264                // Подсказка и так объясняет, почему чекбокса нет; проверка статуса — best effort.
 0265            }
 0266        }
 267
 268        public async Task<OzonCancelExecutionResult> CancelOrdersAsync(
 269            IReadOnlyList<OzonCancelFormRequest> requests,
 270            IReadOnlyDictionary<int, Order> ordersById)
 271        {
 0272            var errors = new List<string>();
 0273            var successEntries = new List<OzonCancelSuccessEntry>();
 274
 0275            if (requests.Count == 0)
 0276                return new OzonCancelExecutionResult(errors, successEntries);
 277
 0278            await EnsureOzonClientsResolvedAsync(ordersById.Values);
 279
 0280            foreach (var request in requests)
 281            {
 0282                if (!ordersById.TryGetValue(request.OrderId, out var order))
 283                {
 0284                    errors.Add($"Заказ #{request.OrderId} не найден в транзакции.");
 0285                    continue;
 286                }
 287
 0288                if (!await IsOzonMarketplaceOrderAsync(order))
 289                    continue;
 290
 0291                if (string.IsNullOrWhiteSpace(order.ShipmentNumber))
 292                {
 0293                    errors.Add($"Заказ {order.ShipmentNumber ?? "#" + order.Id}: нет номера отправления.");
 0294                    continue;
 295                }
 296
 0297                var ozonClientId = ResolveOzonClientId(order);
 0298                if (!ozonClientId.HasValue)
 299                {
 0300                    errors.Add($"Заказ {order.ShipmentNumber}: не указан клиент Ozon.");
 0301                    continue;
 302                }
 303
 0304                if (request.ReasonId <= 0)
 305                {
 0306                    errors.Add($"Заказ {order.ShipmentNumber}: выберите причину отмены Ozon.");
 0307                    continue;
 308                }
 309
 0310                if (request.ReasonId == OzonOrderCancelReasonsInfo.ReasonRequiresMessageId &&
 0311                    string.IsNullOrWhiteSpace(request.ReasonMessage))
 312                {
 0313                    errors.Add(
 0314                        $"Заказ {order.ShipmentNumber}: для причины «Другое (вина продавца)» укажите комментарий.");
 0315                    continue;
 316                }
 317
 318                try
 319                {
 0320                    var ozonClient = await _ozonClientServices.GetOzonClientAsync(ozonClientId.Value);
 0321                    if (ozonClient == null)
 322                    {
 0323                        errors.Add($"Заказ {order.ShipmentNumber}: клиент Ozon не найден.");
 0324                        continue;
 325                    }
 326
 0327                    var clientId = ozonClient.DecryptClientId;
 0328                    var apiKey = ozonClient.DecryptApiKey;
 329
 0330                    if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(apiKey))
 331                    {
 0332                        errors.Add($"Заказ {order.ShipmentNumber}: не настроены учётные данные Ozon API.");
 0333                        continue;
 334                    }
 335
 0336                    var apiManager = new OzonApiDataManager(_httpClientFactory, _emailService, clientId, apiKey);
 0337                    var cancelled = await apiManager.CancelFbsPostingAsync(
 0338                        order.ShipmentNumber,
 0339                        request.ReasonId,
 0340                        request.ReasonMessage);
 341
 0342                    if (!cancelled)
 343                    {
 0344                        errors.Add($"Заказ {order.ShipmentNumber}: Ozon не подтвердил отмену отправления.");
 0345                        continue;
 346                    }
 347
 0348                    await _postingCancellationService.RecordAsync(ozonClient.Id, order.ShipmentNumber.Trim(),
 0349                        request.ReasonId, request.ReasonTitle, request.ReasonMessage, null, DateTime.UtcNow,
 0350                        "Manual", [order.Id]);
 351
 0352                    successEntries.Add(new OzonCancelSuccessEntry(
 0353                        order.ShipmentNumber.Trim(),
 0354                        request.ReasonTitle,
 0355                        request.ReasonMessage));
 0356                }
 0357                catch (Exception ex)
 358                {
 0359                    errors.Add($"Заказ {order.ShipmentNumber}: {ex.Message}");
 0360                }
 0361            }
 362
 0363            return new OzonCancelExecutionResult(errors, successEntries);
 0364        }
 365
 366        public static List<OzonCancelFormRequest> ParseFormRequests(IFormCollection form)
 367        {
 16368            var result = new List<OzonCancelFormRequest>();
 16369            var checkedOrderIds = new HashSet<int>();
 370
 82371            foreach (var key in form.Keys)
 372            {
 25373                if (!key.StartsWith("CancelInOzon[", StringComparison.Ordinal) || !key.EndsWith(']'))
 374                    continue;
 375
 16376                var idPart = key["CancelInOzon[".Length..^1];
 16377                if (!int.TryParse(idPart, out var orderId))
 378                    continue;
 379
 15380                var value = form[key].FirstOrDefault();
 15381                if (string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
 15382                    string.Equals(value, "on", StringComparison.OrdinalIgnoreCase))
 383                {
 12384                    checkedOrderIds.Add(orderId);
 385                }
 386            }
 387
 56388            foreach (var orderId in checkedOrderIds)
 389            {
 12390                long reasonId = 0;
 12391                var reasonKey = $"OzonCancelReasonId[{orderId}]";
 12392                if (form.ContainsKey(reasonKey))
 2393                    long.TryParse(form[reasonKey].FirstOrDefault(), out reasonId);
 394
 12395                string? message = null;
 12396                var messageKey = $"OzonCancelReasonMessage[{orderId}]";
 12397                if (form.ContainsKey(messageKey))
 2398                    message = form[messageKey].FirstOrDefault();
 399
 12400                string? reasonTitle = null;
 12401                var titleKey = $"OzonCancelReasonTitle[{orderId}]";
 12402                if (form.ContainsKey(titleKey))
 2403                    reasonTitle = form[titleKey].FirstOrDefault();
 404
 12405                result.Add(new OzonCancelFormRequest(orderId, reasonId, message, reasonTitle));
 406            }
 407
 16408            return result;
 409        }
 410
 411        public static string? BuildSuccessHtml(IReadOnlyList<OzonCancelSuccessEntry> entries)
 412        {
 12413            if (entries == null || entries.Count == 0)
 2414                return null;
 415
 10416            var sb = new StringBuilder();
 10417            sb.Append("<div class='tx-result-extra ozon-cancel-success-summary'>");
 10418            sb.Append("<div class='small fw-semibold mb-1'><i class='fas fa-store me-1'></i>Отмена в Ozon</div>");
 10419            sb.Append("<ul class='small mb-0 ps-3' style='list-style: disc;'>");
 42420            foreach (var entry in entries)
 421            {
 11422                sb.Append("<li>");
 11423                sb.Append(WebUtility.HtmlEncode(entry.ShipmentNumber));
 11424                if (!string.IsNullOrWhiteSpace(entry.ReasonTitle))
 425                {
 4426                    sb.Append(" — ");
 4427                    sb.Append(WebUtility.HtmlEncode(entry.ReasonTitle));
 428                }
 429
 11430                if (!string.IsNullOrWhiteSpace(entry.ReasonMessage))
 431                {
 3432                    sb.Append(" (");
 3433                    sb.Append(WebUtility.HtmlEncode(entry.ReasonMessage));
 3434                    sb.Append(')');
 435                }
 436
 11437                sb.Append("</li>");
 438            }
 439
 10440            sb.Append("</ul></div>");
 10441            return sb.ToString();
 442        }
 443
 444        /// <summary>
 445        /// Клиент Ozon из FK, навигации (в т.ч. скрытое поле формы) или данных заказа в БД.
 446        /// </summary>
 447        private async Task<bool> IsOzonMarketplaceOrderAsync(Order order)
 448        {
 0449            if (order.OzonClient?.ClientType == ClientType.OZON)
 0450                return true;
 451
 0452            var clientId = ResolveOzonClientId(order);
 0453            if (!clientId.HasValue)
 0454                return false;
 455
 456            try
 457            {
 0458                var client = await _ozonClientServices.GetOzonClientAsync(clientId.Value);
 0459                if (client == null)
 0460                    return false;
 461
 0462                order.OzonClient ??= client;
 0463                return client.ClientType == ClientType.OZON;
 464            }
 0465            catch
 466            {
 0467                return false;
 468            }
 0469        }
 470
 471        private static int? ResolveOzonClientId(Order order)
 472        {
 0473            if (order.OzonClientId is > 0)
 0474                return order.OzonClientId;
 475
 0476            if (order.OzonClient?.Id is > 0)
 0477                return order.OzonClient.Id;
 478
 0479            return null;
 480        }
 481
 482        private async Task EnsureOzonClientsResolvedAsync(IEnumerable<Order> orders)
 483        {
 0484            var orderList = orders.ToList();
 0485            if (orderList.Count == 0)
 0486                return;
 487
 0488            foreach (var order in orderList)
 489            {
 0490                var resolved = ResolveOzonClientId(order);
 0491                if (resolved.HasValue)
 0492                    order.OzonClientId = resolved;
 493            }
 494
 0495            var missingIds = orderList
 0496                .Where(o => !ResolveOzonClientId(o).HasValue)
 0497                .Select(o => o.Id)
 0498                .Distinct()
 0499                .ToList();
 500
 0501            if (missingIds.Count == 0)
 0502                return;
 503
 0504            var dbOrders = await _context.Orders
 0505                .AsNoTracking()
 0506                .Include(o => o.OzonClient)
 0507                .Where(o => missingIds.Contains(o.Id))
 0508                .ToListAsync();
 509
 0510            var dbById = dbOrders.ToDictionary(o => o.Id);
 0511            foreach (var order in orderList.Where(o => !ResolveOzonClientId(o).HasValue))
 512            {
 0513                if (!dbById.TryGetValue(order.Id, out var dbOrder))
 514                    continue;
 515
 0516                ApplyOzonClientFromSource(order, dbOrder);
 517            }
 518
 0519            var stillMissing = orderList.Where(o => !ResolveOzonClientId(o).HasValue).ToList();
 0520            if (stillMissing.Count == 0)
 0521                return;
 522
 0523            var shipmentIds = stillMissing
 0524                .Where(o => o.ShipmentId.HasValue)
 0525                .Select(o => o.ShipmentId!.Value)
 0526                .Distinct()
 0527                .ToList();
 528
 0529            if (shipmentIds.Count > 0)
 530            {
 0531                var clientByShipment = await _context.Orders
 0532                    .AsNoTracking()
 0533                    .Where(o => o.ShipmentId.HasValue &&
 0534                                shipmentIds.Contains(o.ShipmentId.Value) &&
 0535                                o.OzonClientId != null)
 0536                    .GroupBy(o => o.ShipmentId!.Value)
 0537                    .Select(g => new { ShipmentId = g.Key, OzonClientId = g.Select(x => x.OzonClientId).First() })
 0538                    .ToDictionaryAsync(x => x.ShipmentId, x => x.OzonClientId!.Value);
 539
 0540                foreach (var order in stillMissing.Where(o => o.ShipmentId.HasValue))
 541                {
 0542                    if (clientByShipment.TryGetValue(order.ShipmentId!.Value, out var clientId))
 0543                        order.OzonClientId = clientId;
 544                }
 545            }
 546
 0547            stillMissing = orderList.Where(o => !ResolveOzonClientId(o).HasValue).ToList();
 0548            var shipmentNumbers = stillMissing
 0549                .Select(o => o.ShipmentNumber?.Trim())
 0550                .Where(n => !string.IsNullOrWhiteSpace(n))
 0551                .Distinct(StringComparer.OrdinalIgnoreCase)
 0552                .ToList();
 553
 0554            if (shipmentNumbers.Count == 0)
 0555                return;
 556
 0557            var clientByPosting = await _context.Orders
 0558                .AsNoTracking()
 0559                .Where(o => o.ShipmentNumber != null &&
 0560                            o.OzonClientId != null &&
 0561                            shipmentNumbers.Contains(o.ShipmentNumber))
 0562                .GroupBy(o => o.ShipmentNumber!)
 0563                .Select(g => new { ShipmentNumber = g.Key, OzonClientId = g.Select(x => x.OzonClientId).First() })
 0564                .ToDictionaryAsync(x => x.ShipmentNumber, x => x.OzonClientId!.Value, StringComparer.OrdinalIgnoreCase);
 565
 0566            foreach (var order in stillMissing)
 567            {
 0568                var posting = order.ShipmentNumber?.Trim();
 0569                if (string.IsNullOrWhiteSpace(posting))
 570                    continue;
 571
 0572                if (clientByPosting.TryGetValue(posting, out var clientId))
 0573                    order.OzonClientId = clientId;
 574            }
 0575        }
 576
 577        private static void ApplyOzonClientFromSource(Order target, Order source)
 578        {
 0579            if (source.OzonClientId is > 0)
 0580                target.OzonClientId = source.OzonClientId;
 0581            else if (source.OzonClient?.Id is > 0)
 0582                target.OzonClientId = source.OzonClient.Id;
 583
 0584            if (target.OzonClient == null && source.OzonClient != null)
 0585                target.OzonClient = source.OzonClient;
 0586        }
 587    }
 588}