< Summary

Information
Class: Services.BitrixStockServices.BitrixStockSyncFbsWarehouseBatchLoader
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/BitrixStockSyncFbsWarehouseBatchLoader.cs
Line coverage
92%
Covered lines: 90
Uncovered lines: 7
Coverable lines: 97
Total lines: 184
Line coverage: 92.7%
Branch coverage
84%
Covered branches: 54
Total branches: 64
Branch coverage: 84.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
LoadByOfferIdsAsync()86.36%454491.78%
MergeOfferSkusFromStocks(...)80%202092.85%
GroupFbsWarehousesBySku(...)100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/BitrixStockSyncFbsWarehouseBatchLoader.cs

#LineLine coverage
 1using Newtonsoft.Json.Linq;
 2using Services.ApiServices.OzonApi;
 3
 4namespace Services.BitrixStockServices;
 5
 6/// <summary>Пакетная загрузка FBS-остатков Ozon по offer_id (общий для этапов 2 и 3).</summary>
 7internal static class BitrixStockSyncFbsWarehouseBatchLoader
 8{
 9    internal const int FbsStocksChunkSize = 200;
 10
 11    /// <summary>
 12    /// SKU на один запрос stocks-by-warehouse. Ozon отдаёт строку на каждую пару sku×склад и режет
 13    /// ответ по 1000 строк, поэтому пакет читается постранично (см. OzonApiDataManager); 300 SKU —
 14    /// это несколько страниц, а не десятки, и запас до предела пагинации.
 15    /// </summary>
 16    internal const int FbsSkuChunkSize = 300;
 17
 18    public static async Task<Dictionary<string, List<BitrixStockSyncCleanupFbsWarehouse>>> LoadByOfferIdsAsync(
 19        OzonApiDataManager apiManager,
 20        IReadOnlyList<string> offerIds,
 21        Func<string, Task>? log,
 22        CancellationToken cancellationToken)
 23    {
 224        var result = new Dictionary<string, List<BitrixStockSyncCleanupFbsWarehouse>>(StringComparer.OrdinalIgnoreCase);
 225        if (offerIds.Count == 0)
 026            return result;
 27
 228        var distinctOfferIds = offerIds
 229            .Where(id => !string.IsNullOrWhiteSpace(id))
 230            .Select(id => id.Trim())
 231            .Distinct(StringComparer.OrdinalIgnoreCase)
 232            .ToList();
 33
 234        var offerToSkus = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
 235        var stockChunks = (distinctOfferIds.Count + FbsStocksChunkSize - 1) / FbsStocksChunkSize;
 836        for (var chunkIndex = 0; chunkIndex < stockChunks; chunkIndex++)
 37        {
 238            cancellationToken.ThrowIfCancellationRequested();
 239            var chunk = distinctOfferIds
 240                .Skip(chunkIndex * FbsStocksChunkSize)
 241                .Take(FbsStocksChunkSize)
 242                .ToArray();
 243            var stocks = await apiManager.GetProductStocks(chunk);
 244            MergeOfferSkusFromStocks(stocks, offerToSkus);
 245            if (log != null && stockChunks > 1)
 046                await log($"FBS stocks: {chunkIndex + 1}/{stockChunks} пакетов");
 247            if (chunkIndex < stockChunks - 1)
 048                await Task.Delay(600, cancellationToken);
 49        }
 50
 251        var allSkus = offerToSkus.Values
 252            .SelectMany(set => set)
 253            .Distinct(StringComparer.OrdinalIgnoreCase)
 254            .ToList();
 255        if (allSkus.Count == 0)
 056            return result;
 57
 258        var skuToWarehouses = new Dictionary<string, List<BitrixStockSyncCleanupFbsWarehouse>>(StringComparer.OrdinalIgn
 259        var skuChunks = (allSkus.Count + FbsSkuChunkSize - 1) / FbsSkuChunkSize;
 860        for (var chunkIndex = 0; chunkIndex < skuChunks; chunkIndex++)
 61        {
 262            cancellationToken.ThrowIfCancellationRequested();
 263            var chunk = allSkus
 264                .Skip(chunkIndex * FbsSkuChunkSize)
 265                .Take(FbsSkuChunkSize)
 266                .ToArray();
 267            var fbsResponse = await apiManager.GetProductWarehousesForBitrix(chunk, Array.Empty<string>());
 668            foreach (var pair in GroupFbsWarehousesBySku(BitrixStockSyncReportCleanupService.ParseFbsWarehouses(fbsRespo
 69            {
 170                if (!skuToWarehouses.TryGetValue(pair.Key, out var list))
 71                {
 172                    list = new List<BitrixStockSyncCleanupFbsWarehouse>();
 173                    skuToWarehouses[pair.Key] = list;
 74                }
 75
 176                list.AddRange(pair.Value);
 77            }
 78
 279            if (log != null && skuChunks > 1)
 080                await log($"FBS warehouses: {chunkIndex + 1}/{skuChunks} пакетов");
 281            if (chunkIndex < skuChunks - 1)
 082                await Task.Delay(600, cancellationToken);
 83        }
 84
 85        // Ozon иногда не возвращает часть sku в ответе (временный флап API) — такой sku тихо
 86        // остаётся без склада, а обнуление снятых связей падает в report-fallback, который не
 87        // знает про склады вне Stage 1 (напр. региональные FBS-склады) и считает остаток нулевым.
 88        // Переспрашиваем один раз маленькими пакетами только пропущенные sku.
 489        var missingSkus = allSkus.Where(sku => !skuToWarehouses.ContainsKey(sku)).ToList();
 290        if (missingSkus.Count > 0)
 91        {
 192            if (log != null)
 193                await log($"FBS warehouses: повтор для {missingSkus.Count} sku без ответа Ozon…");
 94
 95            const int retryChunkSize = 200;
 196            var retryChunks = (missingSkus.Count + retryChunkSize - 1) / retryChunkSize;
 497            for (var chunkIndex = 0; chunkIndex < retryChunks; chunkIndex++)
 98            {
 199                cancellationToken.ThrowIfCancellationRequested();
 1100                var chunk = missingSkus
 1101                    .Skip(chunkIndex * retryChunkSize)
 1102                    .Take(retryChunkSize)
 1103                    .ToArray();
 1104                await Task.Delay(600, cancellationToken);
 1105                var fbsResponse = await apiManager.GetProductWarehousesForBitrix(chunk, Array.Empty<string>());
 4106                foreach (var pair in GroupFbsWarehousesBySku(BitrixStockSyncReportCleanupService.ParseFbsWarehouses(fbsR
 107                {
 1108                    if (!skuToWarehouses.TryGetValue(pair.Key, out var list))
 109                    {
 1110                        list = new List<BitrixStockSyncCleanupFbsWarehouse>();
 1111                        skuToWarehouses[pair.Key] = list;
 112                    }
 113
 1114                    list.AddRange(pair.Value);
 115                }
 1116            }
 117
 1118            if (log != null)
 119            {
 2120                var stillMissing = missingSkus.Count(sku => !skuToWarehouses.ContainsKey(sku));
 1121                await log($"FBS warehouses: повтор восстановил {missingSkus.Count - stillMissing}/{missingSkus.Count} sk
 122            }
 123        }
 124
 8125        foreach (var (offerId, skus) in offerToSkus)
 126        {
 2127            var merged = new Dictionary<long, BitrixStockSyncCleanupFbsWarehouse>();
 8128            foreach (var sku in skus)
 129            {
 2130                if (!skuToWarehouses.TryGetValue(sku, out var warehouses))
 131                    continue;
 132
 8133                foreach (var warehouse in warehouses)
 2134                    merged[warehouse.WarehouseId] = warehouse;
 135            }
 136
 2137            result[offerId] = merged.Values
 2138                .OrderBy(w => w.WarehouseName, StringComparer.OrdinalIgnoreCase)
 2139                .ToList();
 140        }
 141
 2142        return result;
 2143    }
 144
 145    private static void MergeOfferSkusFromStocks(
 146        JObject? stocks,
 147        IDictionary<string, HashSet<string>> offerToSkus)
 148    {
 2149        if (stocks?["items"] is not JArray items)
 0150            return;
 151
 8152        foreach (var stockItem in items)
 153        {
 2154            var offerId = stockItem["offer_id"]?.ToString();
 2155            if (string.IsNullOrWhiteSpace(offerId))
 156                continue;
 157
 2158            if (!offerToSkus.TryGetValue(offerId, out var skus))
 159            {
 2160                skus = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 2161                offerToSkus[offerId] = skus;
 162            }
 163
 2164            if (stockItem["stocks"] is not JArray stockRows)
 165                continue;
 166
 8167            foreach (var row in stockRows)
 168            {
 2169                var sku = row["sku"]?.ToString();
 2170                if (!string.IsNullOrWhiteSpace(sku))
 2171                    skus.Add(sku.Trim());
 172            }
 173        }
 2174    }
 175
 176    private static IEnumerable<KeyValuePair<string, List<BitrixStockSyncCleanupFbsWarehouse>>> GroupFbsWarehousesBySku(
 177        IEnumerable<BitrixStockSyncCleanupFbsWarehouse> warehouses)
 178    {
 3179        return warehouses
 2180            .Where(w => w.Sku > 0)
 2181            .GroupBy(w => w.Sku.ToString(), StringComparer.OrdinalIgnoreCase)
 5182            .Select(g => new KeyValuePair<string, List<BitrixStockSyncCleanupFbsWarehouse>>(g.Key, g.ToList()));
 183    }
 184}