< Summary

Information
Class: Services.BitrixStockServices.BitrixStockOrphanOfferMatching
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/BitrixStockOrphanOfferMatching.cs
Line coverage
98%
Covered lines: 65
Uncovered lines: 1
Coverable lines: 66
Total lines: 150
Line coverage: 98.4%
Branch coverage
91%
Covered branches: 51
Total branches: 56
Branch coverage: 91%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_ExactByWarehouse()100%11100%
get_NormalizedByWarehouse()100%11100%
BuildReportKeyIndex(...)75%1616100%
IsCoveredByReport(...)100%11100%
SplitByFreeStock(...)100%44100%
ExplainCoverage(...)100%88100%
NormalizeOfferId(...)100%66100%
ExtractBatchMultiplier(...)100%1010100%
ExtractArticle(...)100%44100%
ExtractMarketPrefix(...)87.5%8887.5%
get_OfferId()100%11100%
get_WarehouseId()100%11100%
get_Article()100%11100%

File(s)

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

#LineLine coverage
 1namespace Services.BitrixStockServices;
 2
 3/// <summary>
 4/// Сопоставление offer_id из Ozon seller_stocks с ключами строк отчёта BitrixStock (этап 5).
 5/// Учитывает суффиксы [N], разный регистр бренда и строки NotFound с OzonWarehouseId=0.
 6/// </summary>
 7public static class BitrixStockOrphanOfferMatching
 8{
 9    public sealed class ReportKeyIndex
 10    {
 4011        public HashSet<(string OfferId, string WarehouseId)> ExactByWarehouse { get; } = new();
 3912        public HashSet<(string OfferId, string WarehouseId)> NormalizedByWarehouse { get; } = new();
 13    }
 14
 15    public static ReportKeyIndex BuildReportKeyIndex(IEnumerable<ReportLineKey> lines)
 16    {
 1617        var index = new ReportKeyIndex();
 6418        foreach (var line in lines)
 19        {
 1620            var offerId = line.OfferId?.Trim();
 1621            if (string.IsNullOrEmpty(offerId))
 22                continue;
 23
 1424            var warehouseId = line.WarehouseId?.Trim() ?? string.Empty;
 1425            var exact = offerId.ToLowerInvariant();
 1426            var normalized = NormalizeOfferId(offerId);
 1427            var hasMappedWarehouse = long.TryParse(warehouseId, out var wid) && wid > 0;
 28
 1429            if (hasMappedWarehouse)
 30            {
 1031                index.ExactByWarehouse.Add((exact, warehouseId));
 1032                if (!string.IsNullOrEmpty(normalized))
 1033                    index.NormalizedByWarehouse.Add((normalized, warehouseId));
 34            }
 35        }
 36
 1637        return index;
 38    }
 39
 40    public static bool IsCoveredByReport(ReportKeyIndex index, string ozonOriginalOfferId, string warehouseId)
 41    {
 742        return ExplainCoverage(index, ozonOriginalOfferId, warehouseId) != null;
 43    }
 44
 45    /// <summary>
 46    /// Делит непокрытые отчётом строки на настоящих сирот и полностью зарезервированные.
 47    /// Сирота — только та строка, у которой есть свободный остаток (<c>free_stock &gt; 0</c>): именно её
 48    /// покупатель ещё может заказать. При <c>present = reserved</c> товар уже недоступен, а Ozon не
 49    /// опускает остаток ниже резерва — такие строки обнулялись вхолостую каждый прогон.
 50    /// </summary>
 51    public static (List<OzonCatalogStockRow> Orphans, List<OzonCatalogStockRow> FullyReserved) SplitByFreeStock(
 52        IEnumerable<OzonCatalogStockRow> potentialOrphans)
 53    {
 454        var orphans = new List<OzonCatalogStockRow>();
 455        var fullyReserved = new List<OzonCatalogStockRow>();
 56
 2057        foreach (var row in potentialOrphans)
 58        {
 659            if (row.FreeQty > 0)
 460                orphans.Add(row);
 61            else
 262                fullyReserved.Add(row);
 63        }
 64
 465        return (orphans, fullyReserved);
 66    }
 67
 68    /// <summary>Возвращает имя сработавшего правила или null если не покрыто.</summary>
 69    public static string? ExplainCoverage(ReportKeyIndex index, string ozonOriginalOfferId, string warehouseId)
 70    {
 1571        if (string.IsNullOrWhiteSpace(ozonOriginalOfferId))
 272            return null;
 73
 1374        var exact = ozonOriginalOfferId.Trim().ToLowerInvariant();
 1375        var normalized = NormalizeOfferId(ozonOriginalOfferId);
 1376        var wh = warehouseId.Trim();
 77
 1378        if (index.ExactByWarehouse.Contains((exact, wh)))
 279            return "exact";
 1180        if (!string.IsNullOrEmpty(normalized) && index.NormalizedByWarehouse.Contains((normalized, wh)))
 281            return "normalized_wh";
 982        return null;
 83    }
 84
 85    /// <summary>
 86    /// Нормализует offer_id: lower + убирает вариант [N] после кода бренда (KJ2288=MSM[5] → kj2288=msm).
 87    /// </summary>
 88    public static string NormalizeOfferId(string? offerId)
 89    {
 3890        if (string.IsNullOrWhiteSpace(offerId))
 391            return string.Empty;
 92
 3593        offerId = offerId.Trim();
 3594        var eqIdx = offerId.IndexOf('=');
 3595        if (eqIdx <= 0)
 296            return offerId.ToLowerInvariant();
 97
 3398        var article = offerId[..eqIdx].Trim();
 3399        var brandPart = offerId[(eqIdx + 1)..].Trim();
 33100        var bracketIdx = brandPart.IndexOf('[');
 33101        if (bracketIdx >= 0)
 7102            brandPart = brandPart[..bracketIdx].Trim();
 103
 33104        return $"{article}={brandPart}".ToLowerInvariant();
 105    }
 106
 107    /// <summary>
 108    /// Извлекает размер партии из OfferId: "9046709030=TYT[5]" → 5. Нет [N] или [1] → 1.
 109    /// </summary>
 110    public static int ExtractBatchMultiplier(string? offerId)
 111    {
 113112        if (string.IsNullOrWhiteSpace(offerId)) return 1;
 105113        var start = offerId.LastIndexOf('[');
 105114        var end   = offerId.LastIndexOf(']');
 202115        if (start < 0 || end <= start) return 1;
 8116        return int.TryParse(offerId[(start + 1)..end], out var n) && n > 1 ? n : 1;
 117    }
 118
 119    public static string ExtractArticle(string? offerId)
 120    {
 7121        if (string.IsNullOrWhiteSpace(offerId))
 2122            return string.Empty;
 123
 5124        offerId = offerId.Trim();
 5125        var eqIdx = offerId.IndexOf('=');
 5126        return (eqIdx > 0 ? offerId[..eqIdx] : offerId).Trim().ToLowerInvariant();
 127    }
 128
 129    /// <summary>Возвращает код производителя из <c>ARTICLE=PREFIX[N]</c>, иначе пустую строку.</summary>
 130    public static string ExtractMarketPrefix(string? offerId)
 131    {
 6132        if (string.IsNullOrWhiteSpace(offerId))
 0133            return string.Empty;
 134
 6135        var eqIdx = offerId.IndexOf('=');
 6136        if (eqIdx <= 0 || eqIdx == offerId.Length - 1)
 1137            return string.Empty;
 138
 5139        var value = offerId[(eqIdx + 1)..].Trim();
 5140        var bracketIdx = value.IndexOf('[');
 5141        return (bracketIdx >= 0 ? value[..bracketIdx] : value).Trim().ToUpperInvariant();
 142    }
 143
 144    public sealed class ReportLineKey
 145    {
 48146        public string OfferId { get; init; } = string.Empty;
 46147        public string WarehouseId { get; init; } = string.Empty;
 16148        public string? Article { get; init; }
 149    }
 150}