< Summary

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

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%210%
CreateReturnToWarehouseTransaction()0%5852760%
CreateReturnToWarehouseTransaction()0%5256720%
HasOneCTransactionGuid()0%4260%
CreateStatusAsync()100%210%
ClearSelectedIdsSession()100%210%
GetSortStateCookie()0%2040%
AttachTrackingKeyAsync()0%210140%
CompleteTrackingAsync()0%4260%

File(s)

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

#LineLine coverage
 1using Microsoft.AspNetCore.Authorization;
 2using Microsoft.AspNetCore.Mvc;
 3using Microsoft.AspNetCore.Mvc.Rendering;
 4using Microsoft.Extensions.DependencyInjection;
 5using Newtonsoft.Json;
 6using OzonDomains;
 7using OzonDomains.Models;
 8using OzonOrdersWeb.Areas.Studio2.Controllers.Base;
 9using OzonOrdersWeb.Areas.Studio2.Helpers;
 10using OzonOrdersWeb.Areas.Studio2.Models.Transactions;
 11using OzonOrdersWeb.ViewModels.OrderViewModels;
 12using OzonRepositories.Context;
 13using Services.ApiServices;
 14using Services.ApiServices._1CApi;
 15using Services.ApiServices._1CApi.Models;
 16using Services.CacheServices.Cache.OzonOrdersCache;
 17using Services.CacheServices.Cache.UserCacheService;
 18using Services.DataServices;
 19using Services.FiltersServices.SortModels;
 20using OzonOrdersWeb.WebServices.Currency;
 21using Services.SignalRServices;
 22using Services.TransactionUtilsServices;
 23using OzonOrdersWeb.TransactionServices;
 24using Services.CacheServices.Cache;
 25using System.Text.RegularExpressions;
 26using Microsoft.EntityFrameworkCore;
 27
 28namespace OzonOrdersWeb.Areas.Studio2.Controllers.Transactions;
 29
 30[Authorize(Roles = "User,Admin")]
 31[Area("Studio2")]
 32public class ReturnToWarehouseTransactionController : Controller
 33{
 34    private readonly ApplicationDbContext _context;
 35    private readonly AppStatusDataServices _appStatusServices;
 36    private readonly OrdersDataServices _orderServices;
 37    private readonly TransactionManager _transaction;
 38    private readonly TransactionCache _transactionCache;
 39    private readonly TransactionDataServices _transactionDataServices;
 40    private readonly SupplierDataServices _supplierDataServices;
 41    private readonly IUserCacheService _userCacheService;
 42    private readonly ICurrencyRatesViewBagService _currencyRatesViewBagService;
 43    private readonly CacheUpdater<Order> _cacheUpdater;
 44    private readonly OrderCache _cache;
 45    private readonly WarehouseDataServices _warehouseDataServices;
 46    private readonly OneCTransferManager _oneCTransferManager;
 47    private readonly StatusTransitionDataServices _statusTransitionDataServices;
 48
 49    // Регулярное выражение для проверки GUID формата 1C
 050    private static readonly Regex GuidRegex = new Regex(
 051        @"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
 052        RegexOptions.IgnoreCase | RegexOptions.Compiled);
 53
 054    public ReturnToWarehouseTransactionController(
 055        ApplicationDbContext context,
 056        AppStatusDataServices appStatusServices,
 057        OrdersDataServices orderServices,
 058        TransactionManager transaction,
 059        TransactionCache transactionCache,
 060        TransactionDataServices transactionDataServices,
 061        SupplierDataServices supplierDataServices,
 062        IUserCacheService userCacheService,
 063        ICurrencyRatesViewBagService currencyRatesViewBagService,
 064        CacheUpdater<Order> cacheUpdater,
 065        OrderCache cache,
 066        WarehouseDataServices warehouseDataServices,
 067        OneCTransferManager oneCTransferManager,
 068        StatusTransitionDataServices statusTransitionDataServices)
 69    {
 070        _context = context;
 071        _appStatusServices = appStatusServices;
 072        _orderServices = orderServices;
 073        _transaction = transaction;
 074        _transactionCache = transactionCache;
 075        _transactionDataServices = transactionDataServices;
 076        _supplierDataServices = supplierDataServices;
 077        _userCacheService = userCacheService;
 078        _currencyRatesViewBagService = currencyRatesViewBagService;
 079        _cacheUpdater = cacheUpdater;
 080        _cache = cache;
 081        _warehouseDataServices = warehouseDataServices;
 082        _oneCTransferManager = oneCTransferManager;
 083        _statusTransitionDataServices = statusTransitionDataServices;
 084    }
 85
 86    // GET: CreateReturnToWarehouseTransaction
 87    public async Task<IActionResult> CreateReturnToWarehouseTransaction(string ids, int page)
 88    {
 089        var transitions = await _statusTransitionDataServices.GetByTransactionType(TransactionType.ReturnToWarehouse);
 90        List<AppStatus> filteredStatuses;
 91        AppStatus targetStatus;
 092        HashSet<int>? validSourceStatusIds = null;
 93
 094        if (transitions.Any())
 95        {
 096            validSourceStatusIds = transitions.Select(t => t.FromStatusId).ToHashSet();
 097            targetStatus = transitions.First().ToStatus!;
 098            var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 099            filteredStatuses = new List<AppStatus> { targetStatus };
 0100            if (cancelledStatus != null) filteredStatuses.Add(cancelledStatus);
 0101            filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 102        }
 103        else
 104        {
 0105            var returnedToWarehouseStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Возвращен
 0106            var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 0107            filteredStatuses = new List<AppStatus>();
 0108            if (returnedToWarehouseStatus != null) filteredStatuses.Add(returnedToWarehouseStatus);
 0109            if (cancelledStatus != null) filteredStatuses.Add(cancelledStatus);
 0110            filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 0111            targetStatus = returnedToWarehouseStatus ?? await CreateStatusAsync("Возвращен на склад");
 0112        }
 113
 0114        ViewBag.AppStatuses = new SelectList(filteredStatuses, "Id", "Name");
 0115        ViewBag.StatusColors = filteredStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor());
 116
 0117        if (string.IsNullOrWhiteSpace(ids))
 0118            return BadRequest("Не выбраны заказы для транзакции.");
 119
 0120        int[] idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
 121
 0122        if (idArray.Length == 0)
 0123            return BadRequest("Не выбраны заказы для транзакции.");
 124
 0125        List<Order> ordersToTransaction = new();
 0126        string? errorReason = null;
 127
 0128        var allowedStatusesFallback = new[] { "ПринятНаСкладе", "Отгружен поставщиком", "Отгружен реализатором" };
 129
 130        // ВАЖНО: Загружаем заказы БЕЗ ТРЕКИНГА - изменения статусов для отображения
 131        // НЕ сохранятся в БД при отмене транзакции
 0132        var orders = await _orderServices.GetOrdersNoTracking(idArray);
 133
 0134        foreach (var order in orders)
 135        {
 0136            if (!await HasOneCTransactionGuid(order))
 137            {
 0138                errorReason = $"Заказ {order.ShipmentNumber} не был проведен в 1C (отсутствует GUID операции в комментар
 0139                continue;
 140            }
 141
 0142            bool allowed = validSourceStatusIds != null
 0143                ? (order.AppStatusId.HasValue && validSourceStatusIds.Contains(order.AppStatusId.Value))
 0144                : (order.AppStatus != null && allowedStatusesFallback.Contains(order.AppStatus.Name));
 145
 0146            if (allowed)
 147            {
 148                // Устанавливаем целевой статус для ОТОБРАЖЕНИЯ в форме
 149                // Заказ без трекинга - изменения НЕ сохранятся в БД
 0150                if (order.AppStatus?.Name != "Отменен")
 0151                    order.AppStatus = targetStatus;
 0152                order.UpdatedBy = User.Identity?.Name;
 0153                ordersToTransaction.Add(order);
 154            }
 155            else
 156            {
 0157                errorReason = validSourceStatusIds != null
 0158                    ? $"Заказ {order.ShipmentNumber} имеет статус приложения '{order.AppStatus?.Name}', недопустимый для
 0159                    : $"Заказ {order.ShipmentNumber} имеет статус приложения '{order.AppStatus?.Name}', а должен быть од
 160            }
 0161        }
 162
 0163        if (!ordersToTransaction.Any())
 164        {
 0165            return Json(new
 0166            {
 0167                success = false,
 0168                message = errorReason ?? "Нет подходящих заказов для проведения"
 0169            });
 170        }
 171
 172        // Проверка на одного клиента - разрешаем только если все заказы от одного клиента
 0173        var distinctClients = ordersToTransaction
 0174            .Where(o => o.OzonClient != null)
 0175            .GroupBy(o => o.OzonClient!.Id)
 0176            .ToList();
 177
 0178        if (distinctClients.Count > 1)
 179        {
 0180            errorReason =
 0181                "Нельзя проводить транзакцию для заказов от разных клиентов. Выберите заказы только от одного клиента.";
 0182            ordersToTransaction.Clear();
 183        }
 184
 0185        if (!ordersToTransaction.Any())
 186        {
 0187            return Json(new
 0188            {
 0189                success = false,
 0190                message = errorReason ?? "Нет подходящих заказов для проведения"
 0191            });
 192        }
 193
 0194        var sortState = HttpContext.Request.Cookies["OrderSortState"];
 0195        if (Enum.TryParse(sortState, out OrderSortState sortOrder))
 0196            ViewBag.OrderSortState = sortOrder;
 197
 0198        var conditionsMessage = transitions.Any()
 0199            ? string.Join("<br/>", transitions
 0200                .Select(t => t.Conditions)
 0201                .Where(c => !string.IsNullOrWhiteSpace(c))
 0202                .Select(c => System.Net.WebUtility.HtmlEncode(c!.Trim()))
 0203                .Distinct())
 0204            : null;
 0205        ViewData["TransactionConditionsMessage"] = conditionsMessage;
 206
 207        // === ОПТИМИЗАЦИЯ: non-DB параллельно, DB последовательно ===
 0208        var userTask = _userCacheService.GetCachedUserAsync(User);
 0209        var rateTasks = _currencyRatesViewBagService.StartFetch();
 210
 0211        var suppliers = await _supplierDataServices.GetSuppliers();
 0212        var articles = await _orderServices.GetArticleCountsForOrdersAsync(ordersToTransaction);
 0213        var cities = await _orderServices.GetDeliveryCityCountsForOrdersAsync(ordersToTransaction);
 0214        var numbers = await _orderServices.GetShipmentBaseCountsForOrdersAsync(ordersToTransaction);
 0215        var warehouses = await _warehouseDataServices.GetWarehouses();
 216
 0217        await Task.WhenAll(userTask, rateTasks.Usd, rateTasks.Eur, rateTasks.Byn);
 218
 0219        var rates = rateTasks.ToSnapshot();
 0220        var pageViewModel = new MultiplayEditOrderViewModel
 0221        {
 0222            RedirectPage = page,
 0223            Orders = ordersToTransaction.OrderBy(o => o.ShipmentNumber).ToList(),
 0224            User = (await _userCacheService.GetCachedUserAsync(User))!,
 0225            Suppliers = suppliers.OrderBy(s => s.Name).ToList(),
 0226            RateUSD = rates.RateUsd,
 0227            RateEUR = rates.RateEur,
 0228            RateBYN = rates.RateByn,
 0229            UniqueArticles = articles,
 0230            UniqueDeliveryCitys = cities,
 0231            UniqueNumbers = numbers,
 0232            AppStatus = targetStatus!,
 0233            ZasylValues = new List<string>(),
 0234            Warehouses = warehouses,
 0235        };
 0236        await AttachTrackingKeyAsync(pageViewModel, TransactionType.ReturnToWarehouse);
 237
 0238        if (pageViewModel.User?.AccessProfileId != null)
 239        {
 0240            pageViewModel.User.AccessProfile = _context.AccessProfiles.Find(pageViewModel.User.AccessProfileId);
 241        }
 242
 0243        if (pageViewModel.User?.AccessProfile?.AvailableOrderColumns?.Contains("Себестоимость") == true && ordersToTrans
 244        {
 0245            await _orderServices.LoadExtraExpensesForOrders(ordersToTransaction);
 246        }
 247
 248        // Устанавливаем флаг для блокировки всех полей в представлении
 0249        ViewBag.DisableAllFields = true;
 250
 0251        return View(pageViewModel);
 0252    }
 253
 254    // POST: CreateReturnToWarehouseTransaction
 255    [HttpPost]
 256    [ValidateAntiForgeryToken]
 257    public async Task<IActionResult> CreateReturnToWarehouseTransaction(
 258        List<Order> orders,
 259        string userName,
 260        string? comment,
 261        int page,
 262        string? deletedOrders,
 263        Dictionary<int, string?>? orderSupplierMap)
 264    {
 0265        var aiFlow = TransactionCommentAiSchedulingHelper.BeginFlow(
 0266            comment,
 0267            TransactionType.ReturnToWarehouse,
 0268            userName,
 0269            processIn1C: true);
 0270        comment = aiFlow.UserCommentOnly;
 0271        var trackingKey = Request.Form["TrackingKey"].FirstOrDefault();
 0272        var oneCOrganizationInn = Request.Form["oneCOrganizationInn"].FirstOrDefault();
 0273        var missingOrdersResult = TransactionErrorResponseHelper.EnsureOrdersProvided(
 0274            this,
 0275            orders,
 0276            () => RedirectToAction("Index", "Orders"));
 0277        if (missingOrdersResult != null)
 278        {
 0279            return missingOrdersResult;
 280        }
 281
 0282        orders ??= new List<Order>();
 283
 0284        if (!string.IsNullOrEmpty(deletedOrders))
 285        {
 0286            var deletedOrderIds = DeletedOrderIds.Parse(deletedOrders);
 0287            orders = orders.Where(o => !deletedOrderIds.Contains(o.Id)).ToList();
 288        }
 289
 290        // Очищаем пустые дополнительные расходы перед сохранением
 291        // Это гарантирует, что пустые элементы не попадут в базу данных
 0292        foreach (var order in orders)
 293        {
 0294            if (order.ExtraExpenses != null && order.ExtraExpenses.Any())
 295            {
 296                // Удаляем все расходы, где не заполнены оба поля (тип и сумма)
 0297                order.ExtraExpenses = order.ExtraExpenses
 0298                    .Where(e => e != null && e.ExpenseTypeId > 0 && e.Amount > 0)
 0299                    .ToList();
 300
 301                // Если список стал пустым, устанавливаем null
 0302                if (!order.ExtraExpenses.Any())
 303                {
 0304                    order.ExtraExpenses = new List<OrderExtraExpense>();
 305                }
 306            }
 307        }
 308
 309        try
 310        {
 0311            if (orders.Count != 0)
 312            {
 0313                DateTime createAt = DateTime.Now;
 0314                var changeCount = 0;
 0315                var dateTime = "";
 0316                List<Order> ordersToUpdate = new();
 0317                List<string> ordersNotFoundInOzone = new();
 318
 0319                var orderIds = orders.Select(o => o.Id).Distinct().ToList();
 0320                var fromStatusByOrderId = await _context.Orders.AsNoTracking()
 0321                    .Where(o => orderIds.Contains(o.Id))
 0322                    .Select(o => new { o.Id, FromStatusId = o.AppStatusId ?? 0 })
 0323                    .ToDictionaryAsync(x => x.Id, x => x.FromStatusId);
 324
 0325                var submittedFields = SubmittedOrderFields.FromForm(Request.HasFormContentType ? Request.Form : null);
 326
 327                // Проверяем, что все заказы были проведены в 1C
 0328                foreach (var order in orders)
 329                {
 0330                    if (!await HasOneCTransactionGuid(order))
 331                    {
 0332                        throw new TransactionException(
 0333                            TransactionErrorCode.OneCIntegrationFailed,
 0334                            $"Заказ {order.ShipmentNumber} не был проведен в 1C (отсутствует GUID операции в комментария
 335                    }
 336
 0337                    ordersToUpdate.Add(await _orderServices.TransactOrder(order, submittedFields.For(order.Id)));
 0338                }
 339
 340                // Проверка на одного клиента - разрешаем только если все заказы от одного клиента
 0341                var distinctClientsPost = ordersToUpdate
 0342                    .Where(o => o.OzonClient != null)
 0343                    .GroupBy(o => o.OzonClient!.Id)
 0344                    .ToList();
 345
 0346                if (distinctClientsPost.Count > 1)
 347                {
 0348                    throw new TransactionException(
 0349                        TransactionErrorCode.InvalidSourceStatus,
 0350                        "Нельзя проводить транзакцию для заказов от разных клиентов. Выберите заказы только от одного кл
 351                }
 352
 0353                var postTransitions = await _statusTransitionDataServices.GetByTransactionType(TransactionType.ReturnToW
 354                AppStatus returnedToWarehouseStatus;
 0355                HashSet<int>? validTargetStatusIds = null;
 0356                var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отменен" });
 357
 0358                if (postTransitions.Any())
 359                {
 0360                    validTargetStatusIds = postTransitions.Select(t => t.ToStatusId).ToHashSet();
 0361                    returnedToWarehouseStatus = postTransitions.First().ToStatus!;
 362                }
 363                else
 364                {
 0365                    returnedToWarehouseStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Возвр
 366                }
 367
 0368                List<Order> ordersToTransaction = validTargetStatusIds != null
 0369                    ? ordersToUpdate.Where(o => o.AppStatus != null && validTargetStatusIds.Contains(o.AppStatus.Id)).To
 0370                    : ordersToUpdate.Where(o => (returnedToWarehouseStatus != null && o.AppStatus?.Id == returnedToWareh
 371
 372                // Проводим транзакцию в 1C с обменом складами
 0373                List<OneCResponse> oneCResults = new();
 374                try
 375                {
 376                    // Создаем специальный метод для возврата на склад с обменом складами
 0377                    oneCResults = await _oneCTransferManager.TransferStockByWarehousesWithReversedWarehouses(
 0378                        ordersToTransaction,
 0379                        TransactionType.ReturnToWarehouse,
 0380                        userName: userName,
 0381                        orgInnOverride: oneCOrganizationInn);
 0382                }
 0383                catch (Exception ex)
 384                {
 0385                    throw new TransactionException(
 0386                        TransactionErrorCode.OneCIntegrationFailed,
 0387                        $"Ошибка при проведении транзакции в 1C: {ex.Message}",
 0388                        ex);
 389                }
 390
 391                int transactionId;
 0392                var historyWindowStart = DateTime.Now;
 0393                var commentWithOneC = comment;
 0394                TransactionCommentAiSchedulingHelper.AppendOneCCommentDetails(
 0395                    aiFlow,
 0396                    ref commentWithOneC,
 0397                    ordersToTransaction,
 0398                    oneCResults);
 0399                var commentForDb = TransactionCommentAiSchedulingHelper.ResolveDatabaseComment(aiFlow, commentWithOneC);
 400
 0401                (changeCount, dateTime, transactionId) = await _transaction.CreateReturnToWarehouseTransaction(
 0402                    ordersToTransaction,
 0403                    userName,
 0404                    createAt,
 0405                    commentForDb);
 406
 0407                var statusNameForMessage = string.Join(", ", ordersToTransaction.Select(o => o.AppStatus?.Name).Where(n 
 0408                if (string.IsNullOrEmpty(statusNameForMessage) && postTransitions.Any())
 0409                    statusNameForMessage = string.Join(", ", postTransitions.Select(t => t.ToStatus?.Name).Where(n => !s
 0410                if (string.IsNullOrEmpty(statusNameForMessage)) statusNameForMessage = "Возвращен на склад";
 0411                int returnCount = ordersToTransaction.Count;
 412
 0413                if (changeCount > 0)
 414                {
 0415                    if (oneCResults.Count > 0)
 416                    {
 0417                        await OrderHistoryOneCPostingRecorder.TryRecordMovementAsync(
 0418                            HttpContext.RequestServices,
 0419                            ordersToTransaction,
 0420                            oneCResults,
 0421                            userName,
 0422                            transactionId > 0 ? (int?)transactionId : null,
 0423                            historyWindowStart);
 424                    }
 425
 0426                    var statusTransitionHistoryService = HttpContext.RequestServices.GetService<IStatusTransitionHistory
 0427                    if (postTransitions.Any() && statusTransitionHistoryService != null)
 428                    {
 0429                        var user = await _userCacheService.GetCachedUserAsync(User);
 0430                        await statusTransitionHistoryService.RecordStatusTransitionHistoryAsync(
 0431                            TransactionType.ReturnToWarehouse,
 0432                            ordersToTransaction,
 0433                            fromStatusByOrderId,
 0434                            postTransitions,
 0435                            commentForDb,
 0436                            user?.AccessProfileId,
 0437                            createAt,
 0438                            null);
 439                    }
 440
 0441                    await _cacheUpdater.Update(_cache);
 0442                    string oneCResultMessage = "";
 443
 0444                    if (oneCResults.Any())
 445                    {
 0446                        var successfulTransfers = oneCResults.Where(tr => tr.Success).ToList();
 0447                        var failedTransfers = oneCResults.Where(tr => !tr.Success).ToList();
 448
 0449                        if (failedTransfers.Count > 0)
 450                        {
 451                            // Есть ошибки
 0452                            var errorMessages = failedTransfers.Select(tr => tr.Message).Distinct().Take(3);
 0453                            var errorCount = failedTransfers.Count;
 0454                            var successCount = successfulTransfers.Count;
 0455                            var totalCount = oneCResults.Count;
 456
 0457                            oneCResultMessage = $"<div class='alert alert-warning'>" +
 0458                                                $"<strong>⚠️ Частичный результат обработки в 1С</strong><br/>" +
 0459                                                $"Успешно: {successCount} из {totalCount} документов<br/>" +
 0460                                                $"С ошибками: {errorCount} документов<br/>" +
 0461                                                $"<small>Ошибки: {string.Join("; ", errorMessages)}" +
 0462                                                (errorCount > 3 ? "..." : "") + "</small>" +
 0463                                                "</div>";
 464                        }
 465                        else
 466                        {
 0467                            oneCResultMessage = OneCTransactionSummaryHelper.BuildAlertSummary(ordersToTransaction, oneC
 468                        }
 469                    }
 470                    else
 471                    {
 0472                        oneCResultMessage =
 0473                            "<div class='alert alert-info'><strong>ℹ️ Информация</strong><br/>Создание документов в 1С н
 474                    }
 475
 0476                    var extraHtml = "<div class='alert alert-info mb-2'>" +
 0477                                    $"<strong>Обработано возвратов:</strong> {returnCount}" +
 0478                                    "</div>" +
 0479                                    "<div class='small mb-2'>ID транзакции: " +
 0480                                    $"<a href=\"/Studio2/Transaction/GetTransaction?transactionId={transactionId}\">{tra
 0481                                    "</div>" +
 0482                                    oneCResultMessage;
 0483                    string msg = TransactionConfig.BuildStandardSuccessMessage(
 0484                        changeCount,
 0485                        0,
 0486                        dateTime,
 0487                        userName,
 0488                        commentForDb,
 0489                        orders,
 0490                        statusNameForMessage,
 0491                        extraHtml);
 0492                    await NotificationService.NotifyAllAsync(msg);
 0493                    TempData["TransactionResult"] = msg;
 0494                    await CompleteTrackingAsync(trackingKey, transactionId, ordersToUpdate.Count, commentForDb);
 495
 0496                    if (aiFlow.ScheduleAiComment && aiFlow.Builder != null && transactionId > 0)
 497                    {
 0498                        await TransactionCommentAiSchedulingHelper.TryScheduleAsync(
 0499                            HttpContext.RequestServices,
 0500                            aiFlow.Builder,
 0501                            true,
 0502                            transactionId,
 0503                            ordersToTransaction,
 0504                            fromStatusByOrderId,
 0505                            _context,
 0506                            ct: HttpContext.RequestAborted);
 507                    }
 0508                }
 509
 0510                await _transactionCache.Update();
 511
 0512                if (ordersNotFoundInOzone.Count > 0)
 513                {
 0514                    TempData["OrdersNotFoundInOzone"] = "Заказы не найдены в системе Ozon";
 515                }
 0516            }
 517
 0518            ClearSelectedIdsSession();
 519
 0520            foreach (var order in orders)
 521            {
 0522                var cookieKey = $"PurchasePrice_{order.Id}";
 0523                if (Request.Cookies.ContainsKey(cookieKey))
 524                {
 0525                    Response.Cookies.Delete(cookieKey);
 526                }
 527            }
 528
 0529            await _orderServices.SetZasylAsync(orderSupplierMap ?? new Dictionary<int, string?>());
 0530            return Json(new
 0531                { redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page = page }) });
 532        }
 0533        catch (TransactionException ex)
 534        {
 0535            TempData["TransactionErrorCode"] = ex.Code.ToString();
 0536            TempData["ErorrResult"] = ex.Message;
 0537            await _orderServices.SetZasylAsync(orderSupplierMap ?? new Dictionary<int, string?>());
 0538            return Json(new
 0539                { redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page = page }) });
 540        }
 0541        catch (Exception ex)
 542        {
 0543            TempData["TransactionErrorCode"] = TransactionErrorCode.Unknown.ToString();
 0544            TempData["ErorrResult"] = $"Не удалось провести выбранные заказы ({ex.Message})";
 0545            await _orderServices.SetZasylAsync(orderSupplierMap ?? new Dictionary<int, string?>());
 0546            return Json(new
 0547                { redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page = page }) });
 548        }
 0549    }
 550
 551    /// <summary>
 552    /// Проверяет, есть ли у заказа транзакция с GUID операции 1C в комментарии
 553    /// </summary>
 554    private async Task<bool> HasOneCTransactionGuid(Order order)
 555    {
 556        // Получаем все транзакции, в которых участвует этот заказ
 0557        var transactions = await _transactionDataServices.GetTransactions()
 0558            .Where(t => t.Orders.Any(o => o.Id == order.Id))
 0559            .Where(t => !string.IsNullOrEmpty(t.Comment))
 0560            .ToListAsync();
 561
 562        // Проверяем, есть ли в комментариях GUID формата 1C
 0563        foreach (var transaction in transactions)
 564        {
 0565            if (!string.IsNullOrEmpty(transaction.Comment) && GuidRegex.IsMatch(transaction.Comment))
 566            {
 0567                return true;
 568            }
 569        }
 570
 0571        return false;
 0572    }
 573
 574    private async Task<AppStatus> CreateStatusAsync(string name)
 575    {
 0576        var newStatus = new AppStatus { Name = name };
 0577        _context.AppStatuses.Add(newStatus);
 0578        await _context.SaveChangesAsync();
 0579        return newStatus;
 0580    }
 581
 582    [HttpPost]
 583    public IActionResult ClearSelectedIdsSession()
 584    {
 0585        HttpContext.Session.SetString("selectedIds", JsonConvert.SerializeObject(new List<int>()));
 0586        return Ok();
 587    }
 588
 589    private OrderSortState GetSortStateCookie()
 590    {
 0591        var sortStateCookie = Request.Cookies["SortState"];
 0592        if (!string.IsNullOrEmpty(sortStateCookie) &&
 0593            Enum.TryParse<OrderSortState>(sortStateCookie, out var savedSortState))
 594        {
 0595            return savedSortState;
 596        }
 597
 0598        return OrderSortState.StandardState;
 599    }
 600
 601    private async Task AttachTrackingKeyAsync(MultiplayEditOrderViewModel model, TransactionType transactionType)
 602    {
 0603        if (model.Orders == null || !model.Orders.Any())
 0604            return;
 0605        var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>();
 0606        if (trackingService == null)
 0607            return;
 0608        model.TrackingKey = await trackingService.StartSessionAsync(
 0609            User.Identity?.Name ?? "unknown",
 0610            model.User?.AccessProfileId,
 0611            transactionType,
 0612            model.Orders.Select(o => o.Id),
 0613            $"{Request.Path}{Request.QueryString}",
 0614            HttpContext.Connection.RemoteIpAddress?.ToString(),
 0615            Request.Headers.UserAgent.ToString());
 0616    }
 617
 618    private async Task CompleteTrackingAsync(string? trackingKey, long transactionId, int ordersCount, string? comment)
 619    {
 0620        if (string.IsNullOrWhiteSpace(trackingKey) || transactionId <= 0)
 0621            return;
 0622        var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>();
 0623        if (trackingService == null)
 0624            return;
 0625        await trackingService.CompleteSessionAsync(
 0626            trackingKey,
 0627            transactionId,
 0628            ordersCount,
 0629            comment,
 0630            $"/Studio2/Transaction/GetTransaction?transactionId={transactionId}");
 0631    }
 632}