< Summary

Information
Class: OzonOrdersWeb.WebServices.Transactions.Grid.Load.TransactionGridLoadService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/Grid/Load/TransactionGridLoadService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 319
Coverable lines: 319
Total lines: 609
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 211
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
LoadAsync()0%6480800%
ApplyWarehouseSuggestionsAsync()0%1560390%
ApplyDataScopeAsync()0%4260%
BuildStatusesAsync()0%342180%
BuildLookupsAsync()0%600240%
SafeC0Async()100%210%
FilterByOrderScopeAsync()0%342180%
FilterWarehouseNamesByOrderScopeAsync()0%156120%
FilterBitrixWarehouseNamesByScopeAsync()0%7280%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/Grid/Load/TransactionGridLoadService.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains;
 4using OzonDomains.Models;
 5using OzonOrdersWeb.Areas.Studio2.Controllers.Base;
 6using OzonOrdersWeb.WebServices.Currency;
 7using OzonOrdersWeb.WebServices.Orders.Grid;
 8using OzonOrdersWeb.WebServices.Transactions.Grid.Contracts;
 9using OzonOrdersWeb.WebServices.Transactions.Grid.Definitions;
 10using OzonRepositories.Context;
 11using Services.ApiServices;
 12using Services.ApiServices._1CApi;
 13using Services.BackgroundJobsServices;
 14using Services.ApiServices.OzonApi;
 15using Services.ApiSupplierSettingsServices;
 16using Services.Authorization.Services;
 17using Services.CacheServices.Cache.UserCacheService;
 18using Services.DataServices;
 19using Services.StockSyncServices;
 20using Services.TransactionUtilsServices;
 21
 22namespace OzonOrdersWeb.WebServices.Transactions.Grid.Load;
 23
 24public sealed record TransactionGridLoadRequest(
 25    IReadOnlyList<int> Ids,
 26    ClaimsPrincipal Principal,
 27    string? SourceUrl,
 28    string? ClientIp,
 29    string? UserAgent);
 30
 31public interface ITransactionGridLoadService
 32{
 33    Task<TransactionGridPageDto> LoadAsync(TransactionDefinition definition, TransactionGridLoadRequest request, Cancell
 34}
 35
 36/// <summary>
 37/// Страница проведения: строки через конвейер OrdersGrid (RLS, гейтинг колонок), редактируемая часть из заказов
 38/// БД, справочники по определению. Повторяет GET классических страниц (<c>CreateTransactionBase</c>), но отдаёт JSON.
 39/// </summary>
 40public sealed class TransactionGridLoadService : ITransactionGridLoadService
 41{
 42    private readonly ApplicationDbContext _context;
 43    private readonly IOrdersGridRowHydrator _hydrator;
 44    private readonly IOrdersGridScopedQuery _scoped;
 45    private readonly OrdersDataServices _orders;
 46    private readonly AppStatusDataServices _appStatuses;
 47    private readonly SupplierDataServices _suppliers;
 48    private readonly WarehouseDataServices _warehouses;
 49    private readonly ExpenseTypeDataServices _expenseTypes;
 50    private readonly StatusTransitionDataServices _transitions;
 51    private readonly ICurrencyRatesViewBagService _rates;
 52    private readonly IUserCacheService _userCache;
 53    private readonly IScopeFilterValuesService? _scopeValues;
 54    private readonly IDataScopeService? _dataScope;
 55    private readonly IWarehouseStockLookupService? _stockLookup;
 56    private readonly ApiSupplierWarehouseService? _apiSupplierWarehouses;
 57    private readonly OzonOrderCancellationService? _cancellation;
 58    private readonly OneCDataManager? _oneC;
 59    private readonly ITransactionTimeTrackingService? _tracking;
 60    private readonly WarehouseSelectionResolver? _warehouseResolver;
 61    private readonly OneCReceiptWarehouseHistoryResolver? _receiptHistory;
 62    private readonly ILogger<TransactionGridLoadService> _logger;
 63
 064    public TransactionGridLoadService(
 065        ApplicationDbContext context,
 066        IOrdersGridRowHydrator hydrator,
 067        IOrdersGridScopedQuery scoped,
 068        OrdersDataServices orders,
 069        AppStatusDataServices appStatuses,
 070        SupplierDataServices suppliers,
 071        WarehouseDataServices warehouses,
 072        ExpenseTypeDataServices expenseTypes,
 073        StatusTransitionDataServices transitions,
 074        ICurrencyRatesViewBagService rates,
 075        IUserCacheService userCache,
 076        ILogger<TransactionGridLoadService> logger,
 077        IScopeFilterValuesService? scopeValues = null,
 078        IDataScopeService? dataScope = null,
 079        IWarehouseStockLookupService? stockLookup = null,
 080        ApiSupplierWarehouseService? apiSupplierWarehouses = null,
 081        OzonOrderCancellationService? cancellation = null,
 082        OneCDataManager? oneC = null,
 083        ITransactionTimeTrackingService? tracking = null,
 084        WarehouseSelectionResolver? warehouseResolver = null,
 085        OneCReceiptWarehouseHistoryResolver? receiptHistory = null)
 86    {
 087        _context = context;
 088        _hydrator = hydrator;
 089        _scoped = scoped;
 090        _orders = orders;
 091        _appStatuses = appStatuses;
 092        _suppliers = suppliers;
 093        _warehouses = warehouses;
 094        _expenseTypes = expenseTypes;
 095        _transitions = transitions;
 096        _rates = rates;
 097        _userCache = userCache;
 098        _logger = logger;
 099        _scopeValues = scopeValues;
 0100        _dataScope = dataScope;
 0101        _stockLookup = stockLookup;
 0102        _apiSupplierWarehouses = apiSupplierWarehouses;
 0103        _cancellation = cancellation;
 0104        _oneC = oneC;
 0105        _tracking = tracking;
 0106        _warehouseResolver = warehouseResolver;
 0107        _receiptHistory = receiptHistory;
 0108    }
 109
 110    public async Task<TransactionGridPageDto> LoadAsync(TransactionDefinition d, TransactionGridLoadRequest request, Can
 111    {
 0112        var principal = request.Principal;
 0113        var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty;
 0114        var isAdmin = principal.IsInRole("Admin");
 0115        var warnings = new List<string>();
 0116        var skipped = new List<int>();
 117
 118        // 1. Ids → data scope → RLS → строки грида (в порядке ссылки).
 0119        var ids = request.Ids.Where(i => i > 0).Distinct().ToList();
 0120        var scopedIds = await ApplyDataScopeAsync(ids, userId, ct);
 0121        var rows = await _hydrator.HydrateAsync(scopedIds, principal, ct);
 0122        var rowById = rows.ToDictionary(r => r.Id);
 0123        skipped.AddRange(ids.Where(id => !rowById.ContainsKey(id)));
 0124        if (skipped.Count > 0)
 125        {
 0126            warnings.Add(skipped.Count == 1 ? $"Заказ #{skipped[0]} недоступен и пропущен." : $"Недоступных заказов проп
 127        }
 128
 129        // 2. Заказы БД (без трекинга) — для редактируемых полей и предупреждений.
 0130        var orders = rowById.Count > 0 ? await _orders.GetOrdersNoTracking(rowById.Keys.ToArray()) : new List<Order>();
 0131        var orderById = orders.ToDictionary(o => o.Id);
 0132        orders = ids.Where(orderById.ContainsKey).Select(id => orderById[id]).ToList();
 133
 134        // 3. Статусы: переходы из БД или дефолты определения.
 0135        var (statuses, targetStatusId, transitions) = await BuildStatusesAsync(d, userId, ct);
 136
 137        // 4. Правки по строкам.
 0138        var c0 = d.ClearPurchasePriceOnLoad && _stockLookup != null && orders.Count > 0
 0139            ? await SafeC0Async(orders, ct)
 0140            : new Dictionary<int, decimal>();
 141        // В выпадающем списке показываем обе стороны переходов и дополнительные статусы.
 142        // Та же совокупность считается допустимой для строки, иначе интерфейс предлагал статус, который тут же блокиров
 0143        var selectableStatusIds = statuses.Select(s => s.Id).ToHashSet();
 0144        var allowedNames = d.AllowedSourceStatuses.ToHashSet(StringComparer.OrdinalIgnoreCase);
 0145        var statusOnly = d.StatusOnlyWhenAlreadyIn.ToHashSet(StringComparer.OrdinalIgnoreCase);
 0146        var disallowMarket = d.DisallowMarketplaceStatuses.ToHashSet(StringComparer.OrdinalIgnoreCase);
 0147        var fromToTarget = transitions.GroupBy(t => t.FromStatusId).ToDictionary(g => g.Key, g => g.First().ToStatusId);
 148
 0149        var edits = new List<TransactionGridRowDto>(orders.Count);
 0150        foreach (var o in orders)
 151        {
 0152            var edit = new TransactionGridRowEdit
 0153            {
 0154                Id = o.Id,
 0155                SupplierId = o.SupplierId,
 0156                ShipmentWarehouseId = o.ShipmentWarehouseId,
 0157                Zasyl = TransactionWarehouseHelper.GetZasylValue(o),
 0158                OriginalPurchasePrice = o.PurchasePrice,
 0159                // Данные заказа — стартовое состояние транзакции. C0 остаётся только подсказкой,
 0160                // но не должен стирать уже заполненную цену закупки при переходе из OrdersGrid.
 0161                PurchasePrice = o.PurchasePrice,
 0162                WarehouseStockC0 = c0.TryGetValue(o.Id, out var c0Value) ? c0Value : null,
 0163                Price = o.Price,
 0164                CurrentPriceWithDiscount = o.ProductInfo?.CurrentPriceWithDiscount,
 0165                Quantity = o.Quantity,
 0166                OriginalQuantity = o.Quantity,
 0167                TrueQuantity = o.TrueQuantity,
 0168                ProductKey = o.ProductKey,
 0169                OrderNumberToSupplier = o.OrderNumberToSupplier,
 0170                Weight = o.ProductInfo?.Weight,
 0171                Manufacturer = o.Manufacturer?.Name,
 0172                ExtraExpenses = (o.ExtraExpenses ?? new List<OrderExtraExpense>())
 0173                    .Select(e => new TransactionGridExtraExpenseEdit { ExpenseTypeId = e.ExpenseTypeId, Amount = e.Amoun
 0174                    .ToList()
 0175            };
 176
 0177            var statusName = o.AppStatus?.Name ?? string.Empty;
 0178            if (d.PriceEditingOnly)
 179            {
 0180                edit.AppStatusId = o.AppStatusId;
 181            }
 0182            else if (statusOnly.Contains(statusName))
 183            {
 0184                edit.StatusOnly = true;
 0185                edit.AppStatusId = targetStatusId ?? o.AppStatusId;
 186            }
 187            // Для транзакций с явными переходами сохраняем их семантику: исходный статус
 188            // сопоставляется с целевым. Для status-only сценариев список содержит только
 189            // доступные для выбора целевые статусы, поэтому исходный разрешённый статус
 190            // (например «Не указан») должен сразу получать TargetStatusName.
 0191            else if (transitions.Count > 0 && selectableStatusIds.Count > 0)
 192            {
 0193                if (selectableStatusIds.Contains(o.AppStatusId ?? 0))
 194                {
 0195                    edit.AppStatusId = fromToTarget.TryGetValue(o.AppStatusId ?? 0, out var to) ? to : targetStatusId;
 196                }
 197                else
 198                {
 0199                    edit.AppStatusId = o.AppStatusId;
 0200                    edit.Warnings.Add($"Статус «{statusName}» не входит в допустимые для транзакции.");
 201                }
 202            }
 0203            else if (allowedNames.Count > 0 && !allowedNames.Contains(statusName))
 204            {
 0205                edit.AppStatusId = o.AppStatusId;
 0206                edit.Warnings.Add($"Статус «{statusName}», а должен быть: {string.Join(" / ", allowedNames)}.");
 207            }
 0208            else if (targetStatusId.HasValue)
 209            {
 0210                edit.AppStatusId = targetStatusId;
 211            }
 212            else
 213            {
 0214                edit.AppStatusId = targetStatusId ?? o.AppStatusId;
 215            }
 216
 0217            if (!string.IsNullOrEmpty(o.Status) && disallowMarket.Contains(o.Status))
 218            {
 0219                edit.Warnings.Add($"Статус маркетплейса «{o.Status}» — проведение недоступно.");
 220            }
 0221            else if (!d.PriceEditingOnly && d.TargetStatusName != null && !TransactionConfig.StandardOrderFilter(o))
 222            {
 0223                edit.Warnings.Add("Заказ отменён маркетплейсом.");
 224            }
 225
 0226            edits.Add(new TransactionGridRowDto { Base = rowById[o.Id], Edit = edit });
 227        }
 228
 0229        if (d.RequireSingleSupplierAndWarehouse && orders.Count > 1)
 230        {
 0231            var inns = orders.Select(o => o.Supplier?.INNCode?.Trim()).Where(i => !string.IsNullOrEmpty(i)).Distinct().T
 0232            if (inns.Count > 1)
 233            {
 0234                warnings.Add("Заказы от разных поставщиков (по ИНН) — приходная накладная требует одного поставщика.");
 235            }
 236
 0237            var whs = orders.Select(TransactionWarehouseHelper.GetEffectiveWarehouseName).Where(w => !string.IsNullOrWhi
 0238            if (whs.Count > 1)
 239            {
 0240                warnings.Add($"Заказы с разных складов: {string.Join(", ", whs)}.");
 241            }
 242        }
 243
 244        // 5. Справочники.
 0245        var lookups = await BuildLookupsAsync(d, orders, statuses, userId, isAdmin, ct);
 246
 247        // 5a. Подсказки складов 1С по режиму определения (как на GET классических страниц).
 0248        await ApplyWarehouseSuggestionsAsync(d, orders, edits, lookups, warnings, isAdmin);
 249
 250        // 6. Учёт времени.
 0251        string? trackingKey = null;
 0252        if (_tracking != null && orders.Count > 0)
 253        {
 254            try
 255            {
 0256                var cached = await _userCache.GetCachedUserAsync(principal);
 0257                trackingKey = await _tracking.StartSessionAsync(
 0258                    principal.Identity?.Name ?? string.Empty, cached?.AccessProfileId, d.Type,
 0259                    orders.Select(o => o.Id), request.SourceUrl, request.ClientIp, request.UserAgent);
 0260            }
 0261            catch (OperationCanceledException) when (ct.IsCancellationRequested)
 262            {
 0263                throw;
 264            }
 0265            catch (OperationCanceledException ex)
 266            {
 0267                _logger.LogInformation("TransactionsGrid: старт сессии учёта времени отменён ({Message})", ex.Message);
 0268            }
 0269            catch (Exception ex)
 270            {
 0271                _logger.LogWarning(ex, "TransactionsGrid: не удалось начать сессию учёта времени");
 0272            }
 273        }
 274
 0275        return new TransactionGridPageDto
 0276        {
 0277            Definition = TransactionDefinitionDto.From(d, targetStatusId),
 0278            Rows = edits,
 0279            Lookups = lookups,
 0280            Warnings = warnings,
 0281            SkippedIds = skipped,
 0282            TrackingKey = trackingKey,
 0283            IsAdmin = isAdmin
 0284        };
 0285    }
 286
 287    /// <summary>
 288    /// GlobalPair (WarehouseTransfer): отправитель/получатели по WarehouseSelectionResolver, не-админу отправитель фикс
 289    /// PerOrderRecipient (CustomerReturn): получатель по заказу. PerOrderBoth (ReturnToSupplier): отправитель по маппин
 290    /// возврата, получатель — по последнему проведённому приходу 1С для артикула.
 291    /// </summary>
 292    private async Task ApplyWarehouseSuggestionsAsync(TransactionDefinition d, List<Order> orders, List<TransactionGridR
 293    {
 0294        if (orders.Count == 0 || _warehouseResolver == null || d.OneCMode == OneCMode.None)
 295        {
 0296            return;
 297        }
 298
 299        try
 300        {
 0301            switch (d.WarehouseMode)
 302            {
 303                case WarehouseMode.GlobalPair:
 304                {
 0305                    var sender = await _warehouseResolver.ResolveSenderWarehouseAsync(orders);
 0306                    var recipient = await _warehouseResolver.ResolveRecipientWarehousesAsync(orders);
 0307                    lookups.SuggestedSenderWarehouse = sender.SenderWarehouseName;
 0308                    lookups.SuggestedRecipientWarehouses.AddRange(recipient.PreferredRecipientWarehouses);
 0309                    if (!isAdmin && sender.HasErrors)
 310                    {
 0311                        warnings.AddRange(sender.Errors);
 312                    }
 313
 0314                    warnings.AddRange(recipient.Warnings);
 0315                    break;
 316                }
 317                case WarehouseMode.PerOrderRecipient:
 318                {
 0319                    var res = await _warehouseResolver.ResolveCustomerReturnRecipientByOrderAsync(orders);
 0320                    foreach (var row in rows)
 321                    {
 0322                        if (res.RecipientByOrderId.TryGetValue(row.Edit.Id, out var wh))
 323                        {
 0324                            row.Edit.RecipientWarehouse = wh;
 0325                            row.Edit.RecipientSuggestions.Add(new TransactionGridWarehouseSuggestion(wh, null, res.Recip
 326                        }
 327                    }
 328
 0329                    if (res.HasBlockingErrors)
 330                    {
 0331                        warnings.AddRange(res.BlockingErrors);
 332                    }
 333
 0334                    break;
 335                }
 336                case WarehouseMode.PerOrderBoth:
 337                {
 0338                    var senders = await _warehouseResolver.ResolveCustomerReturnRecipientByOrderAsync(orders);
 0339                    foreach (var row in rows)
 340                    {
 0341                        if (senders.RecipientByOrderId.TryGetValue(row.Edit.Id, out var wh))
 342                        {
 0343                            row.Edit.SenderWarehouse = wh;
 344                        }
 345                    }
 346
 0347                    if (_receiptHistory != null && lookups.OneCWarehouses.Count > 0)
 348                    {
 0349                        var history = await _receiptHistory.ResolveByArticlesAsync(orders.Select(o => o.Article), lookup
 0350                        foreach (var row in rows)
 351                        {
 0352                            var article = orders.First(o => o.Id == row.Edit.Id).Article?.Trim();
 0353                            if (string.IsNullOrEmpty(article) || !history.TryGetValue(article, out var list) || list.Cou
 354                            {
 355                                continue;
 356                            }
 357
 0358                            row.Edit.RecipientWarehouse = list[0].Warehouse;
 0359                            row.Edit.RecipientSuggestions.AddRange(list.Select(x => new TransactionGridWarehouseSuggesti
 360                        }
 361                    }
 362
 363                    break;
 364                }
 365            }
 0366        }
 0367        catch (OperationCanceledException ex)
 368        {
 0369            _logger.LogInformation("TransactionsGrid: подсказки складов отменены ({Message})", ex.Message);
 0370        }
 0371        catch (Exception ex)
 372        {
 0373            _logger.LogWarning(ex, "TransactionsGrid: подсказки складов недоступны");
 0374            warnings.Add("Подсказки складов 1С недоступны — выберите склады вручную.");
 0375        }
 0376    }
 377
 378    private async Task<List<int>> ApplyDataScopeAsync(List<int> ids, string userId, CancellationToken ct)
 379    {
 0380        if (ids.Count == 0)
 381        {
 0382            return ids;
 383        }
 384
 0385        IQueryable<Order> q = _context.Orders.AsNoTracking().Where(o => ids.Contains(o.Id));
 0386        if (_dataScope != null && !string.IsNullOrWhiteSpace(userId))
 387        {
 0388            q = await _dataScope.ApplyDataScopeAsync(q, userId, "Order", null, ct);
 389        }
 390
 0391        var allowed = (await q.Select(o => o.Id).ToListAsync(ct)).ToHashSet();
 0392        return ids.Where(allowed.Contains).ToList();
 0393    }
 394
 395    /// <summary>Список статусов для выпадающего списка строки — как <c>CreateTransactionBase</c>: переходы ⊕ дополнител
 396    private async Task<(List<StatusOption> Statuses, int? TargetStatusId, List<StatusTransition> Transitions)> BuildStat
 397    {
 0398        var transitions = d.UseStatusTransitions ? await _transitions.GetByTransactionType(d.Type) : new List<StatusTran
 0399        var list = new List<AppStatus>();
 0400        AppStatus? target = null;
 401
 0402        if (transitions.Count > 0)
 403        {
 0404            list = transitions.Select(t => t.ToStatus).Concat(transitions.Select(t => t.FromStatus))
 0405                .Where(s => s != null).Select(s => s!).GroupBy(s => s.Id).Select(g => g.First()).OrderBy(s => s.Name).To
 0406            target = transitions.Select(t => t.ToStatus).FirstOrDefault(s => s != null);
 407        }
 0408        else if (!string.IsNullOrEmpty(d.TargetStatusName))
 409        {
 0410            target = await _appStatuses.GetAppStatusAsync(new AppStatus { Name = d.TargetStatusName });
 0411            if (target != null)
 412            {
 0413                list.Add(target);
 414            }
 415        }
 0416        else if (!d.PriceEditingOnly)
 417        {
 0418            list = (await _appStatuses.GetAppStatuses()).OrderBy(s => s.Name).ToList();
 419        }
 420
 0421        foreach (var extra in d.ExtraStatusNames)
 422        {
 0423            var s = await _appStatuses.GetAppStatusAsync(new AppStatus { Name = extra });
 0424            if (s != null && list.All(x => x.Id != s.Id))
 425            {
 0426                list.Add(s);
 427            }
 0428        }
 429
 0430        list = list.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 0431        list = await FilterByOrderScopeAsync(list, s => s.Id, "AppStatusId", userId, ct);
 432
 0433        var options = list.Select(s => new StatusOption(s.Id, s.Name ?? string.Empty, s.GetStatusColor(), s.IsFinal)).To
 0434        return (options, target?.Id, transitions);
 0435    }
 436
 437    private async Task<TransactionGridLookups> BuildLookupsAsync(TransactionDefinition d, List<Order> orders, List<Statu
 438    {
 0439        var rateTasks = _rates.StartFetch();
 0440        var suppliers = await FilterByOrderScopeAsync(await _suppliers.GetSuppliers(), s => s.Id, "SupplierId", userId, 
 0441        var warehouses = await FilterByOrderScopeAsync(await _warehouses.GetWarehouses(), w => w.Id, "ShipmentWarehouseI
 0442        var expenseTypes = (await _expenseTypes.GetExpenseTypes()).Where(e => e.IsActive).OrderBy(e => e.Name).ToList();
 443
 0444        var oneCWarehouseNames = new List<string>();
 0445        var needsOneC = d.OneCMode != OneCMode.None || d.EditableFields.Contains("zasyl");
 0446        if (needsOneC && _oneC != null)
 447        {
 448            try
 449            {
 0450                oneCWarehouseNames = (await _oneC.GetWarehouseListAsync()).Select(w => w.Name).Where(n => !string.IsNull
 0451            }
 0452            catch (OperationCanceledException) when (ct.IsCancellationRequested)
 453            {
 0454                throw;
 455            }
 0456            catch (OperationCanceledException ex)
 457            {
 0458                _logger.LogInformation("TransactionsGrid: получение списка складов 1С отменено ({Message})", ex.Message)
 0459            }
 0460            catch (Exception ex)
 461            {
 0462                _logger.LogWarning(ex, "TransactionsGrid: список складов 1С недоступен");
 0463            }
 464        }
 465
 0466        var zasylValues = await FilterWarehouseNamesByOrderScopeAsync(oneCWarehouseNames, userId, ct);
 0467        var bitrixWarehouses = d.OneCMode != OneCMode.None
 0468            ? await FilterBitrixWarehouseNamesByScopeAsync(oneCWarehouseNames, userId, ct)
 0469            : new List<string>();
 470
 0471        var cancelReasons = new Dictionary<int, OzonOrderCancelReasonsInfo>();
 0472        if (d.OzonCancelPanel && _cancellation != null && orders.Count > 0)
 473        {
 474            try
 475            {
 0476                cancelReasons = await _cancellation.LoadCancelReasonsAsync(orders);
 0477            }
 0478            catch (OperationCanceledException) when (ct.IsCancellationRequested)
 479            {
 0480                throw;
 481            }
 0482            catch (OperationCanceledException ex)
 483            {
 0484                _logger.LogInformation("TransactionsGrid: загрузка причин отмены Ozon отменена ({Message})", ex.Message)
 0485            }
 0486            catch (Exception ex)
 487            {
 0488                _logger.LogWarning(ex, "TransactionsGrid: причины отмены Ozon недоступны");
 0489            }
 490        }
 491
 0492        var allowedWarehouses = new Dictionary<string, List<int>>();
 0493        if (d.SupplierOffers && _apiSupplierWarehouses != null)
 494        {
 0495            allowedWarehouses = await _apiSupplierWarehouses.GetAllAllowedWarehouseIdsByProviderAsync(ct);
 496        }
 497
 0498        var rates = await rateTasks.ToSnapshotAsync();
 499
 500        // ИНН для договоров 1С: приход — поставщик заказов, расход — клиент; сами договоры грузятся лениво (Lookups?kin
 0501        string? treatiesInn = null;
 0502        if (d.ReceiptDocumentFields)
 503        {
 0504            treatiesInn = orders.Select(o => o.Supplier?.INNCode?.Trim()).FirstOrDefault(i => !string.IsNullOrEmpty(i));
 505        }
 0506        else if (d.ExpenseDocumentFields)
 507        {
 0508            treatiesInn = orders.Select(o => o.OzonClient?.INNCode?.Trim()).FirstOrDefault(i => !string.IsNullOrEmpty(i)
 509        }
 510
 0511        return new TransactionGridLookups
 0512        {
 0513            Statuses = statuses,
 0514            Suppliers = suppliers.OrderBy(s => s.Name).Select(s => new SupplierOption(s.Id, s.Name ?? string.Empty, s.Si
 0515            Warehouses = warehouses.OrderBy(w => w.Name).Select(w => new IdName(w.Id, w.Name ?? string.Empty)).ToList(),
 0516            ExpenseTypes = expenseTypes.Select(e => new IdName(e.Id, e.Name ?? string.Empty)).ToList(),
 0517            ZasylValues = zasylValues,
 0518            CancelReasonsByOrder = cancelReasons,
 0519            ApiSupplierAllowedWarehouses = allowedWarehouses,
 0520            RateUsd = rates.RateUsd,
 0521            RateEur = rates.RateEur,
 0522            RateByn = rates.RateByn,
 0523            OneCWarehouses = bitrixWarehouses,
 0524            ForceSenderWarehouse = d.WarehouseMode == WarehouseMode.GlobalPair && !isAdmin,
 0525            TreatiesInn = treatiesInn
 0526        };
 0527    }
 528
 529    private async Task<Dictionary<int, decimal>> SafeC0Async(List<Order> orders, CancellationToken ct)
 530    {
 531        try
 532        {
 0533            return await _stockLookup!.BuildC0MapAsync(orders, ct);
 534        }
 0535        catch (OperationCanceledException) when (ct.IsCancellationRequested)
 536        {
 0537            throw;
 538        }
 0539        catch (OperationCanceledException ex)
 540        {
 0541            _logger.LogInformation("TransactionsGrid: операция получения остатков C0 отменена ({Message})", ex.Message);
 0542            return new Dictionary<int, decimal>();
 543        }
 0544        catch (Exception ex)
 545        {
 0546            _logger.LogWarning(ex, "TransactionsGrid: остатки C0 недоступны");
 0547            return new Dictionary<int, decimal>();
 548        }
 0549    }
 550
 551    private async Task<List<T>> FilterByOrderScopeAsync<T>(List<T> source, Func<T, int> idSelector, string scopeField, s
 552    {
 0553        if (source.Count == 0 || _scopeValues == null || string.IsNullOrWhiteSpace(userId))
 554        {
 0555            return source;
 556        }
 557
 0558        var allowed = await _scopeValues.GetAllowedValuesAsync(userId, "Order", ct);
 0559        if (!allowed.TryGetValue(scopeField, out var raw) || raw.Count == 0)
 560        {
 0561            return source;
 562        }
 563
 0564        var ids = raw.Select(v => int.TryParse(v, out var id) ? id : (int?)null).Where(v => v.HasValue).Select(v => v!.V
 0565        return ids.Count == 0 ? source : source.Where(x => ids.Contains(idSelector(x))).ToList();
 0566    }
 567
 568    private async Task<List<string>> FilterWarehouseNamesByOrderScopeAsync(IEnumerable<string> names, string userId, Can
 569    {
 0570        var normalized = names.Where(n => !string.IsNullOrWhiteSpace(n)).Distinct(StringComparer.OrdinalIgnoreCase).Orde
 0571        if (normalized.Count == 0 || _scopeValues == null || string.IsNullOrWhiteSpace(userId))
 572        {
 0573            return normalized;
 574        }
 575
 0576        var allowed = await _scopeValues.GetAllowedValuesAsync(userId, "Order", ct);
 0577        if (!allowed.TryGetValue("ShipmentWarehouseId", out var raw) || raw.Count == 0)
 578        {
 0579            return normalized;
 580        }
 581
 0582        var ids = raw.Select(v => int.TryParse(v, out var id) ? id : (int?)null).Where(v => v.HasValue).Select(v => v!.V
 0583        if (ids.Count == 0)
 584        {
 0585            return normalized;
 586        }
 587
 0588        var allowedNames = (await _warehouses.GetWarehouses()).Where(w => ids.Contains(w.Id)).Select(w => w.Name)
 0589            .Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!).ToHashSet(StringComparer.OrdinalIgnoreCase);
 0590        return normalized.Where(allowedNames.Contains).OrderBy(n => n).ToList();
 0591    }
 592
 593    private async Task<List<string>> FilterBitrixWarehouseNamesByScopeAsync(IEnumerable<string> names, string userId, Ca
 594    {
 0595        var normalized = names.Where(n => !string.IsNullOrWhiteSpace(n)).Distinct(StringComparer.OrdinalIgnoreCase).Orde
 0596        if (_scopeValues == null || string.IsNullOrWhiteSpace(userId))
 597        {
 0598            return normalized;
 599        }
 600
 0601        var allowed = await _scopeValues.GetAllowedValuesAsync(userId, "BitrixWarehouse", ct);
 0602        if (!allowed.TryGetValue("Name", out var allowedNames) || allowedNames.Count == 0)
 603        {
 0604            return normalized;
 605        }
 606
 0607        return normalized.Where(allowedNames.Contains).OrderBy(n => n).ToList();
 0608    }
 609}