< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.Transactions.ShippedToSellerTransactionController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/Transactions/ShippedToSellerTransactionController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 661
Coverable lines: 661
Total lines: 988
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 308
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/Transactions/ShippedToSellerTransactionController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authorization;
 2using Microsoft.AspNetCore.Mvc;
 3using Microsoft.AspNetCore.Mvc.Rendering;
 4using Microsoft.EntityFrameworkCore;
 5using Microsoft.Extensions.DependencyInjection;
 6using Newtonsoft.Json;
 7using OzonDomains;
 8using OzonDomains.Models;
 9using OzonOrdersWeb.Areas.Studio2.Controllers.Base;
 10using OzonOrdersWeb.Areas.Studio2.Helpers;
 11using OzonOrdersWeb.Areas.Studio2.Models.Transactions;
 12using OzonOrdersWeb.ViewModels.OrderViewModels;
 13using OzonRepositories.Context;
 14using PartsInfo.HttpUtils;
 15using Services.ApiServices;
 16using Services.ApiServices._1CApi;
 17using Services.ApiServices._1CApi.Models;
 18using Services.ApiServices._1CApi.Stock;
 19using Services.CacheServices.Cache.OzonOrdersCache;
 20using Services.CacheServices.Cache.UserCacheService;
 21using Services.DataServices;
 22using Services.FiltersServices.SortModels;
 23using OzonOrdersWeb.WebServices.Currency;
 24using Services.SignalRServices;
 25using Services.TransactionUtilsServices;
 26using Services.CacheServices.Cache;
 27using Services.StockSyncServices;
 28using OzonOrdersWeb.TransactionServices;
 29using Services.TransactionCommentAi;
 30
 31namespace OzonOrdersWeb.Areas.Studio2.Controllers.Transactions;
 32
 33[Authorize(Roles = "User,Admin")]
 34[Area("Studio2")]
 35public class ShippedToSellerTransactionController : Controller
 36{
 37    private readonly ApplicationDbContext _context;
 38    private readonly AppStatusDataServices _appStatusServices;
 39    private readonly OrdersDataServices _orderServices;
 40    private readonly TransactionManager _transaction;
 41    private readonly TransactionCache _transactionCache;
 42    private readonly SupplierDataServices _supplierDataServices;
 43    private readonly IUserCacheService _userCacheService;
 44    private readonly ICurrencyRatesViewBagService _currencyRatesViewBagService;
 45    private readonly CacheUpdater<Order> _cacheUpdater;
 46    private readonly OrderCache _cache;
 47    private readonly WarehouseMappingDataServices _warehouseMappingDataServices;
 48    private readonly OneCTransferManager _oneCTransferManager;
 49    private readonly ProxyHttpClientService _proxyHttpClientService;
 50    private readonly WarehouseDataServices _warehouseDataServices;
 51    private readonly OneCDataManager _oneCDataManager;
 52    private readonly StatusTransitionDataServices _statusTransitionDataServices;
 53    private readonly IWarehouseStockLookupService _warehouseStockLookup;
 54
 055    public ShippedToSellerTransactionController(
 056        ApplicationDbContext context,
 057        AppStatusDataServices appStatusServices,
 058        OrdersDataServices orderServices,
 059        TransactionManager transaction,
 060        TransactionCache transactionCache,
 061        SupplierDataServices supplierDataServices,
 062        IUserCacheService userCacheService,
 063        ICurrencyRatesViewBagService currencyRatesViewBagService,
 064        CacheUpdater<Order> cacheUpdater,
 065        OrderCache cache,
 066        WarehouseMappingDataServices warehouseMappingDataServices,
 067        OneCTransferManager oneCTransferManager,
 068        ProxyHttpClientService proxyHttpClientService,
 069        WarehouseDataServices warehouseDataServices,
 070        OneCDataManager oneCDataManager,
 071        StatusTransitionDataServices statusTransitionDataServices,
 072        IWarehouseStockLookupService warehouseStockLookup)
 73    {
 074        _context = context;
 075        _appStatusServices = appStatusServices;
 076        _orderServices = orderServices;
 077        _transaction = transaction;
 078        _transactionCache = transactionCache;
 079        _supplierDataServices = supplierDataServices;
 080        _userCacheService = userCacheService;
 081        _currencyRatesViewBagService = currencyRatesViewBagService;
 082        _cacheUpdater = cacheUpdater;
 083        _cache = cache;
 084        _warehouseMappingDataServices = warehouseMappingDataServices;
 085        _oneCTransferManager = oneCTransferManager;
 086        _proxyHttpClientService = proxyHttpClientService;
 087        _warehouseDataServices = warehouseDataServices;
 088        _oneCDataManager = oneCDataManager;
 089        _statusTransitionDataServices = statusTransitionDataServices;
 090        _warehouseStockLookup = warehouseStockLookup;
 091    }
 92
 93    // GET
 94    public async Task<IActionResult> CreateShippedToSellerTransaction(string ids, int page)
 95    {
 096        var transitions = await _statusTransitionDataServices.GetByTransactionType(TransactionType.ShippedToSeller);
 97        List<AppStatus> filteredStatuses;
 98        AppStatus targetStatus;
 099        HashSet<int>? validSourceStatusIds = null;
 100
 0101        if (transitions.Any())
 102        {
 0103            validSourceStatusIds = transitions.Select(t => t.FromStatusId).ToHashSet();
 0104            targetStatus = transitions.First().ToStatus!;
 0105            var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 0106            filteredStatuses = new List<AppStatus> { targetStatus };
 0107            if (cancelledStatus != null) filteredStatuses.Add(cancelledStatus);
 0108            filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 109        }
 110        else
 111        {
 0112            var appStatusShippedToSeller = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отгружен
 0113            if (appStatusShippedToSeller == null)
 114            {
 0115                appStatusShippedToSeller = new AppStatus() { Name = "Отгружен реализатором" };
 0116                _context.AppStatuses.Add(appStatusShippedToSeller);
 0117                await _context.SaveChangesAsync();
 118            }
 0119            var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 0120            filteredStatuses = new List<AppStatus>();
 0121            if (appStatusShippedToSeller != null) filteredStatuses.Add(appStatusShippedToSeller);
 0122            if (cancelledStatus != null) filteredStatuses.Add(cancelledStatus);
 0123            filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 0124            targetStatus = appStatusShippedToSeller!;
 0125        }
 126
 0127        ViewBag.AppStatuses = new SelectList(filteredStatuses, "Id", "Name");
 0128        ViewBag.StatusColors = filteredStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor());
 129
 0130        if (string.IsNullOrWhiteSpace(ids))
 0131            return BadRequest("Не выбраны заказы для транзакции.");
 132
 0133        int[] idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
 134
 0135        if (idArray.Length == 0)
 0136            return BadRequest("Не выбраны заказы для транзакции.");
 0137        List<Order> ordersToTransaction = new List<Order>();
 0138        var permittedWarehouses = (await _warehouseMappingDataServices.GetWarehouseMappings())
 0139            .Select(wm => wm.OzonWarehouseName)
 0140            .Where(n => !string.IsNullOrWhiteSpace(n))
 0141            .Select(WarehouseNameNormalizer.Normalize)
 0142            .OfType<string>()
 0143            .ToHashSet(StringComparer.OrdinalIgnoreCase);
 0144        string? errorReason = null;
 0145        var errorCode = TransactionErrorCode.Unknown;
 146
 147        // ВАЖНО: Загружаем заказы БЕЗ ТРЕКИНГА - изменения статусов для отображения
 148        // НЕ сохранятся в БД при отмене транзакции
 0149        var orders = await _orderServices.GetOrdersNoTracking(idArray);
 150
 0151        foreach (var concretOrder in orders)
 152        {
 0153            bool allowed = validSourceStatusIds != null
 0154                ? (concretOrder.AppStatusId.HasValue && validSourceStatusIds.Contains(concretOrder.AppStatusId.Value))
 0155                : (concretOrder.AppStatus?.Name == "Заказан реализатору");
 156
 0157            if (allowed)
 158            {
 159                // Устанавливаем целевой статус для ОТОБРАЖЕНИЯ в форме
 160                // Заказ без трекинга - изменения НЕ сохранятся в БД
 0161                if (concretOrder.AppStatus?.Name != "Отменен")
 0162                    concretOrder.AppStatus = targetStatus;
 0163                concretOrder.UpdatedBy = User.Identity?.Name;
 0164                if (concretOrder.FromFile == true
 0165                    && concretOrder.Supplier != null
 0166                    && !concretOrder.ShipmentWarehouseId.HasValue)
 167                {
 0168                    var curentWarehouse = await _warehouseDataServices.GetWarehouseAsync(
 0169                        new Warehouse { Name = concretOrder.Supplier.Name });
 0170                    if (curentWarehouse != null)
 171                    {
 0172                        concretOrder.ShipmentWarehouse = curentWarehouse;
 0173                        concretOrder.ShipmentWarehouseId = curentWarehouse.Id;
 174                    }
 175                }
 0176                ordersToTransaction.Add(concretOrder);
 177            }
 178            else
 179            {
 0180                errorReason = validSourceStatusIds != null
 0181                    ? $"Заказ {concretOrder.ShipmentNumber} имеет статус '{concretOrder.AppStatus?.Name}', недопустимый 
 0182                    : $"Заказ {concretOrder.ShipmentNumber} имеет статус '{concretOrder.AppStatus?.Name}', а должен быть
 0183                errorCode = TransactionErrorCode.InvalidSourceStatus;
 184            }
 0185        }
 186
 0187        if (!ordersToTransaction.Any())
 188        {
 0189            if (errorReason == null)
 0190                errorReason = validSourceStatusIds != null ? "Нет заказов с допустимым статусом для этой транзакции." : 
 0191            if (!string.IsNullOrWhiteSpace(errorReason) && errorCode == TransactionErrorCode.Unknown)
 0192                errorCode = TransactionErrorCode.NoOrdersForTransaction;
 193        }
 194        else
 195        {
 196            // проверка на поставщиков
 0197            var distinctSuppliers = ordersToTransaction
 0198                .GroupBy(o => o.Supplier?.Name)
 0199                .ToList();
 200
 0201            if (distinctSuppliers.Count > 1)
 202            {
 0203                errorReason = "Выбраны заказы с разными поставщиками: " +
 0204                              string.Join(", ", distinctSuppliers.SelectMany(g => g.Select(o =>
 0205                                  $"{o.ShipmentNumber} (поставщик: {g.Key})")));
 0206                ordersToTransaction.Clear();
 0207                errorCode = TransactionErrorCode.MixedSuppliers;
 208            }
 209
 0210            var invalidWarehouses = ordersToTransaction
 0211                .Where(o =>
 0212                {
 0213                    var effectiveWarehouse = WarehouseNameNormalizer.Normalize(
 0214                        TransactionWarehouseHelper.GetEffectiveWarehouseName(o));
 0215                    return string.IsNullOrWhiteSpace(effectiveWarehouse) ||
 0216                           !permittedWarehouses.Contains(effectiveWarehouse);
 0217                })
 0218                .ToList();
 219
 0220            if (invalidWarehouses.Any())
 221            {
 0222                var invalidWarehousesMessage = "Выбраны заказы с недопустимыми эффективными складами: " +
 0223                                               string.Join(", ", invalidWarehouses.Select(o =>
 0224                                                   $"{o.ShipmentNumber} ({TransactionWarehouseHelper.GetEffectiveWarehou
 0225                var allowedWarehousesMessage = "Допустимые склады: " +
 0226                                               string.Join(", ", permittedWarehouses);
 0227                errorReason = invalidWarehousesMessage + ". " + allowedWarehousesMessage;
 228
 0229                ordersToTransaction.Clear();
 0230                errorCode = TransactionErrorCode.MixedWarehouses;
 231            }
 232            else
 233            {
 234                // проверка на разные склады (только если все разрешённые)
 0235                var distinctWarehouses = ordersToTransaction
 0236                    .GroupBy(o => WarehouseNameNormalizer.Normalize(
 0237                        TransactionWarehouseHelper.GetEffectiveWarehouseName(o)))
 0238                    .ToList();
 239
 0240                if (distinctWarehouses.Count > 1)
 241                {
 0242                    errorReason = "Выбраны заказы с разными эффективными складами: " +
 0243                                  string.Join(", ", distinctWarehouses.SelectMany(g => g.Select(o =>
 0244                                      $"{o.ShipmentNumber} ({TransactionWarehouseHelper.GetEffectiveWarehouseLabel(o)})"
 0245                    ordersToTransaction.Clear();
 0246                    errorCode = TransactionErrorCode.MixedWarehouses;
 247                }
 248            }
 249        }
 250
 0251        ViewData["TransactionErrorCode"] = errorCode.ToString();
 0252        ViewData["ErrorReason"] = errorReason;
 253
 0254        var conditionsMessage = transitions.Any()
 0255            ? string.Join("<br/>", transitions
 0256                .Select(t => t.Conditions)
 0257                .Where(c => !string.IsNullOrWhiteSpace(c))
 0258                .Select(c => System.Net.WebUtility.HtmlEncode(c!.Trim()))
 0259                .Distinct())
 0260            : null;
 0261        ViewData["TransactionConditionsMessage"] = conditionsMessage;
 262
 263        // === ОПТИМИЗАЦИЯ: non-DB параллельно, DB последовательно ===
 0264        var userTask = _userCacheService.GetCachedUserAsync(User);
 0265        var rateTasks = _currencyRatesViewBagService.StartFetch();
 0266        var zasylTask = _oneCDataManager.GetWarehouseListAsync();
 267
 0268        var suppliers = await _supplierDataServices.GetSuppliers();
 0269        var articles = await _orderServices.GetArticleCountsForOrdersAsync(ordersToTransaction);
 0270        var cities = await _orderServices.GetDeliveryCityCountsForOrdersAsync(ordersToTransaction);
 0271        var numbers = await _orderServices.GetShipmentBaseCountsForOrdersAsync(ordersToTransaction);
 0272        var appStatusResult = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отгружен поставщиком"
 0273        var warehouses = await _warehouseDataServices.GetWarehouses();
 274
 0275        await Task.WhenAll(userTask, rateTasks.Usd, rateTasks.Eur, rateTasks.Byn, zasylTask);
 276
 0277        var rates = rateTasks.ToSnapshot();
 0278        var pageViewModel = new MultiplayEditOrderViewModel()
 0279        {
 0280            RedirectPage = page,
 0281            Orders = ordersToTransaction.OrderBy(ot => ot.ShipmentNumber).ToList(),
 0282            User = userTask.Result!,
 0283            Suppliers = suppliers.OrderBy(s => s.Name).ToList(),
 0284            RateUSD = rates.RateUsd,
 0285            RateEUR = rates.RateEur,
 0286            RateBYN = rates.RateByn,
 0287            UniqueArticles = articles,
 0288            UniqueDeliveryCitys = cities,
 0289            UniqueNumbers = numbers,
 0290            AppStatus = appStatusResult!,
 0291            ZasylValues = zasylTask.Result.Select(w => w.Name).Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!)
 0292            Warehouses = warehouses,
 0293        };
 0294        await AttachTrackingKeyAsync(pageViewModel, TransactionType.ShippedToSeller);
 295
 0296        if (pageViewModel.User.AccessProfileId != null)
 297        {
 0298            pageViewModel.User.AccessProfile = _context.AccessProfiles.Find(pageViewModel.User.AccessProfileId);
 299        }
 300
 0301        if (pageViewModel.User.AccessProfile?.AvailableOrderColumns?.Contains("Себестоимость") == true && ordersToTransa
 302        {
 0303            await _orderServices.LoadExtraExpensesForOrders(ordersToTransaction);
 304        }
 305
 0306        ViewBag.ClearPurchasePriceOnLoad = true;
 0307        if (pageViewModel.Orders.Count > 0)
 308        {
 0309            var warehouseC0ByOrderId = await _warehouseStockLookup.BuildC0MapAsync(
 0310                pageViewModel.Orders,
 0311                HttpContext.RequestAborted);
 0312            pageViewModel.WarehouseStockC0ByOrderId = warehouseC0ByOrderId;
 0313            foreach (var order in pageViewModel.Orders)
 314            {
 0315                if (warehouseC0ByOrderId.TryGetValue(order.Id, out var c0))
 0316                    order.OriginalPurchasePrice = c0;
 317            }
 318        }
 319
 0320        RunBackgroundCacheTask(pageViewModel.Orders);
 321
 0322        if (pageViewModel.Orders.Count > 0)
 323        {
 0324            var helper = CreateTransitHelper();
 0325            ViewData["QuantityZeroStatusName"] = LostByCommissionerTransitHelper.LostStatusName;
 0326            ViewData["TransitWarehouseGroups"] = await helper.GetTransitWarehouseGroupsAsync(pageViewModel.Orders);
 0327            ViewData["OrderInfoMap"] = await helper.BuildOrderInfoMapAsync(pageViewModel.Orders);
 0328            var mappingsForZasyl = await _warehouseMappingDataServices.GetWarehouseMappings();
 0329            ViewData["StockCheckSenderByOrderId"] = TransactionStockCheckLineBuilder.BuildStockCheckSenderMap(
 0330                pageViewModel.Orders, mappingsForZasyl);
 331            // Засыл задаёт склад списания, но в 1С уходит склад из связей складов — страница
 332            // пересчитывает его при выборе, поэтому варианты отдаём сразу все.
 0333            ViewData["ZasylSenderOptionsByOrderId"] = ShippedToSellerZasylResolver.BuildSenderOptions(
 0334                pageViewModel.Orders, mappingsForZasyl, warehouses.Select(w => w.Name));
 0335        }
 336
 0337        return View(pageViewModel);
 0338    }
 339
 340    private LostByCommissionerTransitHelper CreateTransitHelper() =>
 0341        new(_context, _appStatusServices, _warehouseMappingDataServices, _oneCTransferManager, _warehouseStockLookup);
 342
 343
 344    [HttpPost]
 345    [ValidateAntiForgeryToken]
 346    public async Task<IActionResult> CreateShippedToSellerTransaction(
 347        List<Order> orders,
 348        string userName,
 349        string? comment,
 350        int page,
 351        string? deletedOrders,
 352        Dictionary<int, string>? manufacturerByOrderId = null,
 353        bool processIn1C = false)
 354    {
 0355        var aiFlow = TransactionCommentAiSchedulingHelper.BeginFlow(
 0356            comment,
 0357            TransactionType.ShippedToSeller,
 0358            userName,
 0359            processIn1C);
 0360        comment = aiFlow.UserCommentOnly;
 0361        var trackingKey = Request.Form["TrackingKey"].FirstOrDefault();
 0362        var oneCOrganizationInn = Request.Form["oneCOrganizationInn"].FirstOrDefault();
 363        // Читаем вручную, чтобы DictionaryModelBinder не падал на форм-полях без orderSupplierMap[X]
 0364        var orderSupplierMap = Request.Form
 0365            .Where(kv => kv.Key.StartsWith("orderSupplierMap[") && kv.Key.EndsWith("]"))
 0366            .Where(kv => int.TryParse(kv.Key[17..^1], out _))
 0367            .ToDictionary(
 0368                kv => int.Parse(kv.Key[17..^1]),
 0369                kv => (string?)kv.Value.ToString());
 0370        if (orderSupplierMap.Count > 0) await _orderServices.SetZasylAsync(orderSupplierMap);
 0371        var missingOrdersResult = TransactionErrorResponseHelper.EnsureOrdersProvided(
 0372            this,
 0373            orders,
 0374            () => RedirectToAction("Index"));
 0375        if (missingOrdersResult != null)
 376        {
 0377            return missingOrdersResult;
 378        }
 379
 0380        ArgumentNullException.ThrowIfNull(orders);
 381
 0382        if (deletedOrders != null)
 383        {
 0384            var deletedOrderIds = DeletedOrderIds.Parse(deletedOrders);
 0385            orders = orders.Where(o => !deletedOrderIds.Contains(o.Id)).ToList();
 386        }
 387
 0388        foreach (var order in orders)
 389        {
 0390            if (order.ExtraExpenses != null && order.ExtraExpenses.Any())
 391            {
 0392                order.ExtraExpenses = order.ExtraExpenses
 0393                    .Where(e => e != null && e.ExpenseTypeId > 0 && e.Amount > 0)
 0394                    .ToList();
 0395                if (!order.ExtraExpenses.Any())
 0396                    order.ExtraExpenses = [];
 397            }
 398        }
 399
 400        // qty=0 в форме — явный сигнал «утеря»; null — поле не передано, берём количество из БД в TransactOrder
 0401        var zeroQtyOrders = orders.Where(o => o.Quantity is 0).ToList();
 402
 403        // Транзитную ветку (qty=0) готовим здесь, а не внутри try: её остатки WS нужны и для проверки
 404        // остатков 1С, и дальше в PrepareLostOrdersAsync — считать их дважды не нужно.
 0405        var lostHelper = CreateTransitHelper();
 0406        List<Order> zeroOrdersReloaded = [];
 0407        (Dictionary<int, decimal> overrides, HashSet<int> zeroStockIds)? zeroStockQty = null;
 408
 0409        if (zeroQtyOrders.Count > 0)
 410        {
 0411            var zeroIds = zeroQtyOrders.Select(o => o.Id).ToList();
 0412            zeroOrdersReloaded = await _context.Orders
 0413                .AsNoTracking()
 0414                .Include(o => o.OzonClient)
 0415                .Include(o => o.ShipmentWarehouse)
 0416                .Include(o => o.OrderExtensions)
 0417                .Include(o => o.EtProducer)
 0418                .Where(o => zeroIds.Contains(o.Id))
 0419                .ToListAsync();
 420
 0421            zeroStockQty = await lostHelper.BuildStockQtyAsync(zeroOrdersReloaded, manufacturerByOrderId);
 422        }
 423
 0424        var stockCheck = StockCheckResult.Empty;
 425
 426        // Заполненный засыл — это склад Ozon, а в документ 1С идёт склад из связей складов.
 427        // Резолвим один раз и отдаём готовые имена и проверке остатков, и перемещению.
 0428        var senderMapFor1C = new Dictionary<int, string>();
 429
 430        // Остатки спрашиваем до изменения статусов, одним запросом на обе ветки.
 0431        if (processIn1C)
 432        {
 0433            var mainOrders = orders.Where(o => o.Quantity is not 0).ToList();
 0434            var mainReloaded = await TransactionStockCheckLineBuilder.LoadForStockCheckAsync(
 0435                _context, mainOrders.Select(o => o.Id));
 0436            var warehouseMappings = await _warehouseMappingDataServices.GetWarehouseMappings();
 0437            senderMapFor1C = ShippedToSellerZasylResolver.BuildSenderMap(
 0438                mainReloaded, warehouseMappings, orderSupplierMap);
 0439            var zasylMap = senderMapFor1C.Count > 0
 0440                ? new Dictionary<int, string>(senderMapFor1C)
 0441                : null;
 442
 0443            var stockLines = TransactionStockCheckLineBuilder
 0444                .FromWarehouseMappings(
 0445                    mainReloaded,
 0446                    warehouseMappings,
 0447                    zasylMap,
 0448                    TransactionStockCheckLineBuilder.QuantitiesFromForm(mainOrders))
 0449                .ToList();
 450
 0451            if (zeroStockQty is { } zeroStock)
 452            {
 0453                var transitOrders = zeroOrdersReloaded.Where(o => !zeroStock.zeroStockIds.Contains(o.Id));
 0454                stockLines.AddRange(
 0455                    await lostHelper.BuildTransitStockCheckLinesAsync(transitOrders, zeroStock.overrides));
 456            }
 457
 0458            stockCheck = await OneCStockGate.RunAsync(HttpContext, stockLines);
 0459            await OneCStockGate.ReportAsync(HttpContext, stockCheck, "Отгружен реализатору");
 460
 0461        }
 462
 463        try
 464        {
 0465            var postTransitions = await _statusTransitionDataServices.GetByTransactionType(TransactionType.ShippedToSell
 0466            AppStatus appStatus = postTransitions.Any()
 0467                ? postTransitions.First().ToStatus!
 0468                : (await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отгружен реализатором" }))!;
 0469            HashSet<int>? validTargetStatusIds = postTransitions.Any() ? postTransitions.Select(t => t.ToStatusId).ToHas
 470
 0471            if (orders.Count != 0)
 472            {
 0473                DateTime createAt = DateTime.Now;
 0474                var changeCount = 0;
 0475                long transactionId = 0;
 0476                var dateTime = "";
 0477                List<Order> ordersToUpdate = new List<Order>();
 0478                List<string> ordersNotFoundInOzone = [];
 479
 0480                var orderIds = orders.Select(o => o.Id).Distinct().ToList();
 0481                var fromStatusByOrderId = await _context.Orders.AsNoTracking()
 0482                    .Where(o => orderIds.Contains(o.Id))
 0483                    .Select(o => new { o.Id, FromStatusId = o.AppStatusId ?? 0 })
 0484                    .ToDictionaryAsync(x => x.Id, x => x.FromStatusId);
 485
 0486                var submittedFields = SubmittedOrderFields.FromForm(Request.HasFormContentType ? Request.Form : null);
 0487                foreach (var order in orders.Where(o => o.Quantity is not 0))
 488                {
 0489                    var orderToUpdate = await _orderServices.TransactOrder(order, submittedFields.For(order.Id));
 0490                    if (orderToUpdate.FromFile == true
 0491                        && orderToUpdate.Supplier != null
 0492                        && !orderToUpdate.ShipmentWarehouseId.HasValue)
 493                    {
 0494                        var curentWarehouse = await _warehouseDataServices.GetWarehouseAsync(
 0495                            new Warehouse { Name = orderToUpdate.Supplier.Name });
 0496                        if (curentWarehouse != null)
 497                        {
 0498                            orderToUpdate.ShipmentWarehouse = curentWarehouse;
 0499                            orderToUpdate.ShipmentWarehouseId = curentWarehouse.Id;
 500                        }
 501                    }
 502
 503                    // Если заказ отменен - оставляем статус "Отменен", иначе устанавливаем основной статус транзакции
 0504                    if (orderToUpdate.AppStatus?.Name != "Отменен")
 505                    {
 0506                        orderToUpdate.AppStatus = appStatus;
 507                    }
 508
 0509                    ordersToUpdate.Add(orderToUpdate);
 0510                }
 511
 512
 513                // === Обработка заказов с qty=0 → «Утерян реализатором» ===
 0514                string lostOneCResultHtml = string.Empty;
 0515                if (zeroQtyOrders.Count > 0)
 516                {
 0517                    var lostPrep = await lostHelper.PrepareLostOrdersAsync(
 0518                        zeroQtyOrders, zeroOrdersReloaded, manufacturerByOrderId, processIn1C, oneCOrganizationInn, user
 0519                        OneCStockGate.GetBlockingOrderIds(stockCheck),
 0520                        precomputedStock: zeroStockQty);
 521
 0522                    if (!string.IsNullOrWhiteSpace(lostPrep.Fatal1CErrorHtml))
 523                    {
 0524                        TempData["TransactionResult"] = lostPrep.Fatal1CErrorHtml + lostPrep.OneCSuccessHtml + lostPrep.
 0525                        return Json(new { redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCooki
 526                    }
 527
 0528                    if (!lostPrep.AllOrdersSkipped)
 529                    {
 0530                        LostByCommissionerTransitHelper.RestoreOriginalQuantities(lostPrep.OrdersToProcess, zeroOrdersRe
 531
 0532                        var lostStatus = await _appStatusServices.GetAppStatusAsync(
 0533                            new AppStatus { Name = LostByCommissionerTransitHelper.LostStatusName });
 0534                        if (lostStatus == null)
 535                        {
 0536                            lostStatus = new AppStatus { Name = LostByCommissionerTransitHelper.LostStatusName };
 0537                            _context.AppStatuses.Add(lostStatus);
 0538                            await _context.SaveChangesAsync();
 539                        }
 540
 0541                        var lostOrdersForTransaction = new List<Order>();
 0542                        foreach (var formOrder in lostPrep.OrdersToProcess)
 543                        {
 0544                            var orderToUpdate = await _orderServices.TransactOrder(formOrder, submittedFields.For(formOr
 0545                            orderToUpdate.AppStatus = lostStatus;
 0546                            orderToUpdate.AppStatusId = lostStatus.Id;
 0547                            orderToUpdate.IsVerified = true;
 0548                            orderToUpdate.IsAccepted = true;
 0549                            lostOrdersForTransaction.Add(orderToUpdate);
 550                        }
 551
 0552                        await _context.SaveChangesAsync();
 0553                        var lostComment = comment;
 0554                        if (!string.IsNullOrWhiteSpace(lostPrep.CommentAppendix) && !aiFlow.ScheduleAiComment)
 0555                            lostComment += lostPrep.CommentAppendix;
 556
 0557                        var lostCommentForDb = TransactionCommentAiSchedulingHelper.ResolveDatabaseComment(
 0558                            aiFlow,
 0559                            lostComment);
 0560                        var (_, _, lostTransactionId) = await _transaction.CreateLostByCommissionerTransaction(
 0561                            lostOrdersForTransaction, userName, createAt, lostCommentForDb);
 562
 0563                        if (aiFlow.ScheduleAiComment && lostTransactionId > 0)
 564                        {
 0565                            var lostBuilder = new TransactionCommentAiContextBuilder(
 0566                                TransactionType.LostByCommissioner,
 0567                                userName,
 0568                                DateTime.UtcNow).WithFlags(processIn1C, false);
 0569                            if (lostPrep.TransferResultForHistory is { Count: > 0 })
 0570                                lostBuilder.WithOneCResults(lostPrep.TransferResultForHistory);
 571
 0572                            await TransactionCommentAiSchedulingHelper.TryScheduleAsync(
 0573                                HttpContext.RequestServices,
 0574                                lostBuilder,
 0575                                true,
 0576                                lostTransactionId,
 0577                                lostOrdersForTransaction,
 0578                                fromStatusByOrderId,
 0579                                _context,
 0580                                ct: HttpContext.RequestAborted);
 581                        }
 582
 0583                        if (lostPrep.TransferResultForHistory != null && lostPrep.OrdersForHistory is { Count: > 0 })
 584                        {
 0585                            var orderIdsForHistory = lostPrep.OrdersForHistory.Select(o => o.Id).ToList();
 0586                            var historyLostTransactionId = await _context.Orders
 0587                                .AsNoTracking()
 0588                                .Where(o => orderIdsForHistory.Contains(o.Id))
 0589                                .Select(o => o.TransactionId)
 0590                                .FirstOrDefaultAsync();
 591
 0592                            await OrderHistoryOneCPostingRecorder.TryRecordMovementAsync(
 0593                                HttpContext.RequestServices,
 0594                                lostPrep.OrdersForHistory,
 0595                                lostPrep.TransferResultForHistory,
 0596                                userName,
 0597                                historyLostTransactionId,
 0598                                lostPrep.HistoryWindowStart,
 0599                                lostPrep.HistorySender,
 0600                                lostPrep.HistoryRecipient);
 601                        }
 602
 0603                        lostOneCResultHtml = lostPrep.OneCSuccessHtml;
 0604                    }
 605
 0606                    if (!string.IsNullOrWhiteSpace(lostPrep.SkippedOrdersHtml))
 0607                        lostOneCResultHtml += lostPrep.SkippedOrdersHtml;
 0608                }
 609
 0610                var oldStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Заказан реализатору
 0611                var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 612
 0613                List<Order> ordersToTransaction = validTargetStatusIds != null
 0614                    ? ordersToUpdate.Where(o => o.AppStatus != null && validTargetStatusIds.Contains(o.AppStatus.Id) && 
 0615                    : ordersToUpdate.Where(o => ((appStatus != null && o.AppStatus?.Id == appStatus.Id) || (cancelledSta
 0616                var blockingOrderIds = OneCStockGate.GetBlockingOrderIds(stockCheck);
 0617                var ordersForOneC = ordersToTransaction.Where(o => !blockingOrderIds.Contains(o.Id)).ToList();
 0618                var ordersForOneCDraft = ordersToTransaction.Where(o => blockingOrderIds.Contains(o.Id)).ToList();
 619
 620
 0621                string oneCResult = "";
 0622                IEnumerable<string> oneCHash = [];
 0623                List<OneCResponse> transferResult = new List<OneCResponse>();
 0624                List<OneCResponse> draftTransferResult = new List<OneCResponse>();
 0625                if (processIn1C && ordersForOneC.Count > 0)
 626                {
 627                    try
 628                    {
 0629                        var transferSupplierMap = senderMapFor1C.Count > 0
 0630                            ? new Dictionary<int, string>(senderMapFor1C)
 0631                            : null;
 0632                        transferResult =
 0633                            await _oneCTransferManager.TransferStockByWarehouses(ordersForOneC,
 0634                                TransactionType.ShippedToSeller,
 0635                                transferSupplierMap,
 0636                                userName: userName,
 0637                                orgInnOverride: oneCOrganizationInn);
 0638                    }
 0639                    catch (Exception e)
 640                    {
 0641                        string msg = $"<div class='alert alert-danger'>" +
 0642                                     $"<strong>⚠️ Ошибка обработки в 1С</strong><br/>" +
 0643                                     $"{e.Message}</div>";
 0644                        foreach (var item in ordersToTransaction)
 645                        {
 0646                            item.AppStatus = oldStatus;
 647                        }
 648
 0649                        int ordersToOldResult = await _orderServices.UpdateOrders(ordersToTransaction);
 0650                        if (ordersToOldResult > 0)
 651                        {
 0652                            msg += $"<div class='alert alert-info'>" +
 0653                                   $"<strong>📊 Обновление статусов</strong><br/>" +
 0654                                   $"Статус 'Заказан поставщику' установлен для <strong>{ordersToOldResult}</strong> зак
 0655                                   $"</div>";
 656                        }
 657
 0658                        TempData["TransactionResult"] = msg;
 0659                        return Json(new
 0660                        {
 0661                            redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page })
 0662                        });
 663                    }
 664
 0665                    var successfulTransfers = transferResult.Where(tr => tr.Success).ToList();
 0666                    var failedTransfers = transferResult.Where(tr => !tr.Success).ToList();
 0667                    var postingFailures = successfulTransfers
 0668                        .Where(tr => !tr.IsPosted && !string.IsNullOrWhiteSpace(tr.PostingError))
 0669                        .ToList();
 670
 0671                    if (failedTransfers.Count > 0)
 672                    {
 673                        // Есть ошибки
 0674                        var errorMessages = failedTransfers.Select(tr => tr.Message).Distinct().Take(3);
 0675                        var errorCount = failedTransfers.Count;
 0676                        var successCount = successfulTransfers.Count;
 0677                        var totalCount = transferResult.Count;
 678
 0679                        string msg = $"<div class='alert alert-warning'>" +
 0680                                     $"<strong>⚠️ Частичный результат обработки в 1С</strong><br/>" +
 0681                                     $"Успешно: {successCount} из {totalCount} документов<br/>" +
 0682                                     $"С ошибками: {errorCount} документов<br/>" +
 0683                                     $"<small>Ошибки: {string.Join("; ", errorMessages)}" +
 0684                                     (errorCount > 3 ? "..." : "") + "</small>" +
 0685                                     "</div>";
 686
 687
 0688                        foreach (var item in ordersToTransaction)
 689                        {
 0690                            item.AppStatus = oldStatus;
 691                        }
 692
 0693                        int ordersToOldResult = await _orderServices.UpdateOrders(ordersToTransaction);
 0694                        if (ordersToOldResult > 0)
 695                        {
 0696                            msg += $"<div class='alert alert-info'>" +
 0697                                   $"<strong>📊 Обновление статусов</strong><br/>" +
 0698                                   $"Статус 'Заказан поставщику' установлен для <strong>{ordersToOldResult}</strong> зак
 0699                                   $"</div>";
 700                        }
 701
 0702                        TempData["TransactionResult"] = msg;
 0703                        return Json(new
 0704                        {
 0705                            redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page })
 0706                        });
 707                    }
 708
 0709                    if (postingFailures.Count > 0)
 710                    {
 0711                        foreach (var item in ordersToTransaction)
 0712                            item.AppStatus = oldStatus;
 713
 0714                        int ordersToOldResult = await _orderServices.UpdateOrders(ordersToTransaction);
 0715                        var mainSummary = OneCTransactionSummaryHelper.BuildAlertSummary(ordersToTransaction, transferRe
 0716                        var rollbackInfo = ordersToOldResult > 0
 0717                            ? $"<div class='alert alert-info mb-2'>" +
 0718                              $"<strong>📊 Откат статусов</strong><br/>" +
 0719                              $"Статус «Заказан реализатору» восстановлен для <strong>{ordersToOldResult}</strong> заказ
 0720                              $"</div>"
 0721                            : string.Empty;
 722
 0723                        oneCResult = CombineOneCResultSections(lostOneCResultHtml, mainSummary) + rollbackInfo;
 0724                        TempData["TransactionResult"] = oneCResult;
 0725                        await NotificationService.NotifyAllAsync(oneCResult);
 0726                        ClearSelectedIdsSession();
 0727                        if (orderSupplierMap != null) await _orderServices.SetZasylAsync(orderSupplierMap);
 0728                        return Json(new
 0729                        {
 0730                            redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page })
 0731                        });
 732                    }
 733
 0734                    oneCHash = successfulTransfers
 0735                        .Select(h => h.TransactionId)
 0736                        .Where(id => id != null)
 0737                        .Select(id => id!);
 0738                    if (ordersForOneCDraft.Any())
 739                    {
 0740                        var draftSupplierMap = senderMapFor1C.Count > 0
 0741                            ? new Dictionary<int, string>(senderMapFor1C)
 0742                            : null;
 0743                        draftTransferResult = await _oneCTransferManager.TransferStockByWarehouses(
 0744                            ordersForOneCDraft, TransactionType.ShippedToSeller, draftSupplierMap,
 0745                            userName: userName, orgInnOverride: oneCOrganizationInn, createAsDraft: true,
 0746                            draftReason: "недостаточно остатка в 1С. Проведите после оприходования товара.");
 0747                        transferResult.AddRange(draftTransferResult);
 748                    }
 0749                    oneCResult = OneCTransactionSummaryHelper.WrapSection("Проведённые документы 1С",
 0750                        OneCTransactionSummaryHelper.BuildAlertSummary(ordersForOneC, successfulTransfers))
 0751                        + OneCTransactionSummaryHelper.WrapSection("Черновики 1С по дефицитным позициям",
 0752                        OneCTransactionSummaryHelper.BuildAlertSummary(ordersForOneCDraft, draftTransferResult))
 0753                        + OneCStockGate.PartialPostingNotice(stockCheck, ordersForOneC.Count, draftTransferResult.Any(r 
 0754                        + OneCStockGate.SkippedNotice(stockCheck);
 0755                }
 0756                else if (processIn1C && ordersForOneCDraft.Any())
 757                {
 0758                    var draftSupplierMap = senderMapFor1C.Count > 0
 0759                        ? new Dictionary<int, string>(senderMapFor1C)
 0760                        : null;
 0761                    draftTransferResult = await _oneCTransferManager.TransferStockByWarehouses(
 0762                        ordersForOneCDraft, TransactionType.ShippedToSeller, draftSupplierMap,
 0763                        userName: userName, orgInnOverride: oneCOrganizationInn, createAsDraft: true,
 0764                        draftReason: "недостаточно остатка в 1С. Проведите после оприходования товара.");
 0765                    transferResult.AddRange(draftTransferResult);
 0766                    oneCResult = OneCTransactionSummaryHelper.WrapSection("Черновики 1С по дефицитным позициям",
 0767                        OneCTransactionSummaryHelper.BuildAlertSummary(ordersForOneCDraft, draftTransferResult))
 0768                        + OneCStockGate.PartialPostingNotice(stockCheck, 0, draftTransferResult.Any(r => r.Success));
 769                }
 0770                else if (processIn1C && ordersToTransaction.Count == 0 && zeroQtyOrders.Count > 0)
 771                {
 0772                    oneCResult = string.Empty;
 773                }
 0774                else if (!processIn1C)
 775                {
 0776                    oneCResult = OneCStockGate.NoDocumentNotice(stockCheck);
 777                }
 778
 0779                oneCResult = CombineOneCResultSections(lostOneCResultHtml, oneCResult);
 780
 0781                if (!string.IsNullOrWhiteSpace(oneCResult))
 0782                    await NotificationService.NotifyAllAsync(oneCResult);
 783
 0784                var historyWindowStart = DateTime.Now;
 0785                if (ordersToTransaction.Count > 0)
 786                {
 0787                    var commentWithOneC = comment;
 0788                    TransactionCommentAiSchedulingHelper.AppendOneCCommentDetails(
 0789                        aiFlow,
 0790                        ref commentWithOneC,
 0791                        ordersToTransaction,
 0792                        transferResult);
 0793                    var commentForDb = TransactionCommentAiSchedulingHelper.ResolveDatabaseComment(
 0794                        aiFlow,
 0795                        commentWithOneC);
 796
 0797                    (changeCount, dateTime, transactionId) = await _transaction.CreateShippedToSellerTransaction(ordersT
 0798                        userName,
 0799                        createAt,
 0800                        commentForDb);
 0801                    int cancelCount = ordersToTransaction.Count(o => o.AppStatus?.Name == "Отменен");
 802
 0803                    if (changeCount > 0)
 804                    {
 0805                        if (processIn1C && transferResult.Count > 0)
 806                        {
 0807                            await OrderHistoryOneCPostingRecorder.TryRecordMovementAsync(
 0808                                HttpContext.RequestServices,
 0809                                ordersForOneC,
 0810                                transferResult.Where(r => !r.IsDraft).ToList(),
 0811                                userName,
 0812                                transactionId > 0 ? (int?)transactionId : null,
 0813                                historyWindowStart);
 814                        }
 815
 0816                        var statusTransitionHistoryService = HttpContext.RequestServices.GetService<IStatusTransitionHis
 0817                        if (postTransitions.Any() && statusTransitionHistoryService != null)
 818                        {
 0819                            var user = await _userCacheService.GetCachedUserAsync(User);
 0820                            await statusTransitionHistoryService.RecordStatusTransitionHistoryAsync(
 0821                                TransactionType.ShippedToSeller,
 0822                                ordersToTransaction,
 0823                                fromStatusByOrderId,
 0824                                postTransitions,
 0825                                commentForDb,
 0826                                user?.AccessProfileId,
 0827                                createAt,
 0828                                null);
 829                        }
 830
 0831                        await _cacheUpdater.Update(_cache);
 0832                        var statusNameForMessage = string.Join(", ", ordersToTransaction.Select(o => o.AppStatus?.Name).
 0833                        if (string.IsNullOrEmpty(statusNameForMessage) && postTransitions.Any())
 0834                            statusNameForMessage = string.Join(", ", postTransitions.Select(t => t.ToStatus?.Name).Where
 0835                        if (string.IsNullOrEmpty(statusNameForMessage)) statusNameForMessage = "Отгружен реализатором";
 0836                        string msg = TransactionConfig.BuildStandardSuccessMessage(
 0837                            changeCount,
 0838                            cancelCount,
 0839                            dateTime,
 0840                            userName,
 0841                            commentForDb,
 0842                            orders,
 0843                            statusNameForMessage,
 0844                            oneCResult);
 0845                        await NotificationService.NotifyAllAsync(msg);
 0846                        TempData["TransactionResult"] = msg;
 0847                        await CompleteTrackingAsync(trackingKey, transactionId, ordersToTransaction.Count, commentForDb)
 848
 0849                        if (aiFlow.ScheduleAiComment && aiFlow.Builder != null && transactionId > 0)
 850                        {
 0851                            await TransactionCommentAiSchedulingHelper.TryScheduleAsync(
 0852                                HttpContext.RequestServices,
 0853                                aiFlow.Builder,
 0854                                true,
 0855                                (int)transactionId,
 0856                                ordersToTransaction,
 0857                                fromStatusByOrderId,
 0858                                _context,
 0859                                ct: HttpContext.RequestAborted);
 860                        }
 0861                    }
 0862                }
 0863                else if (!string.IsNullOrWhiteSpace(oneCResult))
 864                {
 0865                    TempData["TransactionResult"] = oneCResult;
 866                }
 867
 0868                await _transactionCache.Update();
 869
 0870                if (ordersNotFoundInOzone.Count > 0)
 871                {
 0872                    TempData["OrdersNotFoundInOzone"] =
 0873                        $"Заказы не найдены в системе Ozon - {ordersNotFoundInOzone.Aggregate((x, y) => x + ", " + y)}";
 874                }
 0875            }
 876
 0877            ClearSelectedIdsSession();
 0878            if (orderSupplierMap != null) await _orderServices.SetZasylAsync(orderSupplierMap);
 0879            foreach (var order in orders)
 880            {
 0881                var cookieKey = $"PurchasePrice_{order.Id}";
 0882                if (Request.Cookies.ContainsKey(cookieKey))
 883                {
 0884                    Response.Cookies.Delete(cookieKey);
 885                }
 886            }
 887
 0888            return Json(new
 0889                { redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page }) });
 890        }
 0891        catch (TransactionException ex)
 892        {
 0893            TempData["TransactionErrorCode"] = ex.Code.ToString();
 0894            TempData["ErorrResult"] = ex.Message;
 0895            if (orderSupplierMap != null) await _orderServices.SetZasylAsync(orderSupplierMap);
 0896            return Json(new
 0897                { redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page }) });
 898        }
 0899        catch (Exception ex)
 900        {
 0901            TempData["TransactionErrorCode"] = TransactionErrorCode.Unknown.ToString();
 0902            TempData["ErorrResult"] = $"Не удалось провести выбранные заказы ({ex.Message})";
 0903            if (orderSupplierMap != null) await _orderServices.SetZasylAsync(orderSupplierMap);
 0904            return Json(new
 0905                { redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page }) });
 906        }
 0907    }
 908
 909    private static string CombineOneCResultSections(string? transitHtml, string? mainHtml)
 910    {
 0911        var parts = new[] { transitHtml, mainHtml }.Where(s => !string.IsNullOrWhiteSpace(s));
 0912        return string.Concat(parts);
 913    }
 914
 915    private void RunBackgroundCacheTask(List<Order> orders)
 916    {
 0917        _ = Task.Run(async () =>
 0918        {
 0919            foreach (var order in orders)
 0920            {
 0921                try
 0922                {
 0923                    if (order.EtProducer != null && !string.IsNullOrEmpty(order.Article))
 0924                    {
 0925                        _ = await _proxyHttpClientService.GetJsonAsync(
 0926                            $"https://api.interparts.ru/detail-full-info/{order.EtProducer.Name}/{order.Article}");
 0927
 0928                        _ = await _proxyHttpClientService.GetJsonAsync(
 0929                            $"https://api.interparts.ru/product-information/product/?article_number={order.Article}&manu
 0930
 0931                        Console.WriteLine($"Кэширован для проведения: {order.EtProducer.Name}/{order.Article}");
 0932                    }
 0933                }
 0934                catch (Exception ex)
 0935                {
 0936                    Console.WriteLine($"Ошибка в фоне: {ex.Message}");
 0937                }
 0938            }
 0939        });
 0940    }
 941
 942    [HttpPost]
 943    public IActionResult ClearSelectedIdsSession()
 944    {
 0945        HttpContext.Session.SetString("selectedIds", JsonConvert.SerializeObject(new List<int>()));
 0946        return Ok();
 947    }
 948
 949    private async Task AttachTrackingKeyAsync(MultiplayEditOrderViewModel model, TransactionType transactionType)
 950    {
 0951        if (model.Orders == null || !model.Orders.Any())
 0952            return;
 0953        var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>();
 0954        if (trackingService == null)
 0955            return;
 0956        model.TrackingKey = await trackingService.StartSessionAsync(
 0957            User.Identity?.Name ?? "unknown",
 0958            model.User?.AccessProfileId,
 0959            transactionType,
 0960            model.Orders.Select(o => o.Id),
 0961            $"{Request.Path}{Request.QueryString}",
 0962            HttpContext.Connection.RemoteIpAddress?.ToString(),
 0963            Request.Headers.UserAgent.ToString());
 0964    }
 965
 966    private async Task CompleteTrackingAsync(string? trackingKey, long transactionId, int ordersCount, string? comment)
 967    {
 0968        if (string.IsNullOrWhiteSpace(trackingKey) || transactionId <= 0)
 0969            return;
 0970        var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>();
 0971        if (trackingService == null)
 0972            return;
 0973        await trackingService.CompleteSessionAsync(
 0974            trackingKey,
 0975            transactionId,
 0976            ordersCount,
 0977            comment,
 0978            $"/Studio2/Transaction/GetTransaction?transactionId={transactionId}");
 0979    }
 980
 981    private OrderSortState GetSortStateCookie()
 982    {
 0983        var cookie = Request.Cookies["SortState"];
 0984        if (!string.IsNullOrEmpty(cookie) && Enum.TryParse<OrderSortState>(cookie, out var saved))
 0985            return saved;
 0986        return OrderSortState.StandardState;
 987    }
 988}