< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.OrdersGridController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OrdersGridController.cs
Line coverage
16%
Covered lines: 31
Uncovered lines: 156
Coverable lines: 187
Total lines: 487
Line coverage: 16.5%
Branch coverage
4%
Covered branches: 4
Total branches: 86
Branch coverage: 4.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Index()0%2040%
GetData()0%110100%
GetChanges()100%6450%
RefreshStatuses()0%4260%
GetFacets()0%4260%
GetOrderCard()0%2040%
GetStatusHistory()0%2040%
GetOrderTransactions()0%2040%
GetFeedPriceStock()0%2040%
GetFeedPriceBatch()0%7280%
GetFeedPriceExplanation()0%7280%
GetProductCard()0%2040%
GetProductAnalogs()0%2040%
GetSidebarPresets()100%210%
CheckFormula()0%4260%
SaveSetting()0%156120%
GetFilterOptions()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OrdersGridController.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.AspNetCore.Authorization;
 3using Microsoft.AspNetCore.Mvc;
 4using OzonDomains.Models.Authorization.Enums;
 5using OzonOrdersWeb.Areas.Studio2.ViewModels.OrdersGrid;
 6using OzonOrdersWeb.Authorization;
 7using OzonOrdersWeb.WebServices.Orders;
 8using OzonOrdersWeb.WebServices.Orders.Grid;
 9
 10namespace OzonOrdersWeb.Areas.Studio2.Controllers;
 11
 12/// <summary>
 13/// Список заказов на AG Grid (Infinite Row Model). Полностью изолирован от <c>OrdersController</c>:
 14/// свой каркас страницы, свой JSON-эндпоинт блоков, cookies FilterData/SortState не трогает.
 15/// Права — те же, что у страницы заказов (<c>Studio2.Orders.Index</c>): AJAX-действия помечены
 16/// <see cref="PageAuthorizeAttribute"/> с View, иначе auto-авторизация потребовала бы CanEdit для POST.
 17/// </summary>
 18[Area("Studio2")]
 19[Authorize(Roles = "User,Admin")]
 20[Route("Studio2/OrdersGrid")]
 21[Route("OrdersGrid")]
 22public sealed class OrdersGridController : Controller
 23{
 24    private const string OrdersPageCode = "Studio2.Orders.Index";
 25
 26    private readonly IOrdersGridQueryService _gridQueryService;
 27    private readonly IOrdersGridFilterOptionsService _filterOptionsService;
 28    private readonly IOrdersIndexPageViewDataService _pageViewDataService;
 29    private readonly IOrdersGridUserSettingsService _userSettings;
 30    private readonly IOrdersGridChangesService _changesService;
 31    private readonly IOrdersGridStatusRefreshService _statusRefreshService;
 32    private readonly IOrdersGridFacetsService _facetsService;
 33    private readonly IOrdersGridOrderCardService _orderCardService;
 34    private readonly IOrdersGridFeedPriceService _feedPriceService;
 35    private readonly IOrdersGridProductCardService _productCardService;
 36    private readonly IOrdersGridSidebarPresetsService _sidebarPresets;
 37    private readonly ILogger<OrdersGridController> _logger;
 38
 339    public OrdersGridController(
 340        IOrdersGridQueryService gridQueryService,
 341        IOrdersGridFilterOptionsService filterOptionsService,
 342        IOrdersIndexPageViewDataService pageViewDataService,
 343        IOrdersGridUserSettingsService userSettings,
 344        IOrdersGridChangesService changesService,
 345        IOrdersGridStatusRefreshService statusRefreshService,
 346        IOrdersGridFacetsService facetsService,
 347        IOrdersGridOrderCardService orderCardService,
 348        IOrdersGridFeedPriceService feedPriceService,
 349        IOrdersGridProductCardService productCardService,
 350        IOrdersGridSidebarPresetsService sidebarPresets,
 351        ILogger<OrdersGridController> logger)
 52    {
 353        _sidebarPresets = sidebarPresets;
 354        _facetsService = facetsService;
 355        _orderCardService = orderCardService;
 356        _feedPriceService = feedPriceService;
 357        _productCardService = productCardService;
 358        _changesService = changesService;
 359        _statusRefreshService = statusRefreshService;
 360        _gridQueryService = gridQueryService;
 361        _filterOptionsService = filterOptionsService;
 362        _pageViewDataService = pageViewDataService;
 363        _userSettings = userSettings;
 364        _logger = logger;
 365    }
 66
 67    [HttpGet("")]
 68    [HttpGet("Index")]
 69    [PageAuthorize(OrdersPageCode, AccessType.View)]
 70    public async Task<IActionResult> Index(CancellationToken cancellationToken)
 71    {
 072        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
 073        if (string.IsNullOrEmpty(userId))
 74        {
 075            return Challenge();
 76        }
 77
 078        var isAdmin = User.IsInRole("Admin");
 079        var allowedColumns = await _gridQueryService.GetAllowedColumnsAsync(User, cancellationToken);
 080        var options = await _filterOptionsService.BuildAsync(User, cancellationToken);
 81
 082        var pageViewData = await _pageViewDataService.BuildAsync(new OrdersIndexPageViewDataRequest
 083        {
 084            UserId = userId,
 085            IsWebAdmin = isAdmin,
 086            CancellationToken = cancellationToken
 087        });
 088        OrdersIndexPageViewDataApplicator.Apply(pageViewData, ViewBag);
 89
 090        var userSettings = await _userSettings.GetAllAsync(userId, cancellationToken);
 91
 092        var model = new OrdersGridIndexViewModel
 093        {
 094            AllowedColumns = allowedColumns,
 095            FormulaFields = OrdersGridFormulaFields.ForAllowed(allowedColumns),
 096            UserSettings = userSettings,
 097            StatusColors = options.StatusColors,
 098            ClientStatusColors = options.ClientStatusColors,
 099            IsAdmin = isAdmin,
 0100            IsAppSuperAdmin = pageViewData.IsAppSuperAdmin,
 0101            BlockSize = _gridQueryService.BlockSize
 0102        };
 103
 0104        return View(model);
 0105    }
 106
 107    /// <summary>Блок строк для Infinite Row Model.</summary>
 108    [HttpPost("GetData")]
 109    [ValidateAntiForgeryToken]
 110    [PageAuthorize(OrdersPageCode, AccessType.View)]
 111    public async Task<IActionResult> GetData([FromBody] AgGridDataRequest? request, CancellationToken cancellationToken)
 112    {
 0113        if (request is null)
 114        {
 0115            return BadRequest(new { message = "Пустой запрос." });
 116        }
 117
 0118        var blockSize = _gridQueryService.BlockSize;
 0119        if (request.StartRow < 0 || request.EndRow - request.StartRow != blockSize || request.StartRow % blockSize != 0)
 120        {
 0121            return BadRequest(new { message = $"Ожидается блок по {blockSize} строк, выровненный по началу блока." });
 122        }
 123
 0124        if (AgGridRequestLimits.Validate(request.FilterModel, request.SortModel) is { } limitError)
 125        {
 0126            return BadRequest(new { message = limitError });
 127        }
 128
 129        try
 130        {
 0131            var result = await _gridQueryService.LoadBlockAsync(request, User, cancellationToken);
 0132            return Ok(result);
 133        }
 0134        catch (UnauthorizedAccessException)
 135        {
 0136            return Unauthorized();
 137        }
 0138        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 139        {
 0140            return StatusCode(StatusCodes.Status499ClientClosedRequest);
 141        }
 0142        catch (Exception ex)
 143        {
 0144            _logger.LogError(ex, "OrdersGrid: ошибка загрузки блока {StartRow}-{EndRow}", request.StartRow, request.EndR
 0145            return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Не удалось загрузить заказы." }
 146        }
 0147    }
 148
 149    /// <summary>Дельта для автообновления: изменённые/новые заказы после Since (см. OrdersGridChangesService).</summary
 150    [HttpPost("GetChanges")]
 151    [ValidateAntiForgeryToken]
 152    [PageAuthorize(OrdersPageCode, AccessType.View)]
 153    public async Task<IActionResult> GetChanges([FromBody] OrdersGridChangesRequest? request, CancellationToken cancella
 154    {
 3155        if (request is null || !ModelState.IsValid)
 156        {
 2157            ModelState.Clear();
 2158            request = new OrdersGridChangesRequest();
 159        }
 160
 161        try
 162        {
 3163            return Ok(await _changesService.GetChangesAsync(request, User, cancellationToken));
 164        }
 0165        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 166        {
 0167            return StatusCode(StatusCodes.Status499ClientClosedRequest);
 168        }
 0169        catch (Exception ex)
 170        {
 0171            _logger.LogError(ex, "OrdersGrid: ошибка дельты изменений");
 0172            return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Не удалось получить изменения."
 173        }
 3174    }
 175
 176    /// <summary>Ручное «Обновить» статусов Ozon: синхронизация и свежие строки для точечного обновления грида.</summary
 177    [HttpPost("RefreshStatuses")]
 178    [ValidateAntiForgeryToken]
 179    [PageAuthorize("Studio2.Orders.MultiplayUpdateOrdersStatusV2", AccessType.View)]
 180    public async Task<IActionResult> RefreshStatuses([FromBody] OrdersGridRefreshStatusesRequest? request, CancellationT
 181    {
 0182        if (request?.Ids is not { Count: > 0 })
 183        {
 0184            return BadRequest(new { message = "Не переданы заказы для обновления." });
 185        }
 186
 187        try
 188        {
 0189            return Ok(await _statusRefreshService.RefreshAsync(request.Ids, User, cancellationToken));
 190        }
 0191        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 192        {
 0193            return StatusCode(StatusCodes.Status499ClientClosedRequest);
 194        }
 0195        catch (Exception ex)
 196        {
 0197            _logger.LogError(ex, "OrdersGrid: ошибка обновления статусов");
 0198            return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Не удалось обновить статусы." }
 199        }
 0200    }
 201
 202    /// <summary>Счётчики значений справочной колонки при текущих остальных фильтрах (для списка в фильтре).</summary>
 203    [HttpPost("GetFacets")]
 204    [ValidateAntiForgeryToken]
 205    [PageAuthorize(OrdersPageCode, AccessType.View)]
 206    public async Task<IActionResult> GetFacets([FromBody] OrdersGridFacetsRequest? request, CancellationToken cancellati
 207    {
 0208        if (request is null || string.IsNullOrWhiteSpace(request.Column))
 209        {
 0210            return BadRequest(new { message = "Не указана колонка." });
 211        }
 212
 0213        if (AgGridRequestLimits.Validate(request.FilterModel, null) is { } limitError)
 214        {
 0215            return BadRequest(new { message = limitError });
 216        }
 217
 218        try
 219        {
 0220            return Ok(await _facetsService.GetFacetsAsync(request, User, cancellationToken));
 221        }
 0222        catch (UnauthorizedAccessException)
 223        {
 0224            return Unauthorized();
 225        }
 0226        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 227        {
 0228            return StatusCode(StatusCodes.Status499ClientClosedRequest);
 229        }
 0230        catch (Exception ex)
 231        {
 0232            _logger.LogError(ex, "OrdersGrid: ошибка счётчиков фильтра {Column}", request.Column);
 0233            return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Не удалось посчитать значения."
 234        }
 0235    }
 236
 237    /// <summary>Карточка заказа для боковой панели: строка со всеми колонками и последние изменения.</summary>
 238    [HttpGet("GetOrderCard")]
 239    [PageAuthorize(OrdersPageCode, AccessType.View)]
 240    public async Task<IActionResult> GetOrderCard(int id, CancellationToken cancellationToken)
 241    {
 0242        if (id <= 0)
 243        {
 0244            return BadRequest(new { message = "Не указан заказ." });
 245        }
 246
 0247        var card = await _orderCardService.GetCardAsync(id, User, cancellationToken);
 0248        return card is null ? NotFound(new { message = "Заказ не найден." }) : Ok(card);
 0249    }
 250
 251    /// <summary>История переходов статусов заказа (попап по клику на статус).</summary>
 252    [HttpGet("GetStatusHistory")]
 253    [PageAuthorize(OrdersPageCode, AccessType.View)]
 254    public async Task<IActionResult> GetStatusHistory(int id, CancellationToken cancellationToken)
 255    {
 0256        if (id <= 0)
 257        {
 0258            return BadRequest(new { message = "Не указан заказ." });
 259        }
 260
 0261        var history = await _orderCardService.GetStatusHistoryAsync(id, User, cancellationToken);
 0262        return history is null ? NotFound(new { message = "Заказ не найден." }) : Ok(history);
 0263    }
 264
 265    /// <summary>Транзакции, в которых участвовал заказ: тип, кто провёл, когда, комментарий (попап истории статусов).</
 266    [HttpGet("GetOrderTransactions")]
 267    [PageAuthorize(OrdersPageCode, AccessType.View)]
 268    public async Task<IActionResult> GetOrderTransactions(int id, CancellationToken cancellationToken)
 269    {
 0270        if (id <= 0)
 271        {
 0272            return BadRequest(new { message = "Не указан заказ." });
 273        }
 274
 0275        var transactions = await _orderCardService.GetOrderTransactionsAsync(id, User, cancellationToken);
 0276        return transactions is null ? NotFound(new { message = "Заказ не найден." }) : Ok(transactions);
 0277    }
 278
 279    /// <summary>
 280    /// Цена и остатки артикула заказа в фиде (модалка «Фид: цена и остатки» в OrdersGrid и TransactionsGrid).
 281    /// <paramref name="lite"/> — без истории: колонки Feed в TransactionsGrid.
 282    /// </summary>
 283    [HttpGet("GetFeedPriceStock")]
 284    [PageAuthorize(OrdersPageCode, AccessType.View)]
 285    public async Task<IActionResult> GetFeedPriceStock(int id, CancellationToken cancellationToken, bool lite = false)
 286    {
 0287        if (id <= 0)
 288        {
 0289            return BadRequest(new { message = "Не указан заказ." });
 290        }
 291
 0292        var result = await _feedPriceService.GetAsync(id, User, cancellationToken, currentOnly: lite);
 0293        return result is null ? NotFound(new { message = "Заказ не найден." }) : Ok(result);
 0294    }
 295
 296    /// <summary>Максимум заказов в одном пакетном запросе колонок Feed.</summary>
 297    private const int MaxFeedBatchOrders = 100;
 298
 299    /// <summary>
 300    /// Строка фида для каждого заказа пачки (колонки Feed в OrdersGrid и TransactionsGrid). <paramref name="ids"/> — но
 301    /// через запятую, до 100; результат по каждому заказу: <c>ok</c> с <c>forOrder</c>, <c>noAccess</c> или <c>error</c
 302    /// </summary>
 303    [HttpGet("GetFeedPriceBatch")]
 304    [PageAuthorize(OrdersPageCode, AccessType.View)]
 305    public async Task<IActionResult> GetFeedPriceBatch(string? ids, CancellationToken cancellationToken)
 306    {
 0307        var parsed = (ids ?? string.Empty)
 0308            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
 0309            .Select(x => int.TryParse(x, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.Invari
 0310            .ToArray();
 0311        if (parsed.Length == 0 || parsed.Any(id => id <= 0))
 312        {
 0313            return BadRequest(new { message = "ids: номера заказов через запятую." });
 314        }
 315
 0316        var distinct = parsed.Distinct().ToArray();
 0317        if (distinct.Length > MaxFeedBatchOrders)
 318        {
 0319            return BadRequest(new { message = $"ids: не больше {MaxFeedBatchOrders} заказов за запрос." });
 320        }
 321
 0322        var items = await _feedPriceService.GetBatchAsync(distinct, User, cancellationToken);
 0323        return Ok(new { items });
 0324    }
 325
 326    /// <summary>
 327    /// Как получилась цена позиции заказа в фиде: склад и поставщик закупки, комиссии, формула, решение, данные Озона.
 328    /// Без <paramref name="runId"/> — последняя сборка каждого кабинета, где есть план цен.
 329    /// </summary>
 330    [HttpGet("GetFeedPriceExplanation")]
 331    [PageAuthorize(OrdersPageCode, AccessType.View)]
 332    public async Task<IActionResult> GetFeedPriceExplanation(int id, long? runId, CancellationToken cancellationToken)
 333    {
 0334        if (id <= 0)
 335        {
 0336            return BadRequest(new { message = "Не указан заказ." });
 337        }
 338
 0339        if (runId is <= 0)
 340        {
 0341            return BadRequest(new { message = "runId: положительное целое число." });
 342        }
 343
 0344        var result = await _feedPriceService.GetExplanationAsync(id, User, runId, cancellationToken);
 0345        return result is null ? NotFound(new { message = "Заказ не найден." }) : Ok(result);
 0346    }
 347
 348    /// <summary>Карточка товара заказа из Partsnavigator (модалка по клику на название в OrdersGrid и TransactionsGrid)
 349    [HttpGet("GetProductCard")]
 350    [PageAuthorize(OrdersPageCode, AccessType.View)]
 351    public async Task<IActionResult> GetProductCard(int id, CancellationToken cancellationToken)
 352    {
 0353        if (id <= 0)
 354        {
 0355            return BadRequest(new { message = "Не указан заказ." });
 356        }
 357
 0358        var result = await _productCardService.GetAsync(id, User, cancellationToken);
 0359        return result is null ? NotFound(new { message = "Заказ не найден." }) : Ok(result);
 0360    }
 361
 362    /// <summary>Аналоги артикула заказа из Partsnavigator: у сайта страница аналогов медленная, поэтому отдельно от кар
 363    [HttpGet("GetProductAnalogs")]
 364    [PageAuthorize(OrdersPageCode, AccessType.View)]
 365    public async Task<IActionResult> GetProductAnalogs(int id, string? brand, CancellationToken cancellationToken)
 366    {
 0367        if (id <= 0)
 368        {
 0369            return BadRequest(new { message = "Не указан заказ." });
 370        }
 371
 0372        var result = await _productCardService.GetAnalogsAsync(id, brand, User, cancellationToken);
 0373        return result is null ? NotFound(new { message = "Заказ не найден." }) : Ok(result);
 0374    }
 375
 376    /// <summary>Сохранённые представления для левого меню с количеством заказов (кэш 60 с на пользователя).</summary>
 377    [HttpGet("GetSidebarPresets")]
 378    [PageAuthorize(OrdersPageCode, AccessType.View)]
 379    public async Task<IActionResult> GetSidebarPresets(bool counts, CancellationToken cancellationToken)
 380    {
 0381        return Ok(await _sidebarPresets.GetAsync(User, counts, cancellationToken));
 0382    }
 383
 384    /// <summary>Проверка формулы для редактора: тип результата и можно ли по ней сортировать/фильтровать на сервере.</s
 385    [HttpPost("CheckFormula")]
 386    [ValidateAntiForgeryToken]
 387    [PageAuthorize(OrdersPageCode, AccessType.View)]
 388    public async Task<IActionResult> CheckFormula([FromBody] OrdersGridCheckFormulaRequest? request, CancellationToken c
 389    {
 0390        if (request is null || string.IsNullOrWhiteSpace(request.Formula))
 391        {
 0392            return Ok(new { ok = false, message = "Пустая формула" });
 393        }
 394
 0395        var allowedColumns = await _gridQueryService.GetAllowedColumnsAsync(User, cancellationToken);
 0396        var fields = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 0397        foreach (var f in OrdersGridFormulaFields.ForAllowed(allowedColumns))
 398        {
 0399            fields[f.Key] = f.Type;
 0400            fields[f.Label] = f.Type;
 401        }
 402
 403        try
 404        {
 0405            var result = OrdersGridFormula.Validate(request.Formula, fields);
 0406            var translatable = OrdersGridFormulaTranslator.TryTranslate(request.Formula, out _, out var reason);
 0407            return Ok(new { ok = true, type = result.Type, serverCapable = translatable, reason });
 408        }
 0409        catch (OrdersGridFormula.FormulaException ex)
 410        {
 0411            return Ok(new { ok = false, message = ex.Message, position = ex.Position });
 412        }
 0413    }
 414
 415    /// <summary>Сохранить настройку пользователя (раскладка, плотность, последний фильтр, пресеты).</summary>
 416    [HttpPost("SaveSetting")]
 417    [ValidateAntiForgeryToken]
 418    [PageAuthorize(OrdersPageCode, AccessType.View)]
 419    public async Task<IActionResult> SaveSetting([FromBody] OrdersGridSaveSettingRequest? request, CancellationToken can
 420    {
 0421        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
 0422        if (string.IsNullOrEmpty(userId))
 423        {
 0424            return Unauthorized();
 425        }
 426
 0427        if (request is null || string.IsNullOrWhiteSpace(request.Key))
 428        {
 0429            return BadRequest(new { message = "Не указан ключ настройки." });
 430        }
 431
 432        try
 433        {
 0434            if (string.Equals(request.Key, "computedColumns", StringComparison.OrdinalIgnoreCase))
 435            {
 436                // Формулы проверяем на сервере: та же грамматика, что на клиенте, и только разрешённые пользователю пол
 0437                var allowedColumns = await _gridQueryService.GetAllowedColumnsAsync(User, cancellationToken);
 438                try
 439                {
 0440                    OrdersGridComputedColumns.Validate(request.Value, allowedColumns);
 0441                }
 0442                catch (OrdersGridComputedColumns.ValidationException ex)
 443                {
 0444                    return BadRequest(new { message = ex.Message });
 445                }
 446            }
 447
 0448            var saved = await _userSettings.SaveAsync(userId, request.Key, request.Value, request.Version, cancellationT
 0449            if (saved && string.Equals(request.Key, "presets", StringComparison.OrdinalIgnoreCase))
 450            {
 0451                _sidebarPresets.Invalidate(userId);
 452            }
 453
 454            // stale — в базе уже более новая версия ключа: клиент просто пропускает устаревший ответ.
 0455            return Ok(new { ok = true, stale = !saved });
 456        }
 0457        catch (ArgumentException ex)
 458        {
 0459            return BadRequest(new { message = ex.Message });
 460        }
 0461    }
 462
 463    /// <summary>Справочники для колоночных фильтров.</summary>
 464    [HttpGet("GetFilterOptions")]
 465    [PageAuthorize(OrdersPageCode, AccessType.View)]
 466    public async Task<IActionResult> GetFilterOptions(CancellationToken cancellationToken)
 467    {
 0468        var options = await _filterOptionsService.BuildAsync(User, cancellationToken);
 0469        return Ok(options);
 0470    }
 471}
 472
 473public sealed class OrdersGridCheckFormulaRequest
 474{
 475    public string Formula { get; set; } = string.Empty;
 476}
 477
 478public sealed class OrdersGridSaveSettingRequest
 479{
 480    public string Key { get; set; } = string.Empty;
 481
 482    /// <summary>JSON-строка; null/пусто — удалить настройку.</summary>
 483    public string? Value { get; set; }
 484
 485    /// <summary>Версия клиента (монотонная на ключ, например Date.now()); 0 — без проверки.</summary>
 486    public long Version { get; set; }
 487}