< Summary

Information
Class: Services.BitrixStockServices.BitrixStockSyncApplyDisabledReportProduct
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/BitrixStockSyncApplyDisabledPlanner.cs
Line coverage
100%
Covered lines: 6
Uncovered lines: 0
Coverable lines: 6
Total lines: 258
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_OzonClientId()100%11100%
get_Article()100%11100%
get_OfferId()100%11100%
get_OzonProductId()100%11100%
get_ReportWarehouses()100%11100%
.ctor()100%11100%

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    {
 12        var disabledIds = disabledMappingIds.Where(id => id > 0).ToHashSet();
 13        if (disabledIds.Count == 0)
 14            return BitrixStockSyncApplyDisabledWarehouseSets.Empty;
 15
 16        var all = mappings
 17            .Where(m => !string.IsNullOrWhiteSpace(m.OzonWarehouseId))
 18            .ToList();
 19
 20        var enabledWarehouseIds = all
 21            .Where(m => !disabledIds.Contains(m.Id))
 22            .Select(m => ParseWarehouseId(m.OzonWarehouseId))
 23            .Where(id => id > 0)
 24            .ToHashSet();
 25
 26        var disabledWarehouseIds = all
 27            .Where(m => disabledIds.Contains(m.Id))
 28            .Select(m => ParseWarehouseId(m.OzonWarehouseId))
 29            .Where(id => id > 0 && !enabledWarehouseIds.Contains(id))
 30            .ToHashSet();
 31
 32        var warehouseNameById = all
 33            .GroupBy(m => ParseWarehouseId(m.OzonWarehouseId))
 34            .Where(g => g.Key > 0)
 35            .ToDictionary(
 36                g => g.Key,
 37                g => g.First().OzonWarehouseName?.Trim() is { Length: > 0 } name
 38                    ? name
 39                    : g.Key.ToString());
 40
 41        var oneCStoreByWarehouseId = all
 42            .Where(m => disabledIds.Contains(m.Id))
 43            .GroupBy(m => ParseWarehouseId(m.OzonWarehouseId))
 44            .Where(g => g.Key > 0)
 45            .ToDictionary(
 46                g => g.Key,
 47                g => g.First().BitrixWarehouseName?.Trim() ?? string.Empty);
 48
 49        return new BitrixStockSyncApplyDisabledWarehouseSets(
 50            disabledWarehouseIds,
 51            enabledWarehouseIds,
 52            warehouseNameById,
 53            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    {
 65        stats = new BitrixStockSyncApplyDisabledSelectionStats();
 66
 67        if (warehouseSets.DisabledWarehouseIds.Count == 0 || products.Count == 0)
 68            return Array.Empty<BitrixStockSyncApplyDisabledZeroTarget>();
 69
 70        var articleSet = articleFilter is { Count: > 0 }
 71            ? articleFilter.ToHashSet(StringComparer.OrdinalIgnoreCase)
 72            : null;
 73
 74        var targets = new List<BitrixStockSyncApplyDisabledZeroTarget>();
 75        var alreadyZeroCount = 0;
 76        var fallbackFromReportCount = 0;
 77        var alreadyClearedSkipCount = 0;
 78
 79        foreach (var product in products)
 80        {
 81            if (articleSet != null && !articleSet.Contains(product.Article))
 82                continue;
 83
 84            if (string.IsNullOrWhiteSpace(product.OfferId) || product.OzonProductId is not > 0)
 85                continue;
 86
 87            fbsByOfferId.TryGetValue(product.OfferId.Trim(), out var liveWarehouses);
 88            var liveByWarehouseId = (liveWarehouses ?? [])
 89                .Where(w => w.WarehouseId > 0)
 90                .ToDictionary(w => w.WarehouseId, w => w);
 91
 92            foreach (var disabledWarehouseId in warehouseSets.DisabledWarehouseIds)
 93            {
 94                int present;
 95                string? liveWarehouseName = null;
 96                var fromReportFallback = false;
 97
 98                if (liveByWarehouseId.TryGetValue(disabledWarehouseId, out var liveWh))
 99                {
 100                    present = liveWh.Present;
 101                    liveWarehouseName = liveWh.WarehouseName;
 102                }
 103                else
 104                {
 105                    // Live-запрос к Ozon не вернул этот склад для оффера (SDS-схема без разбивки по
 106                    // складам, временный сбой API и т.п.) — раньше это молча пропускало обнуление навсегда.
 107                    // Берём остаток из этого же отчёта (снимок этапа 1), чтобы реальный остаток на
 108                    // отключённом складе всё равно обнулился, а не завис в Pending бесконечно.
 109                    var reportWh = product.ReportWarehouses.FirstOrDefault(w => w.WarehouseId == disabledWarehouseId);
 110                    present = reportWh?.OzonQty ?? 0;
 111                    fromReportFallback = present > 0;
 112                }
 113
 114                if (present <= 0)
 115                {
 116                    alreadyZeroCount++;
 117                    continue;
 118                }
 119
 120                var stateKey = BuildLineStateKey(product.Article, product.OzonClientId, disabledWarehouseId);
 121                if (onlyPending && IsAlreadyClearedDisabledLink(lineStateByKey, stateKey))
 122                {
 123                    alreadyClearedSkipCount++;
 124                    continue;
 125                }
 126
 127                if (fromReportFallback)
 128                    fallbackFromReportCount++;
 129
 130                warehouseSets.WarehouseNameById.TryGetValue(disabledWarehouseId, out var mappedName);
 131                warehouseSets.OneCStoreByWarehouseId.TryGetValue(disabledWarehouseId, out var oneCStore);
 132
 133                targets.Add(new BitrixStockSyncApplyDisabledZeroTarget
 134                {
 135                    OzonClientId = product.OzonClientId,
 136                    Article = product.Article,
 137                    OfferId = product.OfferId.Trim(),
 138                    OzonProductId = product.OzonProductId.Value,
 139                    WarehouseId = disabledWarehouseId,
 140                    WarehouseName = !string.IsNullOrWhiteSpace(mappedName) ? mappedName : (liveWarehouseName ?? string.E
 141                    OneCStore = !string.IsNullOrWhiteSpace(oneCStore) ? oneCStore : (liveWarehouseName ?? string.Empty),
 142                    Present = present,
 143                    FromReportFallback = fromReportFallback
 144                });
 145            }
 146        }
 147
 148        stats = new BitrixStockSyncApplyDisabledSelectionStats
 149        {
 150            AlreadyZeroCount = alreadyZeroCount,
 151            FallbackFromReportCount = fallbackFromReportCount,
 152            AlreadyClearedSkipCount = alreadyClearedSkipCount
 153        };
 154
 155        return targets
 156            .OrderBy(t => t.Article, StringComparer.OrdinalIgnoreCase)
 157            .ThenBy(t => t.WarehouseId)
 158            .ToList();
 159    }
 160
 161    public static string BuildLineStateKey(string article, int clientId, long warehouseId) =>
 162        $"{article.Trim()}|{clientId}|{warehouseId}";
 163
 164    public static bool IsAlreadyClearedDisabledLink(
 165        IReadOnlyDictionary<string, BitrixStockSyncApplyDisabledLineState> lineStateByKey,
 166        string stateKey)
 167    {
 168        if (!lineStateByKey.TryGetValue(stateKey, out var state))
 169            return false;
 170
 171        if (!string.Equals(state.OzonApplyStatus, BitrixStockSyncReportApplyStatuses.Success, StringComparison.OrdinalIg
 172            return false;
 173
 174        if (string.Equals(state.Action, BitrixStockSyncReportActions.ClearDisabledLink, StringComparison.OrdinalIgnoreCa
 175            return true;
 176
 177        var err = state.OzonApplyError ?? string.Empty;
 178        return err.Contains("связь отключена", StringComparison.OrdinalIgnoreCase)
 179               || err.Contains("обнулено", StringComparison.OrdinalIgnoreCase);
 180    }
 181
 182    private static long ParseWarehouseId(string? raw) =>
 183        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{
 14214    public int OzonClientId { get; init; }
 23215    public string Article { get; init; } = string.Empty;
 29216    public string OfferId { get; init; } = string.Empty;
 16217    public long? OzonProductId { get; init; }
 218
 219    /// <summary>Остатки Ozon по складам из ЭТОГО отчёта (снимок этапа 1) — fallback, когда live-запрос
 220    /// к Ozon в момент этапа 2 не вернул разбивку по складу для оффера.</summary>
 5221    public IReadOnlyList<BitrixStockSyncApplyDisabledReportWarehouseQty> ReportWarehouses { get; init; } =
 7222        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}