< Summary

Information
Class: OzonOrdersWeb.WebServices.Transactions.Grid.Preview.ShipToSellerStockGap
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/Grid/Preview/ShipToSellerPreviewService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 2
Coverable lines: 2
Total lines: 271
Line coverage: 0%
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
.ctor(...)100%210%
get_OrderIds()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/Grid/Preview/ShipToSellerPreviewService.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains.Models;
 4using OzonOrdersWeb.Areas.Studio2.Helpers;
 5using OzonRepositories.Context;
 6using Services.ApiServices._1CApi;
 7using Services.ApiServices._1CApi.Stock;
 8using Services.Authorization.Services;
 9using Services.BackgroundJobsServices;
 10
 11namespace OzonOrdersWeb.WebServices.Transactions.Grid.Preview;
 12
 13/// <summary>Тело <c>POST /Studio2/TransactionsGrid/ship-to-seller/Preview1C</c>: текущие значения строк грида.</summary
 14public sealed class ShipToSellerPreviewBody
 15{
 16    public List<ShipToSellerPreviewRow> Rows { get; set; } = new();
 17}
 18
 19public sealed class ShipToSellerPreviewRow
 20{
 21    public int Id { get; set; }
 22
 23    /// <summary>Текущее количество в гриде (0 — транзит «Утерян реализатором»).</summary>
 24    public int? Quantity { get; set; }
 25
 26    public string? Zasyl { get; set; }
 27    public string? Manufacturer { get; set; }
 28}
 29
 030public sealed record ShipToSellerStockGap(
 031    IReadOnlyList<int> OrderIds, string Article, string? ProductName, string Warehouse, decimal Required, decimal Availa
 32
 33public sealed record ShipToSellerZasylChip(string? Zasyl, string Warehouse, bool Mapped, int Orders);
 34
 35public sealed record ShipToSellerUnmappedZasyl(string Zasyl, int? ClientId, string? ClientName, string? DeliveryType, in
 36
 37/// <summary>Исход транзита для одного заказа с количеством 0.</summary>
 38public enum ShipToSellerTransitOutcome
 39{
 40    /// <summary>Остаток WS уйдёт на транзитный склад проведённым перемещением.</summary>
 41    Transit,
 42
 43    /// <summary>Перемещение уйдёт черновиком — в 1С не хватает остатка.</summary>
 44    TransitDraft,
 45
 46    /// <summary>Нет остатка WS — заказ получит «Утерян (без транзита)», в 1С не идёт.</summary>
 47    NoStock,
 48
 49    /// <summary>Транзитный склад не настроен или не найден в 1С — перемещение не создастся.</summary>
 50    TransitUnavailable
 51}
 52
 53public sealed record ShipToSellerTransitLine(
 54    int OrderId, string? ShipmentNumber, string? Article, string Sender, string Transit, decimal StockQty, ShipToSellerT
 55
 56public sealed class ShipToSellerPreviewDto
 57{
 58    /// <summary>1С не ответила (fail-open): нехватку показать нельзя.</summary>
 59    public bool Unavailable { get; set; }
 60
 61    public List<ShipToSellerStockGap> Stock { get; set; } = new();
 62    public List<ShipToSellerZasylChip> ZasylChips { get; set; } = new();
 63    public List<ShipToSellerUnmappedZasyl> ZasylUnmapped { get; set; } = new();
 64    public List<ShipToSellerTransitLine> Transit { get; set; } = new();
 65    public string? NoTransitStatus { get; set; }
 66    public string? LostStatus { get; set; }
 67}
 68
 69public interface IShipToSellerPreviewService
 70{
 71    Task<ShipToSellerPreviewDto> BuildAsync(ShipToSellerPreviewBody body, ClaimsPrincipal user, CancellationToken ct);
 72}
 73
 74/// <summary>
 75/// Предпросмотр боковой панели «Отгрузка реализатору»: нехватка остатка в 1С, склад списания по засылу и
 76/// исход транзита (qty = 0). Считает теми же построителями, что и проведение (<see cref="ShipToSellerOneCPlanner"/>).
 77/// </summary>
 78public sealed class ShipToSellerPreviewService : IShipToSellerPreviewService
 79{
 80    private const int MaxRows = 500;
 81
 82    private readonly ApplicationDbContext _context;
 83    private readonly ShipToSellerOneCPlanner _planner;
 84    private readonly OneCTransferManager? _transfer;
 85    private readonly IDataScopeService? _dataScope;
 86
 87    public ShipToSellerPreviewService(
 88        ApplicationDbContext context,
 89        ShipToSellerOneCPlanner planner,
 90        OneCTransferManager? transfer = null,
 91        IDataScopeService? dataScope = null)
 92    {
 93        _context = context;
 94        _planner = planner;
 95        _transfer = transfer;
 96        _dataScope = dataScope;
 97    }
 98
 99    public async Task<ShipToSellerPreviewDto> BuildAsync(ShipToSellerPreviewBody body, ClaimsPrincipal user, Cancellatio
 100    {
 101        var dto = new ShipToSellerPreviewDto
 102        {
 103            NoTransitStatus = LostByCommissionerTransitHelper.NoTransitStatusName,
 104            LostStatus = LostByCommissionerTransitHelper.LostStatusName
 105        };
 106
 107        var rows = (body.Rows ?? new()).Where(r => r.Id > 0).GroupBy(r => r.Id).Select(g => g.First()).Take(MaxRows).ToL
 108        var allowed = await AllowedIdsAsync(rows.Select(r => r.Id).ToArray(), user, ct);
 109        rows = rows.Where(r => allowed.Contains(r.Id)).ToList();
 110        if (rows.Count == 0)
 111        {
 112            return dto;
 113        }
 114
 115        var zeroIds = rows.Where(r => r.Quantity is 0).Select(r => r.Id).ToList();
 116        var mainRows = rows.Where(r => r.Quantity is not 0).ToList();
 117        var quantities = mainRows.Where(r => r.Quantity > 0).ToDictionary(r => r.Id, r => (decimal)r.Quantity!.Value);
 118        var zasyl = mainRows.ToDictionary(r => r.Id, r => (string?)r.Zasyl);
 119        var manufacturers = rows.Where(r => !string.IsNullOrWhiteSpace(r.Manufacturer)).ToDictionary(r => r.Id, r => r.M
 120
 121        var plan = await _planner.BuildAsync(
 122            new ShipToSellerPlanRequest(mainRows.Select(r => r.Id).ToList(), quantities, zasyl, zeroIds, manufacturers),
 123        var check = await _planner.CheckStockAsync(plan);
 124
 125        dto.Unavailable = check.CheckSkipped;
 126        dto.Stock = check.Blocking
 127            .Select(s => new ShipToSellerStockGap(s.OrderIds, s.Article, s.ProductName, s.WarehouseName, s.Required, s.A
 128            .ToList();
 129
 130        FillZasyl(dto, plan, zasyl);
 131        await FillTransitAsync(dto, plan, OneCStockGate.GetBlockingOrderIds(check));
 132        return dto;
 133    }
 134
 135    private async Task<HashSet<int>> AllowedIdsAsync(int[] ids, ClaimsPrincipal user, CancellationToken ct)
 136    {
 137        if (ids.Length == 0)
 138        {
 139            return new HashSet<int>();
 140        }
 141
 142        IQueryable<Order> query = _context.Orders.AsNoTracking().Where(o => ids.Contains(o.Id));
 143        var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
 144        if (_dataScope != null && !string.IsNullOrWhiteSpace(userId))
 145        {
 146            query = await _dataScope.ApplyDataScopeAsync(query, userId, "Order", null, ct);
 147        }
 148
 149        return (await query.Select(o => o.Id).ToListAsync(ct)).ToHashSet();
 150    }
 151
 152    /// <summary>Чипы «засыл → склад 1С» и засылы без связи складов (с данными для ссылки «Создать связь»).</summary>
 153    internal static void FillZasyl(ShipToSellerPreviewDto dto, ShipToSellerPlan plan, IReadOnlyDictionary<int, string?> 
 154    {
 155        var mappings = (IReadOnlyList<WarehouseMapping>?)plan.Mappings ?? Array.Empty<WarehouseMapping>();
 156        var chips = new Dictionary<string, ShipToSellerZasylChip>();
 157        var unmapped = new Dictionary<string, ShipToSellerUnmappedZasyl>();
 158
 159        foreach (var order in plan.MainOrders)
 160        {
 161            zasylRaw.TryGetValue(order.Id, out var raw);
 162            var zasyl = raw?.Trim();
 163            if (string.IsNullOrEmpty(zasyl))
 164            {
 165                var mapped = OneCTransferManager.ResolveWarehouseFor1C(order, null, plan.Mappings);
 166                if (string.IsNullOrWhiteSpace(mapped))
 167                {
 168                    continue;
 169                }
 170
 171                Bump(chips, $"|{mapped}", () => new ShipToSellerZasylChip(null, mapped, true, 0));
 172                continue;
 173            }
 174
 175            var resolution = ShippedToCommissionerWarehouseResolver.ResolveSender(order, mappings, zasyl);
 176            var warehouse = resolution.Warehouse;
 177            if (string.IsNullOrWhiteSpace(warehouse))
 178            {
 179                continue;
 180            }
 181
 182            Bump(chips, $"{zasyl}|{warehouse}|{resolution.FromMapping}", () => new ShipToSellerZasylChip(zasyl, warehous
 183            if (resolution.FromMapping)
 184            {
 185                continue;
 186            }
 187
 188            var key = $"{ShippedToSellerZasylResolver.OptionKey(zasyl)}|{order.OzonClientId}|{DeliveryTypeOf(order)}";
 189            if (unmapped.TryGetValue(key, out var existing))
 190            {
 191                unmapped[key] = existing with { Orders = existing.Orders + 1 };
 192            }
 193            else
 194            {
 195                unmapped[key] = new ShipToSellerUnmappedZasyl(zasyl, order.OzonClientId, order.OzonClient?.Name?.Trim(),
 196            }
 197        }
 198
 199        dto.ZasylChips = chips.Values.OrderBy(c => c.Zasyl is null).ThenBy(c => c.Zasyl).ToList();
 200        dto.ZasylUnmapped = unmapped.Values.ToList();
 201    }
 202
 203    private static string? DeliveryTypeOf(Order order)
 204        => ShippedToCommissionerWarehouseResolver.GetMappingDeliveryType(order);
 205
 206    private static void Bump(Dictionary<string, ShipToSellerZasylChip> chips, string key, Func<ShipToSellerZasylChip> cr
 207    {
 208        var chip = chips.TryGetValue(key, out var existing) ? existing : create();
 209        chips[key] = chip with { Orders = chip.Orders + 1 };
 210    }
 211
 212    private async Task FillTransitAsync(ShipToSellerPreviewDto dto, ShipToSellerPlan plan, HashSet<int> blocking)
 213    {
 214        if (plan.ZeroOrders.Count == 0)
 215        {
 216            return;
 217        }
 218
 219        var mappings = (IReadOnlyList<WarehouseMapping>?)plan.Mappings ?? Array.Empty<WarehouseMapping>();
 220        var existsCache = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
 221
 222        foreach (var order in plan.ZeroOrders)
 223        {
 224            var resolution = ShippedToCommissionerWarehouseResolver.ResolveTransitWarehouse(order, mappings);
 225            var sender = resolution.SenderWarehouse ?? string.Empty;
 226            var transit = resolution.TransitWarehouse ?? string.Empty;
 227            plan.ZeroOverrides.TryGetValue(order.Id, out var stockQty);
 228
 229            ShipToSellerTransitOutcome outcome;
 230            if (plan.ZeroStockIds.Contains(order.Id))
 231            {
 232                outcome = ShipToSellerTransitOutcome.NoStock;
 233            }
 234            else if (!await TransitExistsAsync(transit, existsCache))
 235            {
 236                outcome = ShipToSellerTransitOutcome.TransitUnavailable;
 237            }
 238            else
 239            {
 240                outcome = blocking.Contains(order.Id) ? ShipToSellerTransitOutcome.TransitDraft : ShipToSellerTransitOut
 241            }
 242
 243            dto.Transit.Add(new ShipToSellerTransitLine(order.Id, order.ShipmentNumber, order.Article, sender, transit, 
 244        }
 245    }
 246
 247    private async Task<bool> TransitExistsAsync(string transit, Dictionary<string, bool> cache)
 248    {
 249        if (string.IsNullOrWhiteSpace(transit) || _transfer == null)
 250        {
 251            return false;
 252        }
 253
 254        if (!cache.TryGetValue(transit, out var exists))
 255        {
 256            try
 257            {
 258                exists = await _transfer.WarehouseExistsAsync(transit);
 259            }
 260            catch
 261            {
 262                // 1С недоступна — не блокируем предпросмотр, считаем склад найденным (проверит само проведение).
 263                exists = true;
 264            }
 265
 266            cache[transit] = exists;
 267        }
 268
 269        return exists;
 270    }
 271}