< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.PercentageController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/PercentageController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 666
Coverable lines: 666
Total lines: 1147
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 310
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/PercentageController.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2using Microsoft.AspNetCore.Authorization;
 3using Microsoft.AspNetCore.Mvc;
 4using Microsoft.AspNetCore.Mvc.Rendering;
 5using Microsoft.EntityFrameworkCore;
 6using Microsoft.Extensions.Caching.Memory;
 7using Newtonsoft.Json;
 8using OfficeOpenXml;
 9using OzonDomains;
 10using OzonDomains.Models;
 11using OzonOrdersWeb.Areas.Studio2.ViewModels.Percentage;
 12using OzonOrdersWeb.ViewModels.OrderViewModels;
 13using OzonRepositories.Context;
 14using PartsInfo.HttpUtils;
 15using Services.ApiServices.OzonApi;
 16using Services.ApiServices.OzonApi.Filters;
 17using Services.CacheServices.Cache.OzonOrdersCache;
 18using Services.CacheServices.Cache.UserCacheService;
 19using Services.DataServices;
 20using Services.FiltersServices.SortModels;
 21using Services.ParserServices.FileProcessing;
 22using Services.PriceСlculationServices;
 23using Services.SignalRServices;
 24using Services.TransactionUtilsServices;
 25using OzonOrdersWeb.TransactionServices;
 26using Services.CacheServices.Cache;
 27using CsvHelper;
 28using OzonOrdersWeb.Areas.Studio2.Controllers.Base;
 29
 30namespace OzonOrdersWeb.Areas.Studio2.Controllers;
 31
 32[Authorize(Roles = "User,Admin")]
 33[Area("Studio2")]
 34public class PercentageController : Controller
 35{
 36    private const string PreferredDeliveryDateColumn = "Фактическая дата передачи в доставку";
 37    private const string PreferredReturnsStatusDateColumn = "Дата статуса";
 38
 39    private sealed class CachedApiReport
 40    {
 041        public required byte[] Bytes { get; init; }
 042        public required string FileName { get; init; }
 043        public string ContentType { get; init; } = "text/csv";
 44    }
 45
 46    private static bool IsSellerReturns(UploadFileViewModel model) =>
 047        model.UploadDataKind == PercentageUploadDataKind.SellerReturns;
 48
 49    private static string GetPreferredDateColumnName(UploadFileViewModel model) =>
 050        IsSellerReturns(model) ? PreferredReturnsStatusDateColumn : PreferredDeliveryDateColumn;
 51
 52    private static bool IsTransactionTypeAllowedForUploadKind(
 53        TransactionType transactionType,
 54        PercentageUploadDataKind uploadDataKind)
 55    {
 056        if (uploadDataKind == PercentageUploadDataKind.SellerReturns)
 057            return transactionType == TransactionType.CustomerReturn;
 58
 059        return transactionType != TransactionType.CustomerReturn;
 60    }
 61
 62    private static FormFile CreateFormFileFromCachedReport(CachedApiReport cachedReport)
 63    {
 064        var cachedStream = new MemoryStream(cachedReport.Bytes);
 065        var contentType = string.IsNullOrWhiteSpace(cachedReport.ContentType)
 066            ? "application/octet-stream"
 067            : cachedReport.ContentType;
 068        return new FormFile(cachedStream, 0, cachedReport.Bytes.Length, "excelFile", cachedReport.FileName)
 069        {
 070            Headers = new HeaderDictionary(),
 071            ContentType = contentType
 072        };
 73    }
 74
 75     private readonly ApplicationDbContext _context;
 76    private readonly AppStatusDataServices _appStatusServices;
 77    private readonly OrdersDataServices _orderServices;
 78    private readonly TransactionManager _transaction;
 79    private readonly TransactionCache _transactionCache;
 80
 81    private readonly SupplierDataServices _supplierDataServices;
 82    private readonly IUserCacheService _userCacheService;
 83    private readonly CurrencyRateFetcher _currencyRateFetcher;
 84    private readonly CacheUpdater<Order> _cacheUpdater;
 85    private readonly OrderCache _cache;
 86    private readonly ProxyHttpClientService _proxyHttpClientService;
 87    private readonly IFileProcessingService _fileProcessingService;
 88    private readonly OzonClientServices _ozonClientServices;
 89    private readonly IHttpClientFactory _httpClientFactory;
 90    private readonly SlqStudio.Application.Services.EmailService.IEmailService? _emailService;
 91    private readonly IMemoryCache _memoryCache;
 92
 093    public PercentageController(ApplicationDbContext context,
 094                                                AppStatusDataServices appStatusServices,
 095                                                OrdersDataServices orderServices,
 096                                                TransactionManager transaction,
 097                                                TransactionCache transactionCache,
 098                                                SupplierDataServices supplierDataServices,
 099                                                IUserCacheService userCacheService,
 0100                                                CurrencyRateFetcher currencyRateFetcher,
 0101                                                CacheUpdater<Order> cacheUpdater,
 0102                                                OrderCache cache,
 0103                                                ProxyHttpClientService proxyHttpClientService,
 0104                                                IFileProcessingService fileProcessingService,
 0105                                                OzonClientServices ozonClientServices,
 0106                                                IHttpClientFactory httpClientFactory,
 0107                                                SlqStudio.Application.Services.EmailService.IEmailService? emailService,
 0108                                                IMemoryCache memoryCache)
 109    {
 0110        _context = context;
 0111        _appStatusServices = appStatusServices;
 0112        _orderServices = orderServices;
 0113        _transaction = transaction;
 0114        _transactionCache = transactionCache;
 115
 0116        _supplierDataServices = supplierDataServices;
 0117        _userCacheService = userCacheService;
 0118        _currencyRateFetcher = currencyRateFetcher;
 0119        _cacheUpdater = cacheUpdater;
 0120        _cache = cache;
 0121        _proxyHttpClientService = proxyHttpClientService;
 0122        _fileProcessingService = fileProcessingService;
 0123        _ozonClientServices = ozonClientServices;
 0124        _httpClientFactory = httpClientFactory;
 0125        _emailService = emailService;
 0126        _memoryCache = memoryCache;
 0127    }
 128
 129    public async Task<IActionResult> UploadFile()
 130    {
 0131        return View(await BuildUploadFileViewModelAsync());
 0132    }
 133
 134    [HttpPost]
 135    [ValidateAntiForgeryToken]
 136    public async Task<IActionResult> UploadFile(UploadFileViewModel model)
 137    {
 0138        if (string.Equals(model.UploadSource, "file", StringComparison.OrdinalIgnoreCase))
 139        {
 0140            if (model.ExcelFile == null || model.ExcelFile.Length == 0)
 141            {
 0142                ModelState.AddModelError(nameof(model.ExcelFile), "Выберите файл для загрузки.");
 143            }
 144
 0145            if (string.IsNullOrWhiteSpace(model.SelectedDateColumn))
 146            {
 0147                ModelState.AddModelError(nameof(model.SelectedDateColumn), "Выберите столбец даты в предпросмотре.");
 148            }
 149
 0150            if (model.SelectedDateValues.Count == 0)
 151            {
 0152                ModelState.AddModelError(nameof(model.SelectedDateValues), "Выберите хотя бы одну дату, которую нужно об
 153            }
 154        }
 155
 0156        if (string.Equals(model.UploadSource, "ozon-api", StringComparison.OrdinalIgnoreCase)
 0157            && !string.IsNullOrWhiteSpace(model.ApiReportKey))
 158        {
 159            // API-отчёт уже загружен — требуем выбор дат
 0160            if (string.IsNullOrWhiteSpace(model.SelectedDateColumn))
 161            {
 0162                ModelState.AddModelError(nameof(model.SelectedDateColumn), "Выберите столбец даты в предпросмотре.");
 163            }
 164
 0165            if (model.SelectedDateValues.Count == 0)
 166            {
 0167                ModelState.AddModelError(nameof(model.SelectedDateValues), "Выберите хотя бы одну дату, которую нужно об
 168            }
 169        }
 170
 0171        if (!ModelState.IsValid)
 172        {
 0173            return View(await PopulateUploadFileViewModelAsync(model));
 174        }
 175
 176        try
 177        {
 0178            if (IsSellerReturns(model))
 179            {
 0180                var shipments = await ResolveShipmentNumbersAsync(model);
 0181                if (!shipments.Any())
 182                {
 0183                    TempData["Error"] = "Файл не содержит подходящих данных";
 0184                    return RedirectToAction("UploadFile");
 185                }
 186
 0187                var (shipmentOrderIds, _) = model.SelectedOzonClientId.HasValue
 0188                    ? await _orderServices.GetOrderIdsAndMatchedShipmentsByShipmentListAsync(
 0189                        shipments,
 0190                        model.SelectedOzonClientId.Value)
 0191                    : await _orderServices.GetOrderIdsAndMatchedShipmentsByShipmentListAsync(shipments);
 192
 0193                if (!shipmentOrderIds.Any())
 194                {
 0195                    TempData["Success"] = $"Обработано {shipments.Count} записей, заказов не найдено.";
 0196                    return RedirectToAction("UploadFile");
 197                }
 198
 0199                var idsCsv = string.Join(",", shipmentOrderIds);
 200
 0201                return RedirectToAction("SelectTransactionType", new
 0202                {
 0203                    ids = idsCsv,
 0204                    processedCount = shipments.Count,
 0205                    matchedCount = shipmentOrderIds.Count,
 0206                    uploadDataKind = model.UploadDataKind
 0207                });
 208            }
 209
 210            {
 0211                var processedOrders = await ResolveProcessedOrdersAsync(model);
 212
 0213                if (!processedOrders.Any())
 214                {
 0215                    TempData["Error"] = "Файл не содержит подходящих данных";
 0216                    return RedirectToAction("UploadFile");
 217                }
 218
 219                List<int> ids;
 220                try
 221                {
 0222                    ids = await _orderServices.GetOrderIdsByNumbersAndArticles(processedOrders);
 0223                }
 0224                catch (Exception ex)
 225                {
 0226                    TempData["Error"] = BuildUserFriendlyUploadError(ex, "Не удалось сопоставить строки файла с заказами
 0227                    return RedirectToAction("UploadFile");
 228                }
 229
 0230                if (!ids.Any())
 231                {
 0232                    TempData["Success"] = $"Обработано {processedOrders.Count} записей, заказов не найдено.";
 0233                    return RedirectToAction("UploadFile");
 234                }
 235
 0236                var idsCsv = string.Join(",", ids);
 237
 0238                return RedirectToAction("SelectTransactionType", new
 0239                {
 0240                    ids = idsCsv,
 0241                    processedCount = processedOrders.Count,
 0242                    matchedCount = ids.Count,
 0243                    uploadDataKind = model.UploadDataKind
 0244                });
 245            }
 246        }
 0247        catch (Exception ex)
 248        {
 0249            TempData["Error"] = BuildUserFriendlyUploadError(ex, "Не удалось обработать файл.");
 0250            return RedirectToAction("UploadFile");
 251        }
 0252    }
 253
 254    [HttpPost]
 255    [ValidateAntiForgeryToken]
 256    public async Task<IActionResult> PreviewUploadFile(UploadFileViewModel model)
 257    {
 0258        if (model.ExcelFile == null || model.ExcelFile.Length == 0)
 259        {
 0260            return BadRequest(new { error = "Выберите файл для предпросмотра." });
 261        }
 262
 263        try
 264        {
 265            FilePreviewResult preview;
 0266            var preferredDateColumnName = GetPreferredDateColumnName(model);
 0267            if (IsSellerReturns(model))
 268            {
 0269                preview = await _fileProcessingService.BuildPreviewShipmentOnlyAsync(model.ExcelFile, preferredDateColum
 270            }
 271            else
 272            {
 0273                preview = await _fileProcessingService.BuildPreviewAsync(model.ExcelFile, preferredDateColumnName);
 274            }
 275
 0276            if (!preview.HasRequiredColumns)
 277            {
 0278                return BadRequest(new { error = preview.ErrorMessage ?? "Не удалось разобрать файл." });
 279            }
 280
 0281            if (preview.DateColumns.Count == 0)
 282            {
 0283                return BadRequest(new { error = preview.ErrorMessage ?? "Не найден столбец с датами." });
 284            }
 285
 0286            var warning = preview.SelectedDateColumn == null
 0287                ? null
 0288                : string.Equals(preview.SelectedDateColumn, preferredDateColumnName, StringComparison.OrdinalIgnoreCase)
 0289                    ? null
 0290                    : $"Столбец '{preferredDateColumnName}' не найден. Используйте другой столбец с датами.";
 291
 0292            return Json(new
 0293            {
 0294                preview.OrderNumberColumn,
 0295                preview.ArticleColumn,
 0296                preview.SelectedDateColumn,
 0297                preview.TotalRows,
 0298                preview.RowsWithRequiredColumns,
 0299                preview.DateColumns,
 0300                preview.PreviewHeaders,
 0301                preview.PreviewRows,
 0302                Warning = warning
 0303            });
 304        }
 0305        catch (Exception ex)
 306        {
 0307            return BadRequest(new { error = BuildUserFriendlyUploadError(ex, "Не удалось построить предпросмотр файла.")
 308        }
 0309    }
 310
 311    [HttpPost]
 312    [ValidateAntiForgeryToken]
 313    public async Task<IActionResult> FetchApiReportPreview(UploadFileViewModel model)
 314    {
 0315        if (model.SelectedOzonClientId is null)
 0316            return BadRequest(new { error = "Не выбран Ozon-клиент." });
 317
 0318        if (!model.ProcessedAtFrom.HasValue || !model.ProcessedAtTo.HasValue)
 0319            return BadRequest(new { error = "Укажите период формирования отчёта." });
 320
 0321        if (model.ProcessedAtFrom.Value > model.ProcessedAtTo.Value)
 0322            return BadRequest(new { error = "Дата начала периода не может быть больше даты окончания." });
 323
 0324        if (model.ProcessedAtTo.Value - model.ProcessedAtFrom.Value > TimeSpan.FromDays(92))
 0325            return BadRequest(new { error = "Период отчёта не должен превышать 3 месяца." });
 326
 327        try
 328        {
 0329            var ozonClient = await _ozonClientServices.GetOzonClientAsync(model.SelectedOzonClientId.Value);
 0330            if (ozonClient == null || string.IsNullOrWhiteSpace(ozonClient.DecryptClientId) ||
 0331                string.IsNullOrWhiteSpace(ozonClient.DecryptApiKey))
 0332                return BadRequest(new { error = "Не удалось загрузить настройки выбранного Ozon-клиента." });
 333
 0334            var apiManager = new OzonApiDataManager(
 0335                _httpClientFactory, _emailService,
 0336                ozonClient.DecryptClientId, ozonClient.DecryptApiKey);
 337
 338            string reportCode;
 339            byte[] fileBytes;
 340
 0341            if (IsSellerReturns(model))
 342            {
 0343                reportCode = await apiManager.CreateSellerReturnsReportAsync(
 0344                    DateTime.SpecifyKind(model.ProcessedAtFrom!.Value, DateTimeKind.Utc),
 0345                    DateTime.SpecifyKind(model.ProcessedAtTo!.Value, DateTimeKind.Utc));
 0346                var returnsFileUrl = await apiManager.WaitForReportReady(reportCode);
 0347                fileBytes = await DownloadReportBytesAsync(returnsFileUrl);
 348            }
 349            else
 350            {
 0351                var reportRequest = new PostingsReportRequest
 0352                {
 0353                    Filter = new PostingsReportRequestFilter
 0354                    {
 0355                        ProcessedAtFrom = DateTime.SpecifyKind(model.ProcessedAtFrom!.Value, DateTimeKind.Utc),
 0356                        ProcessedAtTo = DateTime.SpecifyKind(model.ProcessedAtTo!.Value, DateTimeKind.Utc),
 0357                        DeliverySchema = ["fbs"],
 0358                        IsExpress = model.IsExpress,
 0359                        Sku = ParseStringArray(model.SkuList),
 0360                        CancelReasonId = [],
 0361                        OfferId = model.OfferId?.Trim() ?? string.Empty,
 0362                        StatusAlias = ParseStringArray(model.StatusAliasList),
 0363                        Statuses = ParseStringArray(model.StatusesList),
 0364                        Title = model.Title?.Trim() ?? string.Empty,
 0365                        WarehouseId = ParseLongArray(model.WarehouseIds),
 0366                        DeliveryMethodId = ParseLongArray(model.DeliveryMethodIds)
 0367                    },
 0368                    Language = "DEFAULT",
 0369                    With = new PostingsReportRequestWith
 0370                    {
 0371                        AdditionalData = model.IncludeAdditionalData,
 0372                        AnalyticsData = model.IncludeAnalyticsData,
 0373                        CustomerData = model.IncludeCustomerData,
 0374                        JewelryCodes = model.IncludeJewelryCodes
 0375                    }
 0376                };
 377
 0378                reportCode = await apiManager.GetReportCode(reportRequest);
 0379                var fileUrl = await apiManager.WaitForReportReady(reportCode);
 0380                fileBytes = await DownloadReportBytesAsync(fileUrl);
 381            }
 382
 0383            var reportFileName = BuildReportFileName(reportCode);
 0384            var contentType = GuessContentTypeForReportFile(reportFileName);
 385
 0386            await using var stream = new MemoryStream(fileBytes);
 0387            var reportFile = new FormFile(stream, 0, fileBytes.Length, "excelFile", reportFileName)
 0388            {
 0389                Headers = new HeaderDictionary(),
 0390                ContentType = contentType
 0391            };
 392
 0393            var preferredDateColumnName = GetPreferredDateColumnName(model);
 0394            var preview = IsSellerReturns(model)
 0395                ? await _fileProcessingService.BuildPreviewShipmentOnlyAsync(reportFile, preferredDateColumnName)
 0396                : await _fileProcessingService.BuildPreviewAsync(reportFile, preferredDateColumnName);
 397
 0398            if (!preview.HasRequiredColumns)
 0399                return BadRequest(new { error = preview.ErrorMessage ?? "Не удалось разобрать файл отчёта." });
 400
 0401            var reportKey = Guid.NewGuid().ToString("N");
 0402            _memoryCache.Set($"api_report_{reportKey}", new CachedApiReport
 0403            {
 0404                Bytes = fileBytes,
 0405                FileName = reportFile.FileName,
 0406                ContentType = contentType
 0407            }, TimeSpan.FromMinutes(15));
 408
 0409            var warning = preview.SelectedDateColumn == null
 0410                ? null
 0411                : string.Equals(preview.SelectedDateColumn, preferredDateColumnName, StringComparison.OrdinalIgnoreCase)
 0412                    ? null
 0413                    : $"Столбец '{preferredDateColumnName}' не найден. Используйте другой столбец с датами.";
 414
 0415            return Json(new
 0416            {
 0417                reportKey,
 0418                fileName = reportFile.FileName,
 0419                preview.OrderNumberColumn,
 0420                preview.ArticleColumn,
 0421                preview.SelectedDateColumn,
 0422                preview.TotalRows,
 0423                preview.RowsWithRequiredColumns,
 0424                preview.DateColumns,
 0425                preview.PreviewHeaders,
 0426                preview.PreviewRows,
 0427                Warning = warning
 0428            });
 0429        }
 0430        catch (Exception ex)
 431        {
 0432            return BadRequest(new { error = BuildUserFriendlyUploadError(ex, "Не удалось получить отчёт из Ozon API.") }
 433        }
 0434    }
 435
 436    [HttpGet]
 437    public IActionResult DownloadApiReport(string reportKey)
 438    {
 0439        if (string.IsNullOrWhiteSpace(reportKey))
 440        {
 0441            return BadRequest("Не указан ключ отчёта.");
 442        }
 443
 444        try
 445        {
 0446            if (!_memoryCache.TryGetValue($"api_report_{reportKey}", out CachedApiReport? cachedReport) || cachedReport 
 447            {
 0448                return NotFound("Сессия загрузки отчёта истекла. Загрузите отчёт заново.");
 449            }
 450
 0451            return File(cachedReport.Bytes, cachedReport.ContentType, cachedReport.FileName);
 452        }
 0453        catch (Exception ex)
 454        {
 0455            return BadRequest(BuildUserFriendlyUploadError(ex, "Не удалось скачать сохранённый CSV-файл."));
 456        }
 0457    }
 458
 459    [HttpPost]
 460    [ValidateAntiForgeryToken]
 461    public async Task<IActionResult> AnalyzeUploadFile(UploadFileViewModel model)    {
 0462        var isFileUpload = string.Equals(model.UploadSource, "file", StringComparison.OrdinalIgnoreCase);
 0463        var isApiUpload = string.Equals(model.UploadSource, "ozon-api", StringComparison.OrdinalIgnoreCase)
 0464            && !string.IsNullOrWhiteSpace(model.ApiReportKey);
 465
 0466        if (!isFileUpload && !isApiUpload)
 467        {
 0468            return BadRequest(new { error = "Сначала загрузите файл или отчёт через API." });
 469        }
 470
 0471        if (isFileUpload && (model.ExcelFile == null || model.ExcelFile.Length == 0))
 472        {
 0473            return BadRequest(new { error = "Выберите файл для анализа." });
 474        }
 475
 0476        if (string.IsNullOrWhiteSpace(model.SelectedDateColumn))
 477        {
 0478            return BadRequest(new { error = "Выберите столбец даты в предпросмотре." });
 479        }
 480
 0481        if (model.SelectedDateValues.Count == 0)
 482        {
 0483            return BadRequest(new { error = "Выберите хотя бы одну дату, которую нужно обработать." });
 484        }
 485
 486        try
 487        {
 0488            if (IsSellerReturns(model))
 489            {
 0490                var shipments = await ResolveShipmentNumbersAsync(model);
 0491                var (returnMatchedOrderIds, matchedShipments) = model.SelectedOzonClientId.HasValue
 0492                    ? await _orderServices.GetOrderIdsAndMatchedShipmentsByShipmentListAsync(
 0493                        shipments,
 0494                        model.SelectedOzonClientId.Value)
 0495                    : await _orderServices.GetOrderIdsAndMatchedShipmentsByShipmentListAsync(shipments);
 496
 497                var returnMatchedRowKeys = matchedShipments.Select(s => $"{s}|||").ToList();
 498
 0499                return Json(new
 0500                {
 0501                    processedCount = shipments.Count,
 0502                    matchedCount = returnMatchedOrderIds.Count,
 0503                    hasMatches = returnMatchedOrderIds.Count > 0,
 0504                    matchedRowKeys = returnMatchedRowKeys,
 0505                    message = returnMatchedOrderIds.Count > 0
 0506                        ? $"Найдено заказов: {returnMatchedOrderIds.Count} из {shipments.Count} строк. Можно сразу запус
 0507                        : $"Обработано {shipments.Count} строк, заказов не найдено."
 0508                });
 509            }
 510
 0511            var processedOrders = await ResolveProcessedOrdersAsync(model);
 0512            var ids = await _orderServices.GetOrderIdsByNumbersAndArticles(processedOrders);
 0513            var matchedPairs = await _orderServices.GetMatchedNumbersAndArticles(processedOrders);
 514
 0515            var matchedRowKeys = matchedPairs
 516                .Select(pair => $"{pair.OrderNumber}|||{pair.Article}")
 0517                .ToList();
 518
 0519            return Json(new
 0520            {
 0521                processedCount = processedOrders.Count,
 0522                matchedCount = ids.Count,
 0523                hasMatches = ids.Count > 0,
 0524                matchedRowKeys,
 0525                message = ids.Count > 0
 0526                    ? $"Найдено заказов: {ids.Count} из {processedOrders.Count} строк. Можно сразу запускать обработку."
 0527                    : $"Обработано {processedOrders.Count} строк, заказов не найдено."
 0528            });
 529        }
 0530        catch (Exception ex)
 531        {
 0532            return BadRequest(new { error = BuildUserFriendlyUploadError(ex, "Не удалось выполнить предварительный анали
 533        }
 0534    }
 535
 536    [HttpGet]
 537    public IActionResult SelectTransactionType(string ids, int processedCount, int matchedCount, PercentageUploadDataKin
 538    {
 0539        if (string.IsNullOrWhiteSpace(ids))
 540        {
 0541            TempData["Error"] = "После загрузки не удалось определить заказы для обработки.";
 0542            return RedirectToAction("UploadFile");
 543        }
 544
 0545        return View(new SelectTransactionTypeViewModel
 0546        {
 0547            Ids = ids,
 0548            ProcessedCount = processedCount,
 0549            MatchedCount = matchedCount,
 0550            UploadDataKind = uploadDataKind,
 0551            AvailableTypes =
 0552            [
 0553                TransactionType.Percentage,
 0554                TransactionType.ShippedToClient,
 0555                TransactionType.ShippedToCommissioner,
 0556                TransactionType.CustomerReturn
 0557            ]
 0558        });
 559    }
 560
 561    [HttpPost]
 562    public IActionResult SelectTransactionType(SelectTransactionTypeViewModel model)
 563    {
 0564        if (string.IsNullOrWhiteSpace(model.Ids))
 565        {
 0566            TempData["Error"] = "Не удалось получить список заказов для обработки.";
 0567            return RedirectToAction("UploadFile");
 568        }
 569
 0570        if (!IsTransactionTypeAllowedForUploadKind(model.SelectedTransactionType, model.UploadDataKind))
 571        {
 0572            TempData["Error"] = "Выбранный тип документа недоступен для текущего типа данных.";
 0573            return RedirectToAction("SelectTransactionType", new
 0574            {
 0575                ids = model.Ids,
 0576                processedCount = model.ProcessedCount,
 0577                matchedCount = model.MatchedCount,
 0578                uploadDataKind = model.UploadDataKind
 0579            });
 580        }
 581
 0582        switch (model.SelectedTransactionType)
 583        {
 584            case TransactionType.ShippedToClient:
 0585                return RedirectToAction(
 0586                    "CreateShippedToClientTransactionFromFile",
 0587                    "ShippedToClientTransaction",
 0588                    new { area = "Studio2", ids = model.Ids, page = 1 }
 0589                );
 590
 591            case TransactionType.ShippedToCommissioner:
 0592                return Redirect($"/Studio2/ShippedToCommissionerTransaction/CreateShippedToCommissionerTransactionFromFi
 593
 594            case TransactionType.Percentage:
 0595                return RedirectToAction("Percentage", new { ids = model.Ids });
 596
 597            case TransactionType.CustomerReturn:
 0598                return RedirectToAction(
 0599                    "CreateCustomerReturnTransactionFromFile",
 0600                    "CustomerReturnTransaction",
 0601                    new { area = "Studio2", ids = model.Ids, page = 1 });
 602
 603            default:
 0604                TempData["Error"] = "Неизвестный тип документа";
 0605                return RedirectToAction("SelectTransactionType", new
 0606                {
 0607                    ids = model.Ids,
 0608                    processedCount = model.ProcessedCount,
 0609                    matchedCount = model.MatchedCount
 0610                });
 611        }
 612    }
 613
 614    private async Task<List<(string OrderNumber, string Article)>> ResolveProcessedOrdersAsync(UploadFileViewModel model
 615    {
 0616        if (string.Equals(model.UploadSource, "ozon-api", StringComparison.OrdinalIgnoreCase))
 617        {
 0618            return await GetProcessedOrdersFromOzonReportAsync(model);
 619        }
 620
 0621        if (model.ExcelFile == null || model.ExcelFile.Length == 0)
 0622            throw new Exception("Файл не выбран или пуст");
 623
 0624        return await _fileProcessingService.ProcessFileAsync(model.ExcelFile, model.SelectedDateColumn, model.SelectedDa
 0625    }
 626
 627    private async Task<List<(string OrderNumber, string Article)>> GetProcessedOrdersFromOzonReportAsync(
 628        UploadFileViewModel model)
 629    {
 630        // Если отчёт уже загружен на предыдущем шаге — берём из кэша
 0631        if (!string.IsNullOrWhiteSpace(model.ApiReportKey))
 632        {
 0633            var cacheKey = $"api_report_{model.ApiReportKey}";
 0634            if (!_memoryCache.TryGetValue(cacheKey, out CachedApiReport? cachedReport) || cachedReport == null)
 0635                throw new Exception("Сессия загрузки отчёта истекла. Пожалуйста, загрузите отчёт заново.");
 636
 0637            var cachedFile = CreateFormFileFromCachedReport(cachedReport);
 0638            return await _fileProcessingService.ProcessFileAsync(cachedFile, model.SelectedDateColumn, model.SelectedDat
 639        }
 640
 0641        Console.WriteLine($"[PercentageController/GetProcessedOrdersFromOzonReportAsync] start; clientId={model.Selected
 642
 0643        if (model.SelectedOzonClientId is null)
 0644            throw new Exception("Не выбран Ozon-клиент");
 645
 0646        if (!model.ProcessedAtFrom.HasValue || !model.ProcessedAtTo.HasValue)
 0647            throw new Exception("Укажите период формирования отчета");
 648
 0649        var processedAtFrom = model.ProcessedAtFrom.Value;
 0650        var processedAtTo = model.ProcessedAtTo.Value;
 651
 0652        if (processedAtFrom > processedAtTo)
 0653            throw new Exception("Дата начала периода не может быть больше даты окончания");
 654
 0655        if (processedAtTo - processedAtFrom > TimeSpan.FromDays(92))
 0656            throw new Exception("Период отчета не должен превышать 3 месяца");
 657
 0658        var ozonClient = await _ozonClientServices.GetOzonClientAsync(model.SelectedOzonClientId.Value);
 0659        if (ozonClient == null || string.IsNullOrWhiteSpace(ozonClient.DecryptClientId) ||
 0660            string.IsNullOrWhiteSpace(ozonClient.DecryptApiKey))
 0661            throw new Exception("Не удалось загрузить настройки выбранного Ozon-клиента");
 662
 0663        Console.WriteLine($"[PercentageController/GetProcessedOrdersFromOzonReportAsync] client='{ozonClient.Name}', dec
 664
 0665        var apiManager = new OzonApiDataManager(
 0666            _httpClientFactory,
 0667            _emailService,
 0668            ozonClient.DecryptClientId,
 0669            ozonClient.DecryptApiKey);
 670
 0671        var reportRequest = new PostingsReportRequest
 0672        {
 0673            Filter = new PostingsReportRequestFilter
 0674            {
 0675                ProcessedAtFrom = processedAtFrom,
 0676                ProcessedAtTo = processedAtTo,
 0677                DeliverySchema = ["fbs"],
 0678                IsExpress = model.IsExpress,
 0679                Sku = ParseStringArray(model.SkuList),
 0680                CancelReasonId = [],
 0681                OfferId = model.OfferId?.Trim() ?? string.Empty,
 0682                StatusAlias = ParseStringArray(model.StatusAliasList),
 0683                Statuses = ParseStringArray(model.StatusesList),
 0684                Title = model.Title?.Trim() ?? string.Empty,
 0685                WarehouseId = ParseLongArray(model.WarehouseIds),
 0686                DeliveryMethodId = ParseLongArray(model.DeliveryMethodIds)
 0687            },
 0688            Language = "DEFAULT",
 0689            With = new PostingsReportRequestWith
 0690            {
 0691                AdditionalData = model.IncludeAdditionalData,
 0692                AnalyticsData = model.IncludeAnalyticsData,
 0693                CustomerData = model.IncludeCustomerData,
 0694                JewelryCodes = model.IncludeJewelryCodes
 0695            }
 0696        };
 697
 0698        Console.WriteLine($"[PercentageController/GetProcessedOrdersFromOzonReportAsync] reportRequest={JsonConvert.Seri
 699
 0700        Console.WriteLine("[PercentageController/GetProcessedOrdersFromOzonReportAsync] step=GetReportCode");
 0701        var reportCode = await apiManager.GetReportCode(reportRequest);
 0702        Console.WriteLine($"[PercentageController/GetProcessedOrdersFromOzonReportAsync] reportCode={reportCode}");
 703
 0704        Console.WriteLine("[PercentageController/GetProcessedOrdersFromOzonReportAsync] step=WaitForReportReady");
 0705        var fileUrl = await apiManager.WaitForReportReady(reportCode);
 0706        Console.WriteLine($"[PercentageController/GetProcessedOrdersFromOzonReportAsync] fileUrl={fileUrl}");
 707
 0708        Console.WriteLine("[PercentageController/GetProcessedOrdersFromOzonReportAsync] step=DownloadReportBytesAsync");
 0709        var fileBytes = await DownloadReportBytesAsync(fileUrl);
 0710        Console.WriteLine($"[PercentageController/GetProcessedOrdersFromOzonReportAsync] downloadedBytes={fileBytes.Leng
 711
 0712        await using var stream = new MemoryStream(fileBytes);
 0713        var reportFileName = BuildReportFileName(reportCode);
 0714        var reportFile = new FormFile(stream, 0, fileBytes.Length, "excelFile", reportFileName)
 0715        {
 0716            Headers = new HeaderDictionary(),
 0717            ContentType = GuessContentTypeForReportFile(reportFileName)
 0718        };
 719
 0720        Console.WriteLine("[PercentageController/GetProcessedOrdersFromOzonReportAsync] step=ProcessFileAsync");
 0721        var processedOrders = await _fileProcessingService.ProcessFileAsync(
 0722            reportFile,
 0723            model.SelectedDateColumn,
 0724            model.SelectedDateValues);
 0725        Console.WriteLine($"[PercentageController/GetProcessedOrdersFromOzonReportAsync] processedOrdersCount={processed
 0726        return processedOrders;
 0727    }
 728
 729    private async Task<List<string>> ResolveShipmentNumbersAsync(UploadFileViewModel model)
 730    {
 0731        if (string.Equals(model.UploadSource, "ozon-api", StringComparison.OrdinalIgnoreCase))
 0732            return await GetShipmentsFromOzonReportAsync(model);
 733
 0734        if (model.ExcelFile == null || model.ExcelFile.Length == 0)
 0735            throw new Exception("Файл не выбран или пуст");
 736
 0737        return await _fileProcessingService.ProcessShipmentOnlyAsync(
 0738            model.ExcelFile,
 0739            model.SelectedDateColumn,
 0740            model.SelectedDateValues);
 0741    }
 742
 743    private async Task<List<string>> GetShipmentsFromOzonReportAsync(UploadFileViewModel model)
 744    {
 0745        if (!string.IsNullOrWhiteSpace(model.ApiReportKey))
 746        {
 0747            var cacheKey = $"api_report_{model.ApiReportKey}";
 0748            if (!_memoryCache.TryGetValue(cacheKey, out CachedApiReport? cachedReport) || cachedReport == null)
 0749                throw new Exception("Сессия загрузки отчёта истекла. Пожалуйста, загрузите отчёт заново.");
 750
 0751            var cachedFile = CreateFormFileFromCachedReport(cachedReport);
 0752            return await _fileProcessingService.ProcessShipmentOnlyAsync(
 0753                cachedFile,
 0754                model.SelectedDateColumn,
 0755                model.SelectedDateValues);
 756        }
 757
 0758        if (model.SelectedOzonClientId is null)
 0759            throw new Exception("Не выбран Ozon-клиент");
 760
 0761        if (!model.ProcessedAtFrom.HasValue || !model.ProcessedAtTo.HasValue)
 0762            throw new Exception("Укажите период формирования отчета");
 763
 0764        var processedAtFrom = model.ProcessedAtFrom.Value;
 0765        var processedAtTo = model.ProcessedAtTo.Value;
 766
 0767        if (processedAtFrom > processedAtTo)
 0768            throw new Exception("Дата начала периода не может быть больше даты окончания");
 769
 0770        if (processedAtTo - processedAtFrom > TimeSpan.FromDays(92))
 0771            throw new Exception("Период отчета не должен превышать 3 месяца");
 772
 0773        var ozonClient = await _ozonClientServices.GetOzonClientAsync(model.SelectedOzonClientId.Value);
 0774        if (ozonClient == null || string.IsNullOrWhiteSpace(ozonClient.DecryptClientId) ||
 0775            string.IsNullOrWhiteSpace(ozonClient.DecryptApiKey))
 0776            throw new Exception("Не удалось загрузить настройки выбранного Ozon-клиента");
 777
 0778        var apiManager = new OzonApiDataManager(
 0779            _httpClientFactory,
 0780            _emailService,
 0781            ozonClient.DecryptClientId,
 0782            ozonClient.DecryptApiKey);
 783
 0784        var reportCode = await apiManager.CreateSellerReturnsReportAsync(
 0785            DateTime.SpecifyKind(processedAtFrom, DateTimeKind.Utc),
 0786            DateTime.SpecifyKind(processedAtTo, DateTimeKind.Utc));
 0787        var fileUrl = await apiManager.WaitForReportReady(reportCode);
 0788        var fileBytes = await DownloadReportBytesAsync(fileUrl);
 789
 0790        var reportFileName = BuildReportFileName(reportCode);
 0791        await using var stream = new MemoryStream(fileBytes);
 0792        var reportFile = new FormFile(stream, 0, fileBytes.Length, "excelFile", reportFileName)
 0793        {
 0794            Headers = new HeaderDictionary(),
 0795            ContentType = GuessContentTypeForReportFile(reportFileName)
 0796        };
 797
 0798        return await _fileProcessingService.ProcessShipmentOnlyAsync(
 0799            reportFile,
 0800            model.SelectedDateColumn,
 0801            model.SelectedDateValues);
 0802    }
 803
 804    private async Task<byte[]> DownloadReportBytesAsync(string fileUrl, CancellationToken cancellationToken = default)
 805    {
 0806        using var client = _httpClientFactory.CreateClient();
 0807        return await OzonReportDownloadHelper.DownloadBytesWithRetryAsync(client, fileUrl, cancellationToken: cancellati
 0808    }
 809
 810    private static string BuildUserFriendlyUploadError(Exception ex, string fallbackMessage)
 811    {
 0812        var message = ex.Message?.Trim();
 0813        if (string.IsNullOrWhiteSpace(message))
 814        {
 0815            return fallbackMessage;
 816        }
 817
 0818        if (ex is ArgumentNullException)
 819        {
 0820            return "Файл не был передан. Выберите файл и повторите попытку.";
 821        }
 822
 0823        if (ex is InvalidDataException)
 824        {
 0825            return "Файл повреждён или имеет неверную структуру. Проверьте содержимое и попробуйте снова.";
 826        }
 827
 0828        if (ex is FormatException)
 829        {
 0830            return "Не удалось распознать формат файла или значения в нём. Проверьте файл и повторите попытку.";
 831        }
 832
 0833        if (ex is HeaderValidationException || ex is ReaderException)
 834        {
 0835            return "CSV-файл содержит ошибки структуры или некорректные данные. Проверьте разделитель, кодировку и загол
 836        }
 837
 0838        if (ex is LicenseException)
 839        {
 0840            return "Не удалось открыть Excel-файл. Попробуйте сохранить его заново или экспортировать в CSV.";
 841        }
 842
 0843        if (message.Contains("central directory", StringComparison.OrdinalIgnoreCase)
 0844            || message.Contains("end of central directory", StringComparison.OrdinalIgnoreCase)
 0845            || message.Contains("invalid data", StringComparison.OrdinalIgnoreCase)
 0846            || message.Contains("corrupt", StringComparison.OrdinalIgnoreCase))
 847        {
 0848            return "Файл повреждён или загружен не полностью. Загрузите корректный файл и повторите попытку.";
 849        }
 850
 0851        if (message.Contains("timeout", StringComparison.OrdinalIgnoreCase)
 0852            || message.Contains("timed out", StringComparison.OrdinalIgnoreCase))
 853        {
 0854            return "Операция заняла слишком много времени. Попробуйте повторить позже или уменьшить объём данных.";
 855        }
 856
 0857        if (message.Contains("network", StringComparison.OrdinalIgnoreCase)
 0858            || message.Contains("connection", StringComparison.OrdinalIgnoreCase)
 0859            || ex is HttpRequestException)
 860        {
 0861            return "Не удалось связаться с внешним сервисом. Проверьте соединение и повторите попытку.";
 862        }
 863
 0864        return message;
 865    }
 866
 867    private async Task<UploadFileViewModel> BuildUploadFileViewModelAsync()
 868    {
 0869        var clients = (await _ozonClientServices.GetOzonClients())
 870            .Where(x => x.ClientType == ClientType.OZON)
 871            .OrderBy(x => x.Name)
 872            .Select(x => new SelectListItem(x.Name, x.Id.ToString()))
 0873            .ToList();
 874
 0875        return new UploadFileViewModel
 0876        {
 0877            ProcessedAtFrom = DateTime.Today.AddDays(-7),
 0878            ProcessedAtTo = DateTime.Today,
 0879            OzonClients = clients
 0880        };
 0881    }
 882
 883    private async Task<UploadFileViewModel> PopulateUploadFileViewModelAsync(UploadFileViewModel model)
 884    {
 0885        model.OzonClients = (await _ozonClientServices.GetOzonClients())
 886            .Where(x => x.ClientType == ClientType.OZON)
 887            .OrderBy(x => x.Name)
 888            .Select(x => new SelectListItem(x.Name, x.Id.ToString()))
 0889            .ToList();
 890
 0891        return model;
 0892    }
 893
 894    private static string[] ParseStringArray(string? raw)
 895    {
 0896        return string.IsNullOrWhiteSpace(raw)
 0897            ? []
 0898            : raw.Split(new[] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
 0899                .Select(x => x.Trim())
 0900                .Where(x => !string.IsNullOrWhiteSpace(x))
 0901                .ToArray();
 902    }
 903
 904    private static long[] ParseLongArray(string? raw)
 905    {
 0906        return ParseStringArray(raw)
 0907            .Select(x => long.TryParse(x, out var value) ? value : (long?)null)
 0908            .Where(x => x.HasValue)
 0909            .Select(x => x!.Value)
 0910            .ToArray();
 911    }
 912
 913    private static string GuessContentTypeForReportFile(string fileName)
 914    {
 0915        if (fileName.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase))
 0916            return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
 0917        if (fileName.EndsWith(".xls", StringComparison.OrdinalIgnoreCase))
 0918            return "application/vnd.ms-excel";
 0919        return "text/csv";
 920    }
 921
 922    private static string BuildReportFileName(string reportCode)
 923    {
 0924        var safeCode = string.Concat(reportCode.Select(ch => char.IsLetterOrDigit(ch) || ch is '-' or '_' ? ch : '_'));
 0925        var ext = reportCode.Contains("returns", StringComparison.OrdinalIgnoreCase) ? ".xlsx" : ".csv";
 0926        return $"{safeCode}{ext}";
 927    }
 928    // GET
 929    public async Task<IActionResult> Percentage(string ids, int page)
 930    {
 0931        var filteredStatuses = _context.AppStatuses.Where(status => status.Name == "Отменен").ToList();
 0932        ViewBag.AppStatuses = new SelectList(filteredStatuses, "Id", "Name");
 933        ViewBag.StatusColors = filteredStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor());
 934
 935        // Проверка на пустой или null ids
 0936        if (string.IsNullOrWhiteSpace(ids))
 937        {
 0938            ViewData["ErrorReason"] = "Ни один заказ не был выбран. Выберите заказы с помощью галочек.";
 939
 0940            var emptyViewModel = new MultiplayEditOrderViewModel()
 0941            {
 0942                RedirectPage = page,
 0943                Orders = new List<Order>(),
 0944                User = (await _userCacheService.GetCachedUserAsync(User))!,
 945                Suppliers = (await _supplierDataServices.GetSuppliers()).OrderBy(s => s.Name).ToList(),
 0946                RateEUR = await _currencyRateFetcher.GetEURRateAsync(),
 0947                RateUSD = await _currencyRateFetcher.GetUSDRateAsync(),
 0948                RateBYN = await _currencyRateFetcher.GetBYNRateAsync(),
 0949                UniqueArticles = new Dictionary<string, int>(),
 0950                UniqueDeliveryCitys = new Dictionary<string, int>(),
 0951                UniqueNumbers = new Dictionary<string, int>(),
 0952                AppStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Заказан поставщику" })
 0953                    ?? new AppStatus { Name = "Заказан поставщику" }
 0954            };
 955
 0956            if (emptyViewModel.User?.AccessProfileId != null)
 957            {
 0958                emptyViewModel.User.AccessProfile = await _context.AccessProfiles.FindAsync(emptyViewModel.User.AccessPr
 959            }
 960
 0961            return View(emptyViewModel);
 962        }
 963
 0964        int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 0965        List<Order> ordersToTransaction = new List<Order>();
 966
 0967        foreach (var orderId in idArray)
 968        {
 0969            var concretOrder = await _orderServices.GetOrder(orderId);
 0970            if (concretOrder == null)
 971                continue;
 972
 0973            concretOrder.UpdatedBy = User.Identity?.Name;
 0974            ordersToTransaction.Add(concretOrder);
 975        }
 976
 0977        var pageViewModel = new MultiplayEditOrderViewModel()
 0978        {
 0979            RedirectPage = page,
 980            Orders = ordersToTransaction.OrderBy(ot => ot.ShipmentNumber).ToList(),
 0981            User = (await _userCacheService.GetCachedUserAsync(User))!,
 982            Suppliers = (await _supplierDataServices.GetSuppliers()).OrderBy(s => s.Name).ToList(),
 0983            RateEUR = await _currencyRateFetcher.GetEURRateAsync(),
 0984            RateUSD = await _currencyRateFetcher.GetUSDRateAsync(),
 0985            RateBYN = await _currencyRateFetcher.GetBYNRateAsync(),
 0986            UniqueArticles = await _orderServices.GetArticleCountsForOrdersAsync(ordersToTransaction),
 0987            UniqueDeliveryCitys = await _orderServices.GetDeliveryCityCountsForOrdersAsync(ordersToTransaction),
 0988            UniqueNumbers = await _orderServices.GetShipmentBaseCountsForOrdersAsync(ordersToTransaction),
 0989            AppStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Заказан поставщику" })
 0990                ?? new AppStatus { Name = "Заказан поставщику" }
 0991        };
 992
 0993        if (pageViewModel.User?.AccessProfileId != null)
 994        {
 0995            pageViewModel.User.AccessProfile = await _context.AccessProfiles.FindAsync(pageViewModel.User.AccessProfileI
 996        }
 997
 0998        RunBackgroundCacheTask(pageViewModel.Orders);
 0999        return View(pageViewModel);
 01000    }
 1001
 1002    // фоновой прогрев кэша
 1003    private void RunBackgroundCacheTask(List<Order> orders)
 1004    {
 01005        _ = Task.Run(async () =>
 01006        {
 01007            foreach (var order in orders)
 01008            {
 01009                try
 01010                {
 01011                    if (string.IsNullOrEmpty(order.EtProducer?.Name) || string.IsNullOrEmpty(order.Article))
 01012                        continue;
 01013
 01014                    _ = await _proxyHttpClientService.GetJsonAsync(
 01015                        $"https://api.interparts.ru/detail-full-info/{order.EtProducer.Name}/{order.Article}");
 01016
 01017                    _ = await _proxyHttpClientService.GetJsonAsync(
 01018                        $"https://api.interparts.ru/product-information/product/?article_number={order.Article}&manufact
 01019
 01020                    Console.WriteLine($"Кэширован для проведения: {order.EtProducer.Name}/{order.Article}");
 01021                }
 01022                catch (Exception ex)
 01023                {
 01024                    Console.WriteLine($"Ошибка в фоне: {ex.Message}");
 01025                }
 01026            }
 01027        });
 01028    }
 1029
 1030    // POST
 1031    [HttpPost]
 1032    [ValidateAntiForgeryToken]
 1033    public async Task<IActionResult> Percentage(
 1034        List<Order> orders,
 1035        string userName,
 1036        string comment,
 1037        int page,
 1038        string deletedOrders)
 1039    {
 01040        var aiFlow = TransactionCommentAiSchedulingHelper.BeginFlow(
 01041            comment,
 01042            TransactionType.Percentage,
 01043            userName,
 01044            processIn1C: false);
 01045        comment = aiFlow.UserCommentOnly;
 1046
 01047        if (orders == null)
 1048        {
 01049            TempData["ErorrResult"] = $"Не удалось провести выбранные заказы.<br>Было передано слишком большое количеств
 01050            return RedirectToAction("Index");
 1051        }
 1052        else
 1053        {
 01054            if (deletedOrders != null)
 1055            {
 01056                var deletedOrderIds = DeletedOrderIds.Parse(deletedOrders);
 01057                orders = orders.Where(o => !deletedOrderIds.Contains(o.Id)).ToList();
 1058            }
 1059        }
 1060
 1061        try
 1062        {
 01063            if (orders.Count != 0)
 1064            {
 01065                DateTime createAt = DateTime.Now;
 01066                var changeCount = 0;
 01067                var dateTime = "";
 01068                var transactionId = 0;
 01069                List<Order> ordersToUpdate = new List<Order>();
 01070                List<string> ordersNotFoundInOzone = [];
 1071
 1072                var orderIds = orders.Select(o => o.Id).Distinct().ToList();
 01073                var fromStatusByOrderId = await _context.Orders.AsNoTracking()
 01074                    .Where(o => orderIds.Contains(o.Id))
 01075                    .Select(o => new { o.Id, FromStatusId = o.AppStatusId ?? 0 })
 1076                    .ToDictionaryAsync(x => x.Id, x => x.FromStatusId);
 1077
 01078                var submittedFields = SubmittedOrderFields.FromForm(Request.HasFormContentType ? Request.Form : null);
 01079                foreach (var order in orders)
 1080                {
 01081                    ordersToUpdate.Add(await _orderServices.TransactOrder(order, submittedFields.For(order.Id)));
 1082                }
 1083
 01084                var commentForDb = TransactionCommentAiSchedulingHelper.ResolveDatabaseComment(aiFlow, comment);
 01085                (changeCount, dateTime, transactionId) = await _transaction.CreatePercentageTransaction(ordersToUpdate,
 01086                                                                                              userName,
 01087                                                                                              createAt,
 01088                                                                                              commentForDb);
 01089                await _transactionCache.Update();
 1090
 01091                if (aiFlow.ScheduleAiComment && aiFlow.Builder != null && transactionId > 0)
 1092                {
 01093                    await TransactionCommentAiSchedulingHelper.TryScheduleAsync(
 01094                        HttpContext.RequestServices,
 01095                        aiFlow.Builder,
 01096                        true,
 01097                        transactionId,
 01098                        ordersToUpdate,
 01099                        fromStatusByOrderId,
 01100                        _context,
 01101                        ct: HttpContext.RequestAborted);
 1102                }
 01103                if (ordersNotFoundInOzone.Count > 0)
 1104                {
 1105                    TempData["OrdersNotFoundInOzone"] = $"Заказы не найдены в системе Ozon - {ordersNotFoundInOzone.Aggr
 1106                }
 01107            }
 1108
 01109            ClearSelectedIdsSession();
 1110
 01111            foreach (var order in orders)
 1112            {
 01113                var cookieKey = $"PurchasePrice_{order.Id}";
 01114                if (Request.Cookies.ContainsKey(cookieKey))
 1115                {
 01116                    Response.Cookies.Delete(cookieKey);
 1117                }
 1118            }
 1119
 01120            return Json(new { redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page = 
 1121        }
 01122        catch (Exception ex)
 1123        {
 01124            TempData["ErorrResult"] = $"Не удалось провести выбранные заказы ({ex.Message})";
 01125            return Json(new { redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page = 
 1126        }
 01127    }
 1128
 1129    [HttpPost]
 1130    public IActionResult ClearSelectedIdsSession()
 1131    {
 01132        HttpContext.Session.SetString("selectedIds", JsonConvert.SerializeObject(new List<int>()));
 01133        return Ok();
 1134    }
 1135
 1136    private OrderSortState GetSortStateCookie()
 1137    {
 01138        var sortStateCookie = Request.Cookies["SortState"];
 01139        if (!string.IsNullOrEmpty(sortStateCookie) && Enum.TryParse<OrderSortState>(sortStateCookie, out var savedSortSt
 1140        {
 01141            return savedSortState;
 1142        }
 01143        return OrderSortState.StandardState;
 1144    }
 1145
 1146
 1147}

Methods/Properties

get_Bytes()
get_FileName()
get_ContentType()
IsSellerReturns(OzonOrdersWeb.Areas.Studio2.ViewModels.Percentage.UploadFileViewModel)
GetPreferredDateColumnName(OzonOrdersWeb.Areas.Studio2.ViewModels.Percentage.UploadFileViewModel)
IsTransactionTypeAllowedForUploadKind(OzonDomains.TransactionType,OzonOrdersWeb.Areas.Studio2.ViewModels.Percentage.PercentageUploadDataKind)
CreateFormFileFromCachedReport(OzonOrdersWeb.Areas.Studio2.Controllers.PercentageController/CachedApiReport)
.ctor(OzonRepositories.Context.ApplicationDbContext,Services.DataServices.AppStatusDataServices,Services.DataServices.OrdersDataServices,Services.TransactionUtilsServices.TransactionManager,Services.CacheServices.Cache.OzonOrdersCache.TransactionCache,Services.DataServices.SupplierDataServices,Services.CacheServices.Cache.UserCacheService.IUserCacheService,Services.PriceСlculationServices.CurrencyRateFetcher,Services.CacheServices.Cache.CacheUpdater`1<OzonDomains.Models.Order>,Services.CacheServices.Cache.OzonOrdersCache.OrderCache,PartsInfo.HttpUtils.ProxyHttpClientService,Services.ParserServices.FileProcessing.IFileProcessingService,Services.DataServices.OzonClientServices,System.Net.Http.IHttpClientFactory,SlqStudio.Application.Services.EmailService.IEmailService,Microsoft.Extensions.Caching.Memory.IMemoryCache)
UploadFile()
UploadFile()
PreviewUploadFile()
FetchApiReportPreview()
DownloadApiReport(System.String)
AnalyzeUploadFile()
SelectTransactionType(System.String,System.Int32,System.Int32,OzonOrdersWeb.Areas.Studio2.ViewModels.Percentage.PercentageUploadDataKind)
SelectTransactionType(OzonOrdersWeb.Areas.Studio2.ViewModels.Percentage.SelectTransactionTypeViewModel)
ResolveProcessedOrdersAsync()
GetProcessedOrdersFromOzonReportAsync()
ResolveShipmentNumbersAsync()
GetShipmentsFromOzonReportAsync()
DownloadReportBytesAsync()
BuildUserFriendlyUploadError(System.Exception,System.String)
BuildUploadFileViewModelAsync()
PopulateUploadFileViewModelAsync()
ParseStringArray(System.String)
ParseLongArray(System.String)
GuessContentTypeForReportFile(System.String)
BuildReportFileName(System.String)
Percentage()
RunBackgroundCacheTask(System.Collections.Generic.List`1<OzonDomains.Models.Order>)
Percentage()
ClearSelectedIdsSession()
GetSortStateCookie()