< Summary

Information
Class: Services.BitrixStockServices.BitrixStockSyncApplyDisabledPlanner
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/BitrixStockSyncApplyDisabledPlanner.cs
Line coverage
93%
Covered lines: 99
Uncovered lines: 7
Coverable lines: 106
Total lines: 258
Line coverage: 93.3%
Branch coverage
67%
Covered branches: 50
Total branches: 74
Branch coverage: 67.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
BuildWarehouseSets(...)50%141497.22%
SelectTargets(...)84.09%444498.3%
BuildLineStateKey(...)100%11100%
IsAlreadyClearedDisabledLink(...)30%271044.44%
ParseWarehouseId(...)50%66100%

File(s)

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

#LineLine coverage
 1using OzonDomains.Models;
 2
 3namespace Services.BitrixStockServices;
 4
 5/// <summary>Планирование обнуления снятых связей этапа 2 по Ozon Warehouse ID.</summary>
 6public static class BitrixStockSyncApplyDisabledPlanner
 7{
 8    public static BitrixStockSyncApplyDisabledWarehouseSets BuildWarehouseSets(
 9        IEnumerable<WarehouseMapping> mappings,
 10        ISet<int> disabledMappingIds)
 11    {
 2112        var disabledIds = disabledMappingIds.Where(id => id > 0).ToHashSet();
 1013        if (disabledIds.Count == 0)
 014            return BitrixStockSyncApplyDisabledWarehouseSets.Empty;
 15
 1016        var all = mappings
 6017            .Where(m => !string.IsNullOrWhiteSpace(m.OzonWarehouseId))
 1018            .ToList();
 19
 1020        var enabledWarehouseIds = all
 6021            .Where(m => !disabledIds.Contains(m.Id))
 4922            .Select(m => ParseWarehouseId(m.OzonWarehouseId))
 4923            .Where(id => id > 0)
 1024            .ToHashSet();
 25
 1026        var disabledWarehouseIds = all
 6027            .Where(m => disabledIds.Contains(m.Id))
 1128            .Select(m => ParseWarehouseId(m.OzonWarehouseId))
 1129            .Where(id => id > 0 && !enabledWarehouseIds.Contains(id))
 1030            .ToHashSet();
 31
 1032        var warehouseNameById = all
 6033            .GroupBy(m => ParseWarehouseId(m.OzonWarehouseId))
 4034            .Where(g => g.Key > 0)
 1035            .ToDictionary(
 4036                g => g.Key,
 5037                g => g.First().OzonWarehouseName?.Trim() is { Length: > 0 } name
 5038                    ? name
 5039                    : g.Key.ToString());
 40
 1041        var oneCStoreByWarehouseId = all
 6042            .Where(m => disabledIds.Contains(m.Id))
 1143            .GroupBy(m => ParseWarehouseId(m.OzonWarehouseId))
 1044            .Where(g => g.Key > 0)
 1045            .ToDictionary(
 1046                g => g.Key,
 2047                g => g.First().BitrixWarehouseName?.Trim() ?? string.Empty);
 48
 1049        return new BitrixStockSyncApplyDisabledWarehouseSets(
 1050            disabledWarehouseIds,
 1051            enabledWarehouseIds,
 1052            warehouseNameById,
 1053            oneCStoreByWarehouseId);
 54    }
 55
 56    public static IReadOnlyList<BitrixStockSyncApplyDisabledZeroTarget> SelectTargets(
 57        IReadOnlyList<BitrixStockSyncApplyDisabledReportProduct> products,
 58        IReadOnlyDictionary<string, List<BitrixStockSyncCleanupFbsWarehouse>> fbsByOfferId,
 59        BitrixStockSyncApplyDisabledWarehouseSets warehouseSets,
 60        IReadOnlyDictionary<string, BitrixStockSyncApplyDisabledLineState> lineStateByKey,
 61        bool onlyPending,
 62        IReadOnlySet<string>? articleFilter,
 63        out BitrixStockSyncApplyDisabledSelectionStats stats)
 64    {
 665        stats = new BitrixStockSyncApplyDisabledSelectionStats();
 66
 667        if (warehouseSets.DisabledWarehouseIds.Count == 0 || products.Count == 0)
 068            return Array.Empty<BitrixStockSyncApplyDisabledZeroTarget>();
 69
 670        var articleSet = articleFilter is { Count: > 0 }
 671            ? articleFilter.ToHashSet(StringComparer.OrdinalIgnoreCase)
 672            : null;
 73
 674        var targets = new List<BitrixStockSyncApplyDisabledZeroTarget>();
 675        var alreadyZeroCount = 0;
 676        var fallbackFromReportCount = 0;
 677        var alreadyClearedSkipCount = 0;
 78
 2679        foreach (var product in products)
 80        {
 781            if (articleSet != null && !articleSet.Contains(product.Article))
 82                continue;
 83
 684            if (string.IsNullOrWhiteSpace(product.OfferId) || product.OzonProductId is not > 0)
 85                continue;
 86
 687            fbsByOfferId.TryGetValue(product.OfferId.Trim(), out var liveWarehouses);
 688            var liveByWarehouseId = (liveWarehouses ?? [])
 589                .Where(w => w.WarehouseId > 0)
 1690                .ToDictionary(w => w.WarehouseId, w => w);
 91
 2492            foreach (var disabledWarehouseId in warehouseSets.DisabledWarehouseIds)
 93            {
 94                int present;
 695                string? liveWarehouseName = null;
 696                var fromReportFallback = false;
 97
 698                if (liveByWarehouseId.TryGetValue(disabledWarehouseId, out var liveWh))
 99                {
 4100                    present = liveWh.Present;
 4101                    liveWarehouseName = liveWh.WarehouseName;
 102                }
 103                else
 104                {
 105                    // Live-запрос к Ozon не вернул этот склад для оффера (SDS-схема без разбивки по
 106                    // складам, временный сбой API и т.п.) — раньше это молча пропускало обнуление навсегда.
 107                    // Берём остаток из этого же отчёта (снимок этапа 1), чтобы реальный остаток на
 108                    // отключённом складе всё равно обнулился, а не завис в Pending бесконечно.
 4109                    var reportWh = product.ReportWarehouses.FirstOrDefault(w => w.WarehouseId == disabledWarehouseId);
 2110                    present = reportWh?.OzonQty ?? 0;
 2111                    fromReportFallback = present > 0;
 112                }
 113
 6114                if (present <= 0)
 115                {
 2116                    alreadyZeroCount++;
 2117                    continue;
 118                }
 119
 4120                var stateKey = BuildLineStateKey(product.Article, product.OzonClientId, disabledWarehouseId);
 4121                if (onlyPending && IsAlreadyClearedDisabledLink(lineStateByKey, stateKey))
 122                {
 1123                    alreadyClearedSkipCount++;
 1124                    continue;
 125                }
 126
 3127                if (fromReportFallback)
 1128                    fallbackFromReportCount++;
 129
 3130                warehouseSets.WarehouseNameById.TryGetValue(disabledWarehouseId, out var mappedName);
 3131                warehouseSets.OneCStoreByWarehouseId.TryGetValue(disabledWarehouseId, out var oneCStore);
 132
 3133                targets.Add(new BitrixStockSyncApplyDisabledZeroTarget
 3134                {
 3135                    OzonClientId = product.OzonClientId,
 3136                    Article = product.Article,
 3137                    OfferId = product.OfferId.Trim(),
 3138                    OzonProductId = product.OzonProductId.Value,
 3139                    WarehouseId = disabledWarehouseId,
 3140                    WarehouseName = !string.IsNullOrWhiteSpace(mappedName) ? mappedName : (liveWarehouseName ?? string.E
 3141                    OneCStore = !string.IsNullOrWhiteSpace(oneCStore) ? oneCStore : (liveWarehouseName ?? string.Empty),
 3142                    Present = present,
 3143                    FromReportFallback = fromReportFallback
 3144                });
 145            }
 146        }
 147
 6148        stats = new BitrixStockSyncApplyDisabledSelectionStats
 6149        {
 6150            AlreadyZeroCount = alreadyZeroCount,
 6151            FallbackFromReportCount = fallbackFromReportCount,
 6152            AlreadyClearedSkipCount = alreadyClearedSkipCount
 6153        };
 154
 6155        return targets
 3156            .OrderBy(t => t.Article, StringComparer.OrdinalIgnoreCase)
 3157            .ThenBy(t => t.WarehouseId)
 6158            .ToList();
 159    }
 160
 161    public static string BuildLineStateKey(string article, int clientId, long warehouseId) =>
 5162        $"{article.Trim()}|{clientId}|{warehouseId}";
 163
 164    public static bool IsAlreadyClearedDisabledLink(
 165        IReadOnlyDictionary<string, BitrixStockSyncApplyDisabledLineState> lineStateByKey,
 166        string stateKey)
 167    {
 1168        if (!lineStateByKey.TryGetValue(stateKey, out var state))
 0169            return false;
 170
 1171        if (!string.Equals(state.OzonApplyStatus, BitrixStockSyncReportApplyStatuses.Success, StringComparison.OrdinalIg
 0172            return false;
 173
 1174        if (string.Equals(state.Action, BitrixStockSyncReportActions.ClearDisabledLink, StringComparison.OrdinalIgnoreCa
 1175            return true;
 176
 0177        var err = state.OzonApplyError ?? string.Empty;
 0178        return err.Contains("связь отключена", StringComparison.OrdinalIgnoreCase)
 0179               || err.Contains("обнулено", StringComparison.OrdinalIgnoreCase);
 180    }
 181
 182    private static long ParseWarehouseId(string? raw) =>
 131183        long.TryParse(raw?.Trim(), out var id) && id > 0 ? id : 0;
 184}
 185
 186public sealed class BitrixStockSyncApplyDisabledWarehouseSets
 187{
 188    public static BitrixStockSyncApplyDisabledWarehouseSets Empty { get; } = new(
 189        new HashSet<long>(),
 190        new HashSet<long>(),
 191        new Dictionary<long, string>(),
 192        new Dictionary<long, string>());
 193
 194    public BitrixStockSyncApplyDisabledWarehouseSets(
 195        IReadOnlySet<long> disabledWarehouseIds,
 196        IReadOnlySet<long> enabledWarehouseIds,
 197        IReadOnlyDictionary<long, string> warehouseNameById,
 198        IReadOnlyDictionary<long, string> oneCStoreByWarehouseId)
 199    {
 200        DisabledWarehouseIds = disabledWarehouseIds;
 201        EnabledWarehouseIds = enabledWarehouseIds;
 202        WarehouseNameById = warehouseNameById;
 203        OneCStoreByWarehouseId = oneCStoreByWarehouseId;
 204    }
 205
 206    public IReadOnlySet<long> DisabledWarehouseIds { get; }
 207    public IReadOnlySet<long> EnabledWarehouseIds { get; }
 208    public IReadOnlyDictionary<long, string> WarehouseNameById { get; }
 209    public IReadOnlyDictionary<long, string> OneCStoreByWarehouseId { get; }
 210}
 211
 212public sealed class BitrixStockSyncApplyDisabledReportProduct
 213{
 214    public int OzonClientId { get; init; }
 215    public string Article { get; init; } = string.Empty;
 216    public string OfferId { get; init; } = string.Empty;
 217    public long? OzonProductId { get; init; }
 218
 219    /// <summary>Остатки Ozon по складам из ЭТОГО отчёта (снимок этапа 1) — fallback, когда live-запрос
 220    /// к Ozon в момент этапа 2 не вернул разбивку по складу для оффера.</summary>
 221    public IReadOnlyList<BitrixStockSyncApplyDisabledReportWarehouseQty> ReportWarehouses { get; init; } =
 222        Array.Empty<BitrixStockSyncApplyDisabledReportWarehouseQty>();
 223}
 224
 225public sealed class BitrixStockSyncApplyDisabledReportWarehouseQty
 226{
 227    public long WarehouseId { get; init; }
 228    public int OzonQty { get; init; }
 229}
 230
 231public sealed class BitrixStockSyncApplyDisabledSelectionStats
 232{
 233    public int AlreadyZeroCount { get; init; }
 234    public int FallbackFromReportCount { get; init; }
 235    public int AlreadyClearedSkipCount { get; init; }
 236}
 237
 238public sealed class BitrixStockSyncApplyDisabledLineState
 239{
 240    public string? OzonApplyStatus { get; init; }
 241    public string? OzonApplyError { get; init; }
 242    public string? Action { get; init; }
 243}
 244
 245public sealed class BitrixStockSyncApplyDisabledZeroTarget
 246{
 247    public int OzonClientId { get; init; }
 248    public string Article { get; init; } = string.Empty;
 249    public string OfferId { get; init; } = string.Empty;
 250    public long OzonProductId { get; init; }
 251    public long WarehouseId { get; init; }
 252    public string WarehouseName { get; init; } = string.Empty;
 253    public string OneCStore { get; init; } = string.Empty;
 254    public int Present { get; init; }
 255
 256    /// <summary>true — остаток взят из снимка отчёта, а не из live-ответа Ozon (live не вернул склад).</summary>
 257    public bool FromReportFallback { get; init; }
 258}