< Summary

Information
Class: Services.BitrixStockServices.OzonCatalogStockEnricher
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/OzonCatalogStockEnricher.cs
Line coverage
89%
Covered lines: 69
Uncovered lines: 8
Coverable lines: 77
Total lines: 155
Line coverage: 89.6%
Branch coverage
66%
Covered branches: 48
Total branches: 72
Branch coverage: 66.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
EnrichBatchAsync()67.64%746889.06%
ParseOfferId(...)50%4485.71%

File(s)

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

#LineLine coverage
 1using System.Text.RegularExpressions;
 2using Newtonsoft.Json.Linq;
 3using Services.ApiServices.OzonApi;
 4using Services.ApiServices.OzonApi.Filters;
 5
 6namespace Services.BitrixStockServices;
 7
 8public interface IOzonCatalogStockEnricher
 9{
 10    /// <summary>
 11    /// Обогащает батч каталога остатками по целевым FBS/RFBS-складам через Ozon API.
 12    /// Возвращает строки с AvailableQty &gt; 0 на одном из targetWarehouseIds.
 13    /// </summary>
 14    Task<IReadOnlyList<OzonCatalogStockRow>> EnrichBatchAsync(
 15        OzonApiDataManager apiManager,
 16        IReadOnlyList<OzonProductCatalogRow> catalogBatch,
 17        IReadOnlyCollection<long> targetWarehouseIds,
 18        IReadOnlyDictionary<long, string> warehouseNameById,
 19        List<WarehouseOzon> warehouseList,
 20        Func<string, Task>? log = null,
 21        CancellationToken cancellationToken = default);
 22}
 23
 24public class OzonCatalogStockEnricher : IOzonCatalogStockEnricher
 25{
 126    private static readonly Regex BrandCodeRegex =
 127        new(@"^([A-Z0-9]{2,5})(?:\[.*\])?$", RegexOptions.Compiled);
 28
 29    public async Task<IReadOnlyList<OzonCatalogStockRow>> EnrichBatchAsync(
 30        OzonApiDataManager apiManager,
 31        IReadOnlyList<OzonProductCatalogRow> catalogBatch,
 32        IReadOnlyCollection<long> targetWarehouseIds,
 33        IReadOnlyDictionary<long, string> warehouseNameById,
 34        List<WarehouseOzon> warehouseList,
 35        Func<string, Task>? log = null,
 36        CancellationToken cancellationToken = default)
 37    {
 738        if (catalogBatch.Count == 0 || targetWarehouseIds.Count == 0)
 239            return Array.Empty<OzonCatalogStockRow>();
 40
 541        var targetSet = targetWarehouseIds.ToHashSet();
 542        var catalogByOffer = catalogBatch
 543            .Where(c => !string.IsNullOrWhiteSpace(c.OfferId))
 544            .GroupBy(c => c.OfferId, StringComparer.OrdinalIgnoreCase)
 1545            .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
 46
 547        var offerIds = catalogByOffer.Keys.ToArray();
 548        if (log != null)
 049            await log($"  Ozon API POST /v4/product/info/stocks: {offerIds.Length} offer_id (chunk 200)").ConfigureAwait
 50
 551        var stocksResponse = await apiManager.GetProductStocks(offerIds, chunkSize: 200).ConfigureAwait(false);
 552        if (stocksResponse["items"] is not JArray stockItems || stockItems.Count == 0)
 53        {
 054            if (log != null)
 055                await log($"  Ozon API stocks: 0 товаров с данными в ответе").ConfigureAwait(false);
 056            return Array.Empty<OzonCatalogStockRow>();
 57        }
 58
 559        var itemsWithStock = new List<(string OfferId, JObject Stocks)>();
 2060        foreach (var stockItem in stockItems.OfType<JObject>())
 61        {
 562            cancellationToken.ThrowIfCancellationRequested();
 563            var offerId = stockItem["offer_id"]?.ToString();
 564            if (string.IsNullOrWhiteSpace(offerId))
 65                continue;
 66
 567            var (fbs, rfbs, _) = BitrixOzonStockLinkage.ExtractStocksByType(new JObject { ["items"] = new JArray { stock
 568            if (fbs <= 0 && rfbs <= 0)
 69                continue;
 70
 471            itemsWithStock.Add((offerId, new JObject { ["items"] = new JArray { stockItem } }));
 72        }
 73
 574        if (log != null)
 075            await log($"  Ozon API stocks: FBS>0 или RFBS>0 у {itemsWithStock.Count} из {offerIds.Length} offer_id").Con
 76
 577        if (itemsWithStock.Count == 0)
 178            return Array.Empty<OzonCatalogStockRow>();
 79
 480        var batched = itemsWithStock
 481            .Select(x => (x.OfferId, x.Stocks))
 482            .ToList();
 83
 484        if (log != null)
 085            await log($"  Ozon API POST stocks-by-warehouse: {batched.Count} товар(ов)").ConfigureAwait(false);
 86
 487        var warehouseInfoByOffer = await apiManager.GetWarehouseInfoBatchedAsync(batched, warehouseList)
 488            .ConfigureAwait(false);
 89
 490        var result = new List<OzonCatalogStockRow>();
 1691        foreach (var (offerId, warehouseInfo) in warehouseInfoByOffer)
 92        {
 493            cancellationToken.ThrowIfCancellationRequested();
 494            if (warehouseInfo == null)
 95                continue;
 96
 497            catalogByOffer.TryGetValue(offerId, out var catalogRow);
 498            var warehouses = BitrixOzonStockLinkage.GetWarehousesArray(warehouseInfo);
 1699            foreach (var wh in warehouses.OfType<JObject>())
 100            {
 4101                var warehouseId = wh["warehouse_id"]?.Value<long?>() ?? 0;
 4102                if (warehouseId <= 0 || !targetSet.Contains(warehouseId))
 103                    continue;
 104
 4105                var present = wh["present"]?.Value<int?>() ?? 0;
 4106                if (present <= 0)
 107                    continue;
 108
 109                // free_stock = present - reserved. Строку с present > 0 оставляем в любом случае —
 110                // фаза 5a (партии [N]) накатывает по остатку 1С и не смотрит на Ozon qty. Решение
 111                // «сирота или нет» принимается по FreeQty уже в BitrixStockOzonOrphanCleanupService.
 4112                var reserved = wh["reserved"]?.Value<int?>() ?? 0;
 4113                var freeStock = wh["free_stock"]?.Value<int?>() ?? (present - reserved);
 114
 4115                var (cleanOfferId, brandCode) = ParseOfferId(offerId);
 4116                var warehouseName = warehouseNameById.TryGetValue(warehouseId, out var mappedName) && !string.IsNullOrWh
 4117                    ? mappedName
 4118                    : wh["warehouse_name"]?.ToString() ?? warehouseId.ToString();
 119
 4120                result.Add(new OzonCatalogStockRow
 4121                {
 4122                    WarehouseId = warehouseId.ToString(),
 4123                    WarehouseName = warehouseName,
 4124                    OfferId = cleanOfferId,
 4125                    OriginalOfferId = offerId,
 4126                    BrandCode = brandCode,
 4127                    ProductName = catalogRow?.Name,
 4128                    AvailableQty = present,
 4129                    ReservedQty = reserved,
 4130                    FreeQty = Math.Max(0, freeStock),
 4131                    ProductId = catalogRow?.ProductId ?? wh["product_id"]?.Value<long?>()
 4132                });
 133            }
 134        }
 135
 4136        if (log != null)
 0137            await log($"  Ozon API stocks-by-warehouse: +{result.Count} строк на целевых складах ({targetWarehouseIds.Co
 138
 4139        return result;
 7140    }
 141
 142    private static (string CleanOfferId, string? BrandCode) ParseOfferId(string rawOfferId)
 143    {
 4144        var eqIdx = rawOfferId.IndexOf('=');
 4145        if (eqIdx > 0)
 146        {
 4147            var clean = rawOfferId[..eqIdx].Trim();
 4148            var brandPart = rawOfferId[(eqIdx + 1)..].Trim();
 4149            var m = BrandCodeRegex.Match(brandPart);
 4150            return (clean, m.Success ? m.Groups[1].Value : null);
 151        }
 152
 0153        return (rawOfferId.Trim(), null);
 154    }
 155}