< Summary

Information
Class: Services.BackgroundJobsServices.WarehouseSelectionResolver
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BackgroundJobsServices/WarehouseSelectionResolver.cs
Line coverage
65%
Covered lines: 246
Uncovered lines: 129
Coverable lines: 375
Total lines: 612
Line coverage: 65.6%
Branch coverage
49%
Covered branches: 121
Total branches: 244
Branch coverage: 49.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BackgroundJobsServices/WarehouseSelectionResolver.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using OzonDomains.Models;
 3using OzonRepositories.Context;
 4
 5namespace Services.BackgroundJobsServices;
 6
 7public sealed class WarehouseSelectionResolver
 8{
 9    private const string ZasylKey = "Засыл";
 10
 11    public sealed class SenderWarehouseResolution
 12    {
 013        public string? SenderWarehouseName { get; set; }
 014        public List<string> Errors { get; } = new();
 015        public bool HasErrors => Errors.Count > 0;
 16    }
 17
 18    public sealed class RecipientWarehouseResolution
 19    {
 720        public List<string> PreferredRecipientWarehouses { get; } = new();
 321        public List<string> Warnings { get; } = new();
 222        public List<string> BlockingErrors { get; } = new();
 023        public bool HasBlockingErrors => BlockingErrors.Count > 0;
 24    }
 25
 26    public sealed class RecipientByOrderResolution
 27    {
 3428        public Dictionary<int, string> RecipientByOrderId { get; } = new();
 2129        public Dictionary<int, string> RecipientExplanationByOrderId { get; } = new();
 1930        public HashSet<int> ReturnWarehouseOrderIds { get; } = [];
 1431        public List<string> Warnings { get; } = new();
 3732        public List<string> BlockingErrors { get; } = new();
 1433        public bool HasBlockingErrors => BlockingErrors.Count > 0;
 34    }
 35
 36    /// <summary>Вариант склада возврата для введённого в форме засыла.</summary>
 337    public sealed record CustomerReturnZasylOption(
 238        string Warehouse,
 139        string Explanation,
 440        bool UsesReturnWarehouse);
 41
 42    /// <summary>
 43    /// Варианты засыла одного заказа для браузера: <see cref="Fallback"/> применяется при пустом
 44    /// поле «Засыл», <see cref="Options"/> — при вводе конкретного значения.
 45    /// </summary>
 146    public sealed record CustomerReturnZasylOrderOptions(
 147        Dictionary<string, CustomerReturnZasylOption> Options,
 248        CustomerReturnZasylOption? Fallback);
 49
 50    public sealed class CommissionerSenderWarehouseResolution
 51    {
 1052        public Dictionary<int, string> WarehouseByOrderId { get; } = new();
 1053        public List<string> BlockingErrors { get; } = new();
 454        public bool HasBlockingErrors => BlockingErrors.Count > 0;
 55    }
 56
 57    private readonly ApplicationDbContext _context;
 58
 2159    public WarehouseSelectionResolver(ApplicationDbContext context)
 60    {
 2161        _context = context;
 2162    }
 63
 64    public async Task<SenderWarehouseResolution> ResolveSenderWarehouseAsync(IEnumerable<Order> orders)
 65    {
 066        var resolution = new SenderWarehouseResolution();
 067        var sourceOrders = orders?.ToList() ?? new List<Order>();
 68
 069        if (!sourceOrders.Any())
 70        {
 071            resolution.Errors.Add("Не выбраны заказы для определения склада отправителя.");
 072            return resolution;
 73        }
 74
 075        var missingClientOrderIds = sourceOrders
 076            .Where(o => o.OzonClient == null || string.IsNullOrWhiteSpace(o.OzonClient.WarehouseName))
 077            .Select(o => o.Id)
 078            .Distinct()
 079            .ToList();
 80
 081        if (missingClientOrderIds.Count > 0)
 82        {
 083            var dbOrders = await _context.Orders
 084                .AsNoTracking()
 085                .Include(o => o.OzonClient)
 086                .Where(o => missingClientOrderIds.Contains(o.Id))
 087                .ToListAsync();
 088            var dbById = dbOrders.ToDictionary(o => o.Id);
 89
 090            foreach (var order in sourceOrders.Where(o => missingClientOrderIds.Contains(o.Id)))
 91            {
 092                if (dbById.TryGetValue(order.Id, out var dbOrder))
 93                {
 094                    order.OzonClient = dbOrder.OzonClient;
 95                }
 96            }
 97        }
 98
 099        var resolvedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 0100        foreach (var order in sourceOrders)
 101        {
 0102            var senderWarehouseName = order.OzonClient?.WarehouseName?.Trim();
 0103            if (string.IsNullOrWhiteSpace(senderWarehouseName))
 104            {
 0105                resolution.Errors.Add(
 0106                    $"Заказ {order.ShipmentNumber}: у клиента Ozon не заполнено поле 'Название склада'.");
 0107                continue;
 108            }
 109
 0110            resolvedNames.Add(senderWarehouseName);
 111        }
 112
 0113        if (resolvedNames.Count > 1)
 114        {
 0115            resolution.Errors.Add(
 0116                $"Невозможно выбрать единый склад-источник: по выбранным заказам получены разные склады ({string.Join(",
 0117            return resolution;
 118        }
 119
 0120        if (resolvedNames.Count == 1 && !resolution.HasErrors)
 121        {
 0122            resolution.SenderWarehouseName = resolvedNames.First();
 123        }
 124
 0125        return resolution;
 0126    }
 127
 128    public async Task<RecipientWarehouseResolution> ResolveRecipientWarehousesAsync(IEnumerable<Order> orders)
 129    {
 2130        var result = new RecipientWarehouseResolution();
 2131        var sourceOrders = orders?.ToList() ?? new List<Order>();
 2132        if (!sourceOrders.Any())
 133        {
 0134            return result;
 135        }
 136
 2137        var missingOrderIds = sourceOrders
 2138            .Where(o => o.OzonClient == null ||
 2139                        string.IsNullOrWhiteSpace(o.OzonClient.Name) ||
 2140                        o.ShipmentWarehouse == null ||
 2141                        string.IsNullOrWhiteSpace(o.ShipmentWarehouse.Name))
 2142            .Select(o => o.Id)
 2143            .Distinct()
 2144            .ToList();
 145
 2146        if (missingOrderIds.Count > 0)
 147        {
 2148            var dbOrders = await _context.Orders
 2149                .AsNoTracking()
 2150                .Include(o => o.OzonClient)
 2151                .Include(o => o.ShipmentWarehouse)
 2152                .Where(o => missingOrderIds.Contains(o.Id))
 2153                .ToListAsync();
 2154            var byId = dbOrders.ToDictionary(o => o.Id);
 10155            foreach (var order in sourceOrders.Where(o => missingOrderIds.Contains(o.Id)))
 156            {
 2157                if (byId.TryGetValue(order.Id, out var dbOrder))
 158                {
 0159                    order.OzonClient = dbOrder.OzonClient;
 0160                    order.ShipmentWarehouse = dbOrder.ShipmentWarehouse;
 161                }
 162            }
 163        }
 164
 2165        var mappings = await _context.WarehouseMappings.AsNoTracking().ToListAsync();
 2166        var preferred = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 167
 2168        var uniquePairs = sourceOrders
 2169            .Select(o => new
 2170            {
 2171                ClientId = o.OzonClientId,
 2172                Warehouse = o.ShipmentWarehouse?.Name?.Trim()
 2173            })
 2174            .Where(x => x.ClientId.HasValue && !string.IsNullOrWhiteSpace(x.Warehouse))
 2175            .Distinct()
 2176            .ToList();
 177
 2178        var missingPairData = sourceOrders.Any(o =>
 4179            !o.OzonClientId.HasValue || string.IsNullOrWhiteSpace(o.ShipmentWarehouse?.Name));
 180
 2181        if (missingPairData)
 182        {
 0183            result.Warnings.Add(
 0184                "Для части заказов не заполнены клиент маркетплейса или склад отгрузки — приоритетный склад получателя н
 185        }
 186
 2187        if (uniquePairs.Count > 1)
 188        {
 0189            result.Warnings.Add(
 0190                "В набор попали заказы с разными комбинациями клиента маркетплейса и склада отгрузки — приоритетный скла
 191        }
 192
 8193        foreach (var order in sourceOrders)
 194        {
 2195            var clientId = order.OzonClientId;
 2196            var ozonWarehouseName = order.ShipmentWarehouse?.Name?.Trim();
 197
 2198            if (!clientId.HasValue || string.IsNullOrWhiteSpace(ozonWarehouseName))
 199            {
 0200                result.Warnings.Add(
 0201                    $"Заказ {order.ShipmentNumber}: не удалось определить приоритетный склад получателя (клиент/склад за
 0202                continue;
 203            }
 204
 2205            var candidates = mappings
 2206                .Where(m =>
 5207                    !string.IsNullOrWhiteSpace(m.OzonWarehouseName) &&
 5208                    !string.IsNullOrWhiteSpace(m.BitrixWarehouseName) &&
 5209                    m.OzonClientId.HasValue &&
 5210                    m.OzonClientId.Value == clientId.Value &&
 5211                    string.Equals(m.OzonWarehouseName.Trim(), ozonWarehouseName, StringComparison.OrdinalIgnoreCase))
 5212                .OrderBy(m => m.Priority)
 5213                .ThenBy(m => m.Id)
 5214                .Select(m => m.BitrixWarehouseName!.Trim())
 2215                .Distinct(StringComparer.OrdinalIgnoreCase)
 2216                .ToList();
 217
 2218            if (candidates.Count == 0)
 219            {
 0220                result.Warnings.Add(
 0221                    $"Заказ {order.ShipmentNumber}: для клиента (Id={clientId}) и склада '{ozonWarehouseName}' не найден
 0222                continue;
 223            }
 224
 12225            foreach (var candidate in candidates)
 226            {
 4227                preferred.Add(candidate);
 228            }
 229        }
 230
 6231        result.PreferredRecipientWarehouses.AddRange(preferred.OrderBy(x => x));
 2232        return result;
 2233    }
 234
 235    public async Task<RecipientByOrderResolution> ResolveRecipientByOrderAsync(IEnumerable<Order> orders)
 236    {
 9237        var result = new RecipientByOrderResolution();
 9238        var sourceOrders = orders?.ToList() ?? new List<Order>();
 9239        if (!sourceOrders.Any())
 240        {
 1241            result.BlockingErrors.Add("Не выбраны заказы для определения склада получателя.");
 1242            return result;
 243        }
 244
 8245        var missingOrderIds = sourceOrders
 9246            .Where(o => o.ShipmentWarehouse == null || string.IsNullOrWhiteSpace(o.ShipmentWarehouse.Name))
 0247            .Select(o => o.Id)
 8248            .Distinct()
 8249            .ToList();
 250
 8251        if (missingOrderIds.Count > 0)
 252        {
 0253            var dbOrders = await _context.Orders
 0254                .AsNoTracking()
 0255                .Include(o => o.ShipmentWarehouse)
 0256                .Include(o => o.OzonClient)
 0257                .Where(o => missingOrderIds.Contains(o.Id))
 0258                .ToListAsync();
 0259            var byId = dbOrders.ToDictionary(o => o.Id);
 0260            foreach (var order in sourceOrders.Where(o => missingOrderIds.Contains(o.Id)))
 261            {
 0262                if (byId.TryGetValue(order.Id, out var dbOrder))
 263                {
 0264                    order.ShipmentWarehouse = dbOrder.ShipmentWarehouse;
 0265                    order.OzonClient ??= dbOrder.OzonClient;
 266                }
 267            }
 268        }
 269
 8270        var mappings = await _context.WarehouseMappings.AsNoTracking().ToListAsync();
 34271        foreach (var order in sourceOrders)
 272        {
 9273            var clientId = order.OzonClientId;
 9274            var ozonWarehouseName = order.ShipmentWarehouse?.Name?.Trim();
 9275            if (!clientId.HasValue || string.IsNullOrWhiteSpace(ozonWarehouseName))
 276            {
 1277                result.BlockingErrors.Add(
 1278                    $"Заказ {order.ShipmentNumber}: не заполнены данные для определения склада получателя.");
 1279                continue;
 280            }
 281
 8282            var best = mappings
 8283                .Where(m =>
 12284                    m.OzonClientId.HasValue &&
 12285                    m.OzonClientId.Value == clientId.Value &&
 12286                    !string.IsNullOrWhiteSpace(m.OzonWarehouseName) &&
 12287                    !string.IsNullOrWhiteSpace(m.BitrixWarehouseName) &&
 12288                    string.Equals(m.OzonWarehouseName.Trim(), ozonWarehouseName, StringComparison.OrdinalIgnoreCase))
 8289                .OrderBy(m => m.Priority)
 8290                .ThenBy(m => m.Id)
 8291                .FirstOrDefault();
 292
 8293            if (best == null)
 294            {
 2295                result.BlockingErrors.Add(
 2296                    $"Заказ {order.ShipmentNumber}: не найден маппинг получателя для склада '{ozonWarehouseName}'.");
 2297                continue;
 298            }
 299
 6300            result.RecipientByOrderId[order.Id] = best.BitrixWarehouseName!.Trim();
 301        }
 302
 8303        return result;
 9304    }
 305
 306    /// <summary>
 307    /// Склад-получатель для «Возврата покупателя»: выделенный склад возврата из WarehouseMapping,
 308    /// а при его отсутствии — обычный склад 1С из той же связи.
 309    /// </summary>
 310    public async Task<RecipientByOrderResolution> ResolveCustomerReturnRecipientByOrderAsync(
 311        IEnumerable<Order> orders)
 312    {
 5313        var result = new RecipientByOrderResolution();
 5314        var sourceOrders = orders?.ToList() ?? new List<Order>();
 5315        if (!sourceOrders.Any())
 316        {
 0317            result.BlockingErrors.Add("Не выбраны заказы для определения склада получателя.");
 0318            return result;
 319        }
 320
 5321        var missingOrderIds = sourceOrders
 5322            .Where(o => o.ShipmentWarehouse == null || string.IsNullOrWhiteSpace(o.ShipmentWarehouse.Name))
 0323            .Select(o => o.Id)
 5324            .Distinct()
 5325            .ToList();
 326
 5327        if (missingOrderIds.Count > 0)
 328        {
 0329            var dbOrders = await _context.Orders
 0330                .AsNoTracking()
 0331                .Include(o => o.ShipmentWarehouse)
 0332                .Include(o => o.OzonClient)
 0333                .Where(o => missingOrderIds.Contains(o.Id))
 0334                .ToListAsync();
 0335            var byId = dbOrders.ToDictionary(o => o.Id);
 0336            foreach (var order in sourceOrders.Where(o => missingOrderIds.Contains(o.Id)))
 337            {
 0338                if (byId.TryGetValue(order.Id, out var dbOrder))
 339                {
 0340                    order.ShipmentWarehouse = dbOrder.ShipmentWarehouse;
 0341                    order.OzonClient ??= dbOrder.OzonClient;
 342                }
 343            }
 344        }
 345
 5346        var mappings = await _context.WarehouseMappings.AsNoTracking().ToListAsync();
 20347        foreach (var order in sourceOrders)
 348        {
 5349            var clientId = order.OzonClientId;
 5350            var zasyl = order.OrderExtensions
 1351                .FirstOrDefault(extension => string.Equals(extension.Key, ZasylKey, StringComparison.OrdinalIgnoreCase))
 5352                ?.Value?
 5353                .Trim();
 5354            var ozonWarehouseName = WarehouseNameNormalizer.Normalize(
 5355                !string.IsNullOrWhiteSpace(zasyl) ? zasyl : order.ShipmentWarehouse?.Name);
 5356            if (!clientId.HasValue || string.IsNullOrWhiteSpace(ozonWarehouseName))
 357            {
 0358                result.BlockingErrors.Add(
 0359                    $"Заказ {order.ShipmentNumber}: не заполнены данные для определения склада получателя.");
 0360                continue;
 361            }
 362
 5363            var best = mappings
 5364                .Where(m =>
 6365                    m.OzonClientId.HasValue &&
 6366                    m.OzonClientId.Value == clientId.Value &&
 6367                    !string.IsNullOrWhiteSpace(m.OzonWarehouseName) &&
 6368                    !string.IsNullOrWhiteSpace(m.BitrixWarehouseName) &&
 6369                    string.Equals(WarehouseNameNormalizer.Normalize(m.OzonWarehouseName), ozonWarehouseName,
 6370                        StringComparison.OrdinalIgnoreCase))
 5371                .OrderBy(m => m.Priority)
 5372                .ThenBy(m => m.Id)
 5373                .FirstOrDefault();
 374
 5375            if (best == null)
 376            {
 1377                result.BlockingErrors.Add(
 1378                    $"Заказ {order.ShipmentNumber}: не найден маппинг получателя для склада '{ozonWarehouseName}'.");
 1379                continue;
 380            }
 381
 4382            var returnWarehouse = best.ReturnBitrixWarehouseName?.Trim();
 4383            var mappingSource = !string.IsNullOrWhiteSpace(zasyl)
 4384                ? $" по засылу {ozonWarehouseName}"
 4385                : string.Empty;
 4386            if (!string.IsNullOrWhiteSpace(returnWarehouse))
 387            {
 3388                result.RecipientByOrderId[order.Id] = returnWarehouse;
 3389                result.RecipientExplanationByOrderId[order.Id] =
 3390                    $"Склад возврата из связи{mappingSource}: {returnWarehouse}";
 3391                result.ReturnWarehouseOrderIds.Add(order.Id);
 3392                continue;
 393            }
 394
 1395            var primaryWarehouse = best.BitrixWarehouseName!.Trim();
 1396            result.RecipientByOrderId[order.Id] = primaryWarehouse;
 1397            result.RecipientExplanationByOrderId[order.Id] =
 1398                $"Основной склад из связи{mappingSource}: {primaryWarehouse} — склад возврата не задан.";
 399        }
 400
 5401        return result;
 5402    }
 403
 404    /// <summary>
 405    /// Строит для страницы варианты склада возврата при изменении «Засыла». Правило идентично
 406    /// <see cref="ResolveCustomerReturnRecipientByOrderAsync"/>: засыл заменяет склад отгрузки,
 407    /// затем выбирается связь с минимальными Priority и Id.
 408    /// </summary>
 409    public async Task<Dictionary<string, CustomerReturnZasylOrderOptions>>
 410        BuildCustomerReturnZasylOptionsAsync(IEnumerable<Order> orders, IEnumerable<string?>? warehouseNames)
 411    {
 1412        var result = new Dictionary<string, CustomerReturnZasylOrderOptions>();
 1413        var sourceOrders = orders?.ToList() ?? [];
 1414        if (sourceOrders.Count == 0)
 0415            return result;
 416
 1417        var mappings = await _context.WarehouseMappings.AsNoTracking().ToListAsync();
 1418        var names = (warehouseNames ?? Enumerable.Empty<string?>())
 0419            .Where(name => !string.IsNullOrWhiteSpace(name))
 0420            .Select(name => name!.Trim())
 1421            .ToList();
 422
 4423        foreach (var order in sourceOrders)
 424        {
 1425            if (!order.OzonClientId.HasValue)
 426                continue;
 427
 1428            var orderNames = names
 2429                .Concat(mappings.Where(mapping => mapping.OzonClientId == order.OzonClientId)
 2430                    .Select(mapping => mapping.OzonWarehouseName))
 1431                .Append(order.ShipmentWarehouse?.Name)
 1432                .Append(order.OrderExtensions?
 0433                    .FirstOrDefault(extension => string.Equals(extension.Key, ZasylKey, StringComparison.OrdinalIgnoreCa
 1434                    ?.Value)
 4435                .Where(name => !string.IsNullOrWhiteSpace(name))
 3436                .Select(name => name!.Trim())
 1437                .Distinct(StringComparer.OrdinalIgnoreCase)
 1438                .ToList();
 439
 440            CustomerReturnZasylOption? Resolve(string? sourceWarehouse, bool fromZasyl)
 441            {
 3442                var normalized = WarehouseNameNormalizer.Normalize(sourceWarehouse);
 3443                if (string.IsNullOrWhiteSpace(normalized))
 0444                    return null;
 445
 3446                var mapping = mappings
 6447                    .Where(candidate => candidate.OzonClientId == order.OzonClientId &&
 6448                                        !string.IsNullOrWhiteSpace(candidate.OzonWarehouseName) &&
 6449                                        !string.IsNullOrWhiteSpace(candidate.BitrixWarehouseName) &&
 6450                                        string.Equals(WarehouseNameNormalizer.Normalize(candidate.OzonWarehouseName), no
 6451                                            StringComparison.OrdinalIgnoreCase))
 3452                    .OrderBy(candidate => candidate.Priority)
 3453                    .ThenBy(candidate => candidate.Id)
 3454                    .FirstOrDefault();
 3455                if (mapping == null)
 0456                    return null;
 457
 3458                var returnWarehouse = mapping.ReturnBitrixWarehouseName?.Trim();
 3459                var sourceSuffix = fromZasyl ? $" по засылу {normalized}" : string.Empty;
 3460                if (!string.IsNullOrWhiteSpace(returnWarehouse))
 461                {
 1462                    return new CustomerReturnZasylOption(
 1463                        returnWarehouse,
 1464                        $"Склад возврата из связи{sourceSuffix}: {returnWarehouse}",
 1465                        true);
 466                }
 467
 2468                var primaryWarehouse = mapping.BitrixWarehouseName!.Trim();
 2469                return new CustomerReturnZasylOption(
 2470                    primaryWarehouse,
 2471                    $"Основной склад из связи{sourceSuffix}: {primaryWarehouse} — склад возврата не задан.",
 2472                    false);
 473            }
 474
 1475            var options = new Dictionary<string, CustomerReturnZasylOption>(StringComparer.OrdinalIgnoreCase);
 6476            foreach (var name in orderNames)
 477            {
 2478                var key = WarehouseNameNormalizer.Normalize(name)?.ToLowerInvariant();
 2479                var option = Resolve(name, fromZasyl: true);
 2480                if (key != null && option != null)
 2481                    options.TryAdd(key, option);
 482            }
 483
 1484            result[order.Id.ToString()] = new CustomerReturnZasylOrderOptions(
 1485                options,
 1486                Resolve(order.ShipmentWarehouse?.Name, fromZasyl: false));
 487        }
 488
 1489        return result;
 1490    }
 491
 492    /// <summary>
 493    /// Склад отправителя для отгрузки комиссионеру: BitrixWarehouseName из связки складов (клиент + склад заказа).
 494    /// </summary>
 495    public async Task<CommissionerSenderWarehouseResolution> ResolveCommissionerSenderWarehousesAsync(
 496        IEnumerable<Order> orders)
 497    {
 4498        var result = new CommissionerSenderWarehouseResolution();
 4499        var sourceOrders = orders?.ToList() ?? new List<Order>();
 4500        if (!sourceOrders.Any())
 501        {
 0502            result.BlockingErrors.Add("Не выбраны заказы для определения склада отправителя.");
 0503            return result;
 504        }
 505
 4506        var missingOrderIds = sourceOrders
 4507            .Where(o => o.ShipmentWarehouse == null || string.IsNullOrWhiteSpace(o.ShipmentWarehouse.Name))
 1508            .Select(o => o.Id)
 4509            .Distinct()
 4510            .ToList();
 511
 4512        if (missingOrderIds.Count > 0)
 513        {
 1514            var dbOrders = await _context.Orders
 1515                .AsNoTracking()
 1516                .Include(o => o.ShipmentWarehouse)
 1517                .Include(o => o.OzonClient)
 1518                .Include(o => o.OrderExtensions)
 1519                .Where(o => missingOrderIds.Contains(o.Id))
 1520                .ToListAsync();
 1521            var byId = dbOrders.ToDictionary(o => o.Id);
 5522            foreach (var order in sourceOrders.Where(o => missingOrderIds.Contains(o.Id)))
 523            {
 1524                if (byId.TryGetValue(order.Id, out var dbOrder))
 525                {
 0526                    order.ShipmentWarehouse = dbOrder.ShipmentWarehouse;
 0527                    order.OzonClient ??= dbOrder.OzonClient;
 0528                    order.OrderExtensions = dbOrder.OrderExtensions;
 529                }
 530            }
 531        }
 532
 4533        var mappings = await _context.WarehouseMappings.AsNoTracking().ToListAsync();
 16534        foreach (var order in sourceOrders)
 535        {
 4536            var warehouse = ShippedToCommissionerWarehouseResolver.ResolveSenderWarehouse(order, mappings);
 4537            if (string.IsNullOrWhiteSpace(warehouse))
 538            {
 1539                result.BlockingErrors.Add(
 1540                    $"Заказ {order.ShipmentNumber}: не заполнен склад отгрузки и не найдена связка WarehouseMapping.");
 1541                continue;
 542            }
 543
 3544            result.WarehouseByOrderId[order.Id] = warehouse;
 545        }
 546
 4547        return result;
 4548    }
 549
 550    public async Task<SenderWarehouseResolution> ResolveSenderWarehouseFromShipmentAsync(IEnumerable<Order> orders)
 551    {
 0552        var resolution = new SenderWarehouseResolution();
 0553        var sourceOrders = orders?.ToList() ?? new List<Order>();
 554
 0555        if (!sourceOrders.Any())
 556        {
 0557            resolution.Errors.Add("Не выбраны заказы для определения склада отправителя.");
 0558            return resolution;
 559        }
 560
 0561        var missingOrderIds = sourceOrders
 0562            .Where(o => o.ShipmentWarehouse == null || string.IsNullOrWhiteSpace(o.ShipmentWarehouse.Name))
 0563            .Select(o => o.Id)
 0564            .Distinct()
 0565            .ToList();
 566
 0567        if (missingOrderIds.Count > 0)
 568        {
 0569            var dbOrders = await _context.Orders
 0570                .AsNoTracking()
 0571                .Include(o => o.ShipmentWarehouse)
 0572                .Where(o => missingOrderIds.Contains(o.Id))
 0573                .ToListAsync();
 0574            var byId = dbOrders.ToDictionary(o => o.Id);
 0575            foreach (var order in sourceOrders.Where(o => missingOrderIds.Contains(o.Id)))
 576            {
 0577                if (byId.TryGetValue(order.Id, out var dbOrder))
 578                {
 0579                    order.ShipmentWarehouse = dbOrder.ShipmentWarehouse;
 580                }
 581            }
 582        }
 583
 0584        var resolvedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 0585        foreach (var order in sourceOrders)
 586        {
 0587            var shipmentWarehouseName = order.ShipmentWarehouse?.Name?.Trim();
 0588            if (string.IsNullOrWhiteSpace(shipmentWarehouseName))
 589            {
 0590                resolution.Errors.Add(
 0591                    $"Заказ {order.ShipmentNumber}: не заполнено поле 'Склад отгрузки'.");
 0592                continue;
 593            }
 594
 0595            resolvedNames.Add(shipmentWarehouseName);
 596        }
 597
 0598        if (resolvedNames.Count > 1)
 599        {
 0600            resolution.Errors.Add(
 0601                $"Невозможно выбрать единый склад-источник: по выбранным заказам получены разные склады ({string.Join(",
 0602            return resolution;
 603        }
 604
 0605        if (resolvedNames.Count == 1 && !resolution.HasErrors)
 606        {
 0607            resolution.SenderWarehouseName = resolvedNames.First();
 608        }
 609
 0610        return resolution;
 0611    }
 612}