< Summary

Information
Class: Services.BitrixStockServices.BatchFreeStockRequest
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/BitrixStockBatchVariantFreeStock.cs
Line coverage
100%
Covered lines: 6
Uncovered lines: 0
Coverable lines: 6
Total lines: 122
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_Article()100%11100%
get_Producer()100%11100%
get_Stores()100%11100%
get_RawQty()100%11100%
get_Multiplier()100%11100%
get_SkipLive()100%11100%

File(s)

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

#LineLine coverage
 1namespace Services.BitrixStockServices;
 2
 3/// <summary>Партийная позиция [N], для которой нужно получить свободный остаток базового артикула.</summary>
 4/// <param name="Article">Базовый артикул (без суффикса [N] и производителя).</param>
 5/// <param name="Producer">Производитель из снимка таблицы; без него карточку в 1С не найти.</param>
 6/// <param name="Stores">Склады 1С, связанные с Ozon-складом партии.</param>
 7/// <param name="RawQty">Остаток базового артикула по снимку таблицы, штук.</param>
 8/// <param name="Multiplier">Штук в упаковке.</param>
 9/// <param name="SkipLive">Сверка с 1С онлайн не нужна (склад обнуляется, нет остатка, производитель не совпал).</param>
 10public readonly record struct BatchFreeStockRequest(
 4711    string Article,
 4312    string? Producer,
 6213    IReadOnlyList<string> Stores,
 4014    int RawQty,
 1915    int Multiplier,
 2016    bool SkipLive);
 17
 18/// <summary>Свободный остаток базового артикула партии, штук.</summary>
 19/// <param name="ReservedQty">Резерв заказов, который реально вычтен из остатка.</param>
 20/// <param name="FreeQty">Итог: остаток снимка минус резерв, ограниченный остатком 1С онлайн.</param>
 21/// <param name="LiveFreeQty">Свободный остаток по 1С онлайн; null — не проверялось или проверить не удалось.</param>
 22public readonly record struct BatchFreeStockResult(int ReservedQty, int FreeQty, int? LiveFreeQty)
 23{
 24    /// <summary>1С онлайн показала меньше, чем осталось по снимку за вычетом резерва.</summary>
 25    public bool ClampedByLive => LiveFreeQty.HasValue && FreeQty < RawMinusReserved;
 26
 27    /// <summary>Остаток снимка минус резерв до сверки с 1С онлайн; заполняется планировщиком.</summary>
 28    public int RawMinusReserved { get; init; }
 29}
 30
 31/// <summary>
 32/// Свободный остаток для партий [N] на этапе 5a. Партия накатывается как floor(остаток базового артикула / N),
 33/// поэтому остаток должен быть свободным — без резерва заказов и не выше того, что сейчас числится в 1С.
 34/// Раньше брался сырой остаток снимка Google-таблицы: полностью зарезервированный и уже не лежащий в 1С товар
 35/// продолжал продаваться упаковками (SF1008=SFX[4]: в таблице 23, резерв 23, на Ozon 5 уп.).
 36/// </summary>
 37public static class BitrixStockBatchVariantFreeStock
 38{
 39    /// <summary>Остаток снимка минус резерв (резерв не больше самого остатка), не меньше нуля.</summary>
 40    public static int FreeAfterReserve(int rawQty, int reservedQty)
 41    {
 42        var raw = Math.Max(0, rawQty);
 43        return raw - Math.Min(Math.Max(0, reservedQty), raw);
 44    }
 45
 46    /// <summary>Остаток к накату: свободный по снимку, ограниченный свободным по 1С онлайн (если он известен).</summary
 47    public static int ApplyLive(int freeAfterReserve, int? liveFreeQty) =>
 48        liveFreeQty.HasValue ? BitrixStockLiveStockClamp.Clamp(freeAfterReserve, liveFreeQty.Value) : freeAfterReserve;
 49
 50    /// <summary>
 51    /// Считает свободный остаток для каждой партии. Результат идёт в том же порядке, что и <paramref name="requests"/>.
 52    /// Сверка с 1С онлайн — fail-open: если 1С недоступна или карточка не найдена, остаётся остаток снимка за вычетом р
 53    /// </summary>
 54    public static async Task<IReadOnlyList<BatchFreeStockResult>> ResolveAsync(
 55        IReadOnlyList<BatchFreeStockRequest> requests,
 56        IReadOnlyDictionary<string, Dictionary<string, int>> reserveMap,
 57        IBitrixStockLiveStockGuard liveGuard,
 58        CancellationToken ct = default)
 59    {
 60        var results = new BatchFreeStockResult[requests.Count];
 61        var reservedByIndex = new int[requests.Count];
 62        var toCheck = new Dictionary<string, (LiveStockLine Line, List<int> Indexes)>(StringComparer.OrdinalIgnoreCase);
 63
 64        for (var i = 0; i < requests.Count; i++)
 65        {
 66            var r = requests[i];
 67            var reserved = BitrixOzonStockLinkage.GetLinkedReservedQuantity(r.Article, r.Stores, reserveMap);
 68            var actualReserved = Math.Min(reserved, Math.Max(0, r.RawQty));
 69            var free = FreeAfterReserve(r.RawQty, reserved);
 70            reservedByIndex[i] = actualReserved;
 71            results[i] = new BatchFreeStockResult(actualReserved, free, null) { RawMinusReserved = free };
 72
 73            // Нет смысла спрашивать 1С, если упаковка и так не набирается.
 74            if (r.SkipLive || free < Math.Max(1, r.Multiplier) || string.IsNullOrWhiteSpace(r.Producer) || r.Stores.Coun
 75                continue;
 76
 77            // Одна и та же карточка на нескольких Ozon-складах с одинаковыми складами 1С — один запрос к 1С.
 78            var key = $"{r.Article}|{r.Producer}|{string.Join(',', r.Stores.OrderBy(s => s, StringComparer.OrdinalIgnore
 79            if (!toCheck.TryGetValue(key, out var group))
 80            {
 81                group = (new LiveStockLine(i, r.Article, r.Producer, r.Stores, actualReserved), new List<int>());
 82                toCheck[key] = group;
 83            }
 84
 85            group.Indexes.Add(i);
 86        }
 87
 88        if (toCheck.Count == 0)
 89            return results;
 90
 91        IReadOnlyDictionary<long, int> liveFree;
 92        try
 93        {
 94            liveFree = await liveGuard.GetLiveFreeStockAsync(toCheck.Values.Select(g => g.Line).ToList(), ct);
 95        }
 96        catch (OperationCanceledException)
 97        {
 98            throw;
 99        }
 100        catch
 101        {
 102            return results; // fail-open: недоступность 1С не должна останавливать этап 5
 103        }
 104
 105        foreach (var (line, indexes) in toCheck.Values)
 106        {
 107            if (!liveFree.TryGetValue(line.LineId, out var live))
 108                continue;
 109
 110            foreach (var index in indexes)
 111            {
 112                var free = ApplyLive(results[index].RawMinusReserved, live);
 113                results[index] = new BatchFreeStockResult(reservedByIndex[index], free, live)
 114                {
 115                    RawMinusReserved = results[index].RawMinusReserved
 116                };
 117            }
 118        }
 119
 120        return results;
 121    }
 122}