< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.OrdersGridCheckFormulaRequest
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OrdersGridController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 1
Coverable lines: 1
Total lines: 487
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Formula()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
 39    public OrdersGridController(
 40        IOrdersGridQueryService gridQueryService,
 41        IOrdersGridFilterOptionsService filterOptionsService,
 42        IOrdersIndexPageViewDataService pageViewDataService,
 43        IOrdersGridUserSettingsService userSettings,
 44        IOrdersGridChangesService changesService,
 45        IOrdersGridStatusRefreshService statusRefreshService,
 46        IOrdersGridFacetsService facetsService,
 47        IOrdersGridOrderCardService orderCardService,
 48        IOrdersGridFeedPriceService feedPriceService,
 49        IOrdersGridProductCardService productCardService,
 50        IOrdersGridSidebarPresetsService sidebarPresets,
 51        ILogger<OrdersGridController> logger)
 52    {
 53        _sidebarPresets = sidebarPresets;
 54        _facetsService = facetsService;
 55        _orderCardService = orderCardService;
 56        _feedPriceService = feedPriceService;
 57        _productCardService = productCardService;
 58        _changesService = changesService;
 59        _statusRefreshService = statusRefreshService;
 60        _gridQueryService = gridQueryService;
 61        _filterOptionsService = filterOptionsService;
 62        _pageViewDataService = pageViewDataService;
 63        _userSettings = userSettings;
 64        _logger = logger;
 65    }
 66
 67    [HttpGet("")]
 68    [HttpGet("Index")]
 69    [PageAuthorize(OrdersPageCode, AccessType.View)]
 70    public async Task<IActionResult> Index(CancellationToken cancellationToken)
 71    {
 72        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
 73        if (string.IsNullOrEmpty(userId))
 74        {
 75            return Challenge();
 76        }
 77
 78        var isAdmin = User.IsInRole("Admin");
 79        var allowedColumns = await _gridQueryService.GetAllowedColumnsAsync(User, cancellationToken);
 80        var options = await _filterOptionsService.BuildAsync(User, cancellationToken);
 81
 82        var pageViewData = await _pageViewDataService.BuildAsync(new OrdersIndexPageViewDataRequest
 83        {
 84            UserId = userId,
 85            IsWebAdmin = isAdmin,
 86            CancellationToken = cancellationToken
 87        });
 88        OrdersIndexPageViewDataApplicator.Apply(pageViewData, ViewBag);
 89
 90        var userSettings = await _userSettings.GetAllAsync(userId, cancellationToken);
 91
 92        var model = new OrdersGridIndexViewModel
 93        {
 94            AllowedColumns = allowedColumns,
 95            FormulaFields = OrdersGridFormulaFields.ForAllowed(allowedColumns),
 96            UserSettings = userSettings,
 97            StatusColors = options.StatusColors,
 98            ClientStatusColors = options.ClientStatusColors,
 99            IsAdmin = isAdmin,
 100            IsAppSuperAdmin = pageViewData.IsAppSuperAdmin,
 101            BlockSize = _gridQueryService.BlockSize
 102        };
 103
 104        return View(model);
 105    }
 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    {
 113        if (request is null)
 114        {
 115            return BadRequest(new { message = "Пустой запрос." });
 116        }
 117
 118        var blockSize = _gridQueryService.BlockSize;
 119        if (request.StartRow < 0 || request.EndRow - request.StartRow != blockSize || request.StartRow % blockSize != 0)
 120        {
 121            return BadRequest(new { message = $"Ожидается блок по {blockSize} строк, выровненный по началу блока." });
 122        }
 123
 124        if (AgGridRequestLimits.Validate(request.FilterModel, request.SortModel) is { } limitError)
 125        {
 126            return BadRequest(new { message = limitError });
 127        }
 128
 129        try
 130        {
 131            var result = await _gridQueryService.LoadBlockAsync(request, User, cancellationToken);
 132            return Ok(result);
 133        }
 134        catch (UnauthorizedAccessException)
 135        {
 136            return Unauthorized();
 137        }
 138        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 139        {
 140            return StatusCode(StatusCodes.Status499ClientClosedRequest);
 141        }
 142        catch (Exception ex)
 143        {
 144            _logger.LogError(ex, "OrdersGrid: ошибка загрузки блока {StartRow}-{EndRow}", request.StartRow, request.EndR
 145            return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Не удалось загрузить заказы." }
 146        }
 147    }
 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    {
 155        if (request is null || !ModelState.IsValid)
 156        {
 157            ModelState.Clear();
 158            request = new OrdersGridChangesRequest();
 159        }
 160
 161        try
 162        {
 163            return Ok(await _changesService.GetChangesAsync(request, User, cancellationToken));
 164        }
 165        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 166        {
 167            return StatusCode(StatusCodes.Status499ClientClosedRequest);
 168        }
 169        catch (Exception ex)
 170        {
 171            _logger.LogError(ex, "OrdersGrid: ошибка дельты изменений");
 172            return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Не удалось получить изменения."
 173        }
 174    }
 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    {
 182        if (request?.Ids is not { Count: > 0 })
 183        {
 184            return BadRequest(new { message = "Не переданы заказы для обновления." });
 185        }
 186
 187        try
 188        {
 189            return Ok(await _statusRefreshService.RefreshAsync(request.Ids, User, cancellationToken));
 190        }
 191        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 192        {
 193            return StatusCode(StatusCodes.Status499ClientClosedRequest);
 194        }
 195        catch (Exception ex)
 196        {
 197            _logger.LogError(ex, "OrdersGrid: ошибка обновления статусов");
 198            return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Не удалось обновить статусы." }
 199        }
 200    }
 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    {
 208        if (request is null || string.IsNullOrWhiteSpace(request.Column))
 209        {
 210            return BadRequest(new { message = "Не указана колонка." });
 211        }
 212
 213        if (AgGridRequestLimits.Validate(request.FilterModel, null) is { } limitError)
 214        {
 215            return BadRequest(new { message = limitError });
 216        }
 217
 218        try
 219        {
 220            return Ok(await _facetsService.GetFacetsAsync(request, User, cancellationToken));
 221        }
 222        catch (UnauthorizedAccessException)
 223        {
 224            return Unauthorized();
 225        }
 226        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 227        {
 228            return StatusCode(StatusCodes.Status499ClientClosedRequest);
 229        }
 230        catch (Exception ex)
 231        {
 232            _logger.LogError(ex, "OrdersGrid: ошибка счётчиков фильтра {Column}", request.Column);
 233            return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Не удалось посчитать значения."
 234        }
 235    }
 236
 237    /// <summary>Карточка заказа для боковой панели: строка со всеми колонками и последние изменения.</summary>
 238    [HttpGet("GetOrderCard")]
 239    [PageAuthorize(OrdersPageCode, AccessType.View)]
 240    public async Task<IActionResult> GetOrderCard(int id, CancellationToken cancellationToken)
 241    {
 242        if (id <= 0)
 243        {
 244            return BadRequest(new { message = "Не указан заказ." });
 245        }
 246
 247        var card = await _orderCardService.GetCardAsync(id, User, cancellationToken);
 248        return card is null ? NotFound(new { message = "Заказ не найден." }) : Ok(card);
 249    }
 250
 251    /// <summary>История переходов статусов заказа (попап по клику на статус).</summary>
 252    [HttpGet("GetStatusHistory")]
 253    [PageAuthorize(OrdersPageCode, AccessType.View)]
 254    public async Task<IActionResult> GetStatusHistory(int id, CancellationToken cancellationToken)
 255    {
 256        if (id <= 0)
 257        {
 258            return BadRequest(new { message = "Не указан заказ." });
 259        }
 260
 261        var history = await _orderCardService.GetStatusHistoryAsync(id, User, cancellationToken);
 262        return history is null ? NotFound(new { message = "Заказ не найден." }) : Ok(history);
 263    }
 264
 265    /// <summary>Транзакции, в которых участвовал заказ: тип, кто провёл, когда, комментарий (попап истории статусов).</
 266    [HttpGet("GetOrderTransactions")]
 267    [PageAuthorize(OrdersPageCode, AccessType.View)]
 268    public async Task<IActionResult> GetOrderTransactions(int id, CancellationToken cancellationToken)
 269    {
 270        if (id <= 0)
 271        {
 272            return BadRequest(new { message = "Не указан заказ." });
 273        }
 274
 275        var transactions = await _orderCardService.GetOrderTransactionsAsync(id, User, cancellationToken);
 276        return transactions is null ? NotFound(new { message = "Заказ не найден." }) : Ok(transactions);
 277    }
 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    {
 287        if (id <= 0)
 288        {
 289            return BadRequest(new { message = "Не указан заказ." });
 290        }
 291
 292        var result = await _feedPriceService.GetAsync(id, User, cancellationToken, currentOnly: lite);
 293        return result is null ? NotFound(new { message = "Заказ не найден." }) : Ok(result);
 294    }
 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    {
 307        var parsed = (ids ?? string.Empty)
 308            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
 309            .Select(x => int.TryParse(x, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.Invari
 310            .ToArray();
 311        if (parsed.Length == 0 || parsed.Any(id => id <= 0))
 312        {
 313            return BadRequest(new { message = "ids: номера заказов через запятую." });
 314        }
 315
 316        var distinct = parsed.Distinct().ToArray();
 317        if (distinct.Length > MaxFeedBatchOrders)
 318        {
 319            return BadRequest(new { message = $"ids: не больше {MaxFeedBatchOrders} заказов за запрос." });
 320        }
 321
 322        var items = await _feedPriceService.GetBatchAsync(distinct, User, cancellationToken);
 323        return Ok(new { items });
 324    }
 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    {
 334        if (id <= 0)
 335        {
 336            return BadRequest(new { message = "Не указан заказ." });
 337        }
 338
 339        if (runId is <= 0)
 340        {
 341            return BadRequest(new { message = "runId: положительное целое число." });
 342        }
 343
 344        var result = await _feedPriceService.GetExplanationAsync(id, User, runId, cancellationToken);
 345        return result is null ? NotFound(new { message = "Заказ не найден." }) : Ok(result);
 346    }
 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    {
 353        if (id <= 0)
 354        {
 355            return BadRequest(new { message = "Не указан заказ." });
 356        }
 357
 358        var result = await _productCardService.GetAsync(id, User, cancellationToken);
 359        return result is null ? NotFound(new { message = "Заказ не найден." }) : Ok(result);
 360    }
 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    {
 367        if (id <= 0)
 368        {
 369            return BadRequest(new { message = "Не указан заказ." });
 370        }
 371
 372        var result = await _productCardService.GetAnalogsAsync(id, brand, User, cancellationToken);
 373        return result is null ? NotFound(new { message = "Заказ не найден." }) : Ok(result);
 374    }
 375
 376    /// <summary>Сохранённые представления для левого меню с количеством заказов (кэш 60 с на пользователя).</summary>
 377    [HttpGet("GetSidebarPresets")]
 378    [PageAuthorize(OrdersPageCode, AccessType.View)]
 379    public async Task<IActionResult> GetSidebarPresets(bool counts, CancellationToken cancellationToken)
 380    {
 381        return Ok(await _sidebarPresets.GetAsync(User, counts, cancellationToken));
 382    }
 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    {
 390        if (request is null || string.IsNullOrWhiteSpace(request.Formula))
 391        {
 392            return Ok(new { ok = false, message = "Пустая формула" });
 393        }
 394
 395        var allowedColumns = await _gridQueryService.GetAllowedColumnsAsync(User, cancellationToken);
 396        var fields = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 397        foreach (var f in OrdersGridFormulaFields.ForAllowed(allowedColumns))
 398        {
 399            fields[f.Key] = f.Type;
 400            fields[f.Label] = f.Type;
 401        }
 402
 403        try
 404        {
 405            var result = OrdersGridFormula.Validate(request.Formula, fields);
 406            var translatable = OrdersGridFormulaTranslator.TryTranslate(request.Formula, out _, out var reason);
 407            return Ok(new { ok = true, type = result.Type, serverCapable = translatable, reason });
 408        }
 409        catch (OrdersGridFormula.FormulaException ex)
 410        {
 411            return Ok(new { ok = false, message = ex.Message, position = ex.Position });
 412        }
 413    }
 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    {
 421        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
 422        if (string.IsNullOrEmpty(userId))
 423        {
 424            return Unauthorized();
 425        }
 426
 427        if (request is null || string.IsNullOrWhiteSpace(request.Key))
 428        {
 429            return BadRequest(new { message = "Не указан ключ настройки." });
 430        }
 431
 432        try
 433        {
 434            if (string.Equals(request.Key, "computedColumns", StringComparison.OrdinalIgnoreCase))
 435            {
 436                // Формулы проверяем на сервере: та же грамматика, что на клиенте, и только разрешённые пользователю пол
 437                var allowedColumns = await _gridQueryService.GetAllowedColumnsAsync(User, cancellationToken);
 438                try
 439                {
 440                    OrdersGridComputedColumns.Validate(request.Value, allowedColumns);
 441                }
 442                catch (OrdersGridComputedColumns.ValidationException ex)
 443                {
 444                    return BadRequest(new { message = ex.Message });
 445                }
 446            }
 447
 448            var saved = await _userSettings.SaveAsync(userId, request.Key, request.Value, request.Version, cancellationT
 449            if (saved && string.Equals(request.Key, "presets", StringComparison.OrdinalIgnoreCase))
 450            {
 451                _sidebarPresets.Invalidate(userId);
 452            }
 453
 454            // stale — в базе уже более новая версия ключа: клиент просто пропускает устаревший ответ.
 455            return Ok(new { ok = true, stale = !saved });
 456        }
 457        catch (ArgumentException ex)
 458        {
 459            return BadRequest(new { message = ex.Message });
 460        }
 461    }
 462
 463    /// <summary>Справочники для колоночных фильтров.</summary>
 464    [HttpGet("GetFilterOptions")]
 465    [PageAuthorize(OrdersPageCode, AccessType.View)]
 466    public async Task<IActionResult> GetFilterOptions(CancellationToken cancellationToken)
 467    {
 468        var options = await _filterOptionsService.BuildAsync(User, cancellationToken);
 469        return Ok(options);
 470    }
 471}
 472
 473public sealed class OrdersGridCheckFormulaRequest
 474{
 0475    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}

Methods/Properties

get_Formula()