< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.TransactionsGridController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/TransactionsGridController.cs
Line coverage
6%
Covered lines: 10
Uncovered lines: 134
Coverable lines: 144
Total lines: 316
Line coverage: 6.9%
Branch coverage
14%
Covered branches: 12
Total branches: 84
Branch coverage: 14.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_Definition()100%210%
Index()0%2040%
Load()0%7280%
Commit()0%7280%
Preview1C()0%2040%
Lookups()0%210140%
PreviewSupplierOrder()0%4260%
SaveSetting()0%156120%
ParseIds(...)0%2040%
SanitizeReturnUrl(...)100%1212100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/TransactionsGridController.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.TransactionsGrid;
 6using OzonOrdersWeb.Authorization;
 7using OzonOrdersWeb.WebServices.Orders.Grid;
 8using OzonOrdersWeb.WebServices.Transactions.Grid.Authorization;
 9using OzonOrdersWeb.WebServices.Transactions.Grid.Commit;
 10using OzonOrdersWeb.WebServices.Transactions.Grid.Contracts;
 11using OzonOrdersWeb.WebServices.Transactions.Grid.Definitions;
 12using OzonOrdersWeb.WebServices.Transactions.Grid.Load;
 13using OzonOrdersWeb.WebServices.Transactions.Grid.Preview;
 14using OzonOrdersWeb.WebServices.Transactions.Grid.Settings;
 15using Services.ApiServices;
 16
 17namespace OzonOrdersWeb.Areas.Studio2.Controllers;
 18
 19/// <summary>
 20/// Проведение транзакций на AG Grid: одна страница на все типы (<c>{slug}</c> из каталога определений).
 21/// Права — коды классических страниц через <see cref="TransactionGridAuthorizeAttribute"/>; [ServiceEndpoint]
 22/// исключает контроллер из авто-регистрации страниц, чтобы не плодить псевдостраницы Studio2.TransactionsGrid.*.
 23/// </summary>
 24[Area("Studio2")]
 25[Authorize(Roles = "User,Admin")]
 26[Route("Studio2/TransactionsGrid")]
 27[ServiceEndpoint]
 28public sealed class TransactionsGridController : Controller
 29{
 30    private const string OrdersPageCode = "Studio2.Orders.Index";
 31    private const string ReturnUrlPrefix = "/Studio2/OrdersGrid";
 32
 33    private readonly ITransactionDefinitionProvider _definitions;
 34    private readonly ITransactionGridLoadService _load;
 35    private readonly ITransactionCommitService _commit;
 36    private readonly IOrdersGridQueryService _gridQueryService;
 37    private readonly IOrdersGridFilterOptionsService _filterOptionsService;
 38    private readonly IOrdersGridUserSettingsService _userSettings;
 39    private readonly ITransactionGridQuotationSourcesProvider _quotationSources;
 40    private readonly OneCDataManager? _oneC;
 41
 042    public TransactionsGridController(
 043        ITransactionDefinitionProvider definitions,
 044        ITransactionGridLoadService load,
 045        ITransactionCommitService commit,
 046        IOrdersGridQueryService gridQueryService,
 047        IOrdersGridFilterOptionsService filterOptionsService,
 048        IOrdersGridUserSettingsService userSettings,
 049        ITransactionGridQuotationSourcesProvider quotationSources,
 050        OneCDataManager? oneC = null)
 51    {
 052        _definitions = definitions;
 053        _load = load;
 054        _commit = commit;
 055        _gridQueryService = gridQueryService;
 056        _filterOptionsService = filterOptionsService;
 057        _userSettings = userSettings;
 058        _quotationSources = quotationSources;
 059        _oneC = oneC;
 060    }
 61
 062    private TransactionDefinition Definition => (TransactionDefinition)HttpContext.Items[TransactionGridAuthorizeAttribu
 63
 64    [HttpGet("{slug}")]
 65    [TransactionGridAuthorize(AccessType.View)]
 66    public async Task<IActionResult> Index(string slug, string? ids, string? returnUrl, CancellationToken cancellationTo
 67    {
 068        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
 069        if (string.IsNullOrEmpty(userId))
 70        {
 071            return Challenge();
 72        }
 73
 074        var d = Definition;
 075        var allowedColumns = await _gridQueryService.GetAllowedColumnsAsync(User, cancellationToken);
 076        var options = await _filterOptionsService.BuildAsync(User, cancellationToken);
 077        var stored = await _userSettings.GetAllAsync(userId, cancellationToken);
 78
 079        var model = new TransactionsGridIndexViewModel
 080        {
 081            Definition = TransactionDefinitionDto.From(d, null),
 082            OrderIds = ParseIds(ids),
 083            ReturnUrl = SanitizeReturnUrl(returnUrl),
 084            AllowedColumns = allowedColumns,
 085            FormulaFields = OrdersGridFormulaFields.ForAllowed(allowedColumns),
 086            UserSettings = TransactionGridSettingsMapper.BuildClientSettings(stored, d.Slug),
 087            StatusColors = options.StatusColors,
 088            ClientStatusColors = options.ClientStatusColors,
 089            IsAdmin = User.IsInRole("Admin"),
 090            QuotationSources = d.SupplierOffers ? await _quotationSources.GetAsync(Url, cancellationToken) : []
 091        };
 92
 093        return View(model);
 094    }
 95
 96    /// <summary>Строки, справочники и параметры страницы по списку Id.</summary>
 97    [HttpPost("{slug}/Load")]
 98    [ValidateAntiForgeryToken]
 99    [TransactionGridAuthorize(AccessType.View)]
 100    public async Task<IActionResult> Load(string slug, [FromBody] TransactionGridLoadBody? body, CancellationToken cance
 101    {
 0102        var ids = body?.Ids ?? [];
 0103        var request = new TransactionGridLoadRequest(
 0104            ids,
 0105            User,
 0106            Request.Headers.Referer.FirstOrDefault() ?? Request.Path.Value,
 0107            HttpContext.Connection.RemoteIpAddress?.ToString(),
 0108            Request.Headers.UserAgent.FirstOrDefault());
 0109        var page = await _load.LoadAsync(Definition, request, cancellationToken);
 0110        return Json(page);
 0111    }
 112
 113    /// <summary>Проведение. JSON-результат с HTML-карточкой итога и адресом возврата.</summary>
 114    [HttpPost("{slug}/Commit")]
 115    [ValidateAntiForgeryToken]
 116    [TransactionGridAuthorize(AccessType.Create)]
 117    public async Task<IActionResult> Commit(string slug, [FromBody] TransactionCommitRequest? request, CancellationToken
 118    {
 0119        if (request is null)
 120        {
 0121            return BadRequest(new { message = "Пустой запрос." });
 122        }
 123
 0124        var result = await _commit.CommitAsync(Definition, request, User, cancellationToken);
 0125        var redirect = SanitizeReturnUrl(request.ReturnUrl) ?? Url.Action("Index", "OrdersGrid", new { area = "Studio2" 
 0126        return Json(new
 0127        {
 0128            result.Success,
 0129            result.TransactionId,
 0130            result.ChangedCount,
 0131            result.SummaryHtml,
 0132            result.Warnings,
 0133            result.Errors,
 0134            result.RowErrors,
 0135            result.SupplierOrderConflicts,
 0136            RequiresConfirmation = result.SupplierOrderConflicts.Count > 0,
 0137            ErrorCode = result.ErrorCode?.ToString(),
 0138            RedirectUrl = result.Success ? redirect : null
 0139        });
 0140    }
 141
 142    /// <summary>
 143    /// Предпросмотр проведения «Отгрузка реализатору» для боковой панели: нехватка остатка в 1С, склад списания
 144    /// по засылу и исход транзита (qty = 0). Только для типа ShippedToSeller.
 145    /// </summary>
 146    [HttpPost("{slug}/Preview1C")]
 147    [ValidateAntiForgeryToken]
 148    [TransactionGridAuthorize(AccessType.View)]
 149    public async Task<IActionResult> Preview1C(string slug, [FromBody] ShipToSellerPreviewBody? body, CancellationToken 
 150    {
 0151        if (Definition.Type != OzonDomains.TransactionType.ShippedToSeller)
 152        {
 0153            return BadRequest(new { message = "Предпросмотр 1С доступен только для отгрузки реализатору." });
 154        }
 155
 0156        var preview = HttpContext.RequestServices.GetRequiredService<IShipToSellerPreviewService>();
 0157        var dto = await preview.BuildAsync(body ?? new ShipToSellerPreviewBody(), User, cancellationToken);
 0158        return Json(dto);
 0159    }
 160
 161    /// <summary>ИНН организации 1С по умолчанию (Interparts) — совпадает с OneCReceiptManager.</summary>
 162    private const string DefaultOneCOrganizationInn = "0000000000";
 163
 164    /// <summary>Ленивые справочники 1С: организации, склады и договоры по ИНН контрагента (и организации).</summary>
 165    [HttpGet("{slug}/Lookups")]
 166    [TransactionGridAuthorize(AccessType.View)]
 167    public async Task<IActionResult> Lookups(string slug, string kind, string? inn, string? org, CancellationToken cance
 168    {
 0169        if (_oneC == null)
 170        {
 0171            return Json(new { ok = false, message = "1С не настроена." });
 172        }
 173
 174        try
 175        {
 176            switch (kind)
 177            {
 178                case "treaties":
 179                    // Без ИНН контрагента — договоры всей организации по всем контрагентам («Контрагент — договор», пои
 0180                    var hasInn = !string.IsNullOrWhiteSpace(inn);
 0181                    var treaties = await _oneC.GetTreatyListAsync(hasInn ? inn!.Trim() : null, string.IsNullOrWhiteSpace
 0182                    Response.Headers.CacheControl = "private, max-age=120";
 0183                    return Json(new
 0184                    {
 0185                        ok = true,
 0186                        allCounterparties = !hasInn,
 0187                        items = treaties.Select(t =>
 0188                        {
 0189                            var name = string.IsNullOrWhiteSpace(t.Name) ? $"Договор без названия ({t.Link?[..Math.Min(8
 0190                            return new
 0191                            {
 0192                                id = t.Link,
 0193                                name = hasInn || string.IsNullOrWhiteSpace(t.Partner) ? name : $"{t.Partner} — {name}",
 0194                                hint = hasInn ? null : (string.IsNullOrWhiteSpace(t.Inn) ? null : "ИНН " + t.Inn)
 0195                            };
 0196                        })
 0197                    });
 198                case "organizations":
 0199                    var organizations = await _oneC.GetOrganizationListAsync();
 0200                    Response.Headers.CacheControl = "private, max-age=300";
 0201                    return Json(new
 0202                    {
 0203                        ok = true,
 204                        items = organizations.Select(o => new { id = o.Inn, name = o.Name, inn = o.Inn, isDefault = o.In
 0205                    });
 206                case "oneCWarehouses":
 0207                    var warehouses = await _oneC.GetWarehouseListAsync();
 208                    return Json(new { ok = true, items = warehouses.Select(w => w.Name).Where(n => !string.IsNullOrWhite
 209                case "countries":
 0210                    var countries = await _oneC.GetCountryListAsync();
 211                    return Json(new { ok = true, items = countries.Where(n => !string.IsNullOrWhiteSpace(n)).OrderBy(n =
 212                default:
 0213                    return BadRequest(new { message = "Неизвестный справочник." });
 214            }
 215        }
 0216        catch (Exception ex)
 217        {
 0218            return Json(new { ok = false, message = ex.Message });
 219        }
 0220    }
 221
 222    /// <summary>Предпросмотр прямого заказа у поставщика (пока только Armtek — как на классической странице).</summary>
 223    [HttpPost("{slug}/PreviewSupplierOrder")]
 224    [ValidateAntiForgeryToken]
 225    [TransactionGridAuthorize(AccessType.View)]
 226    public async Task<IActionResult> PreviewSupplierOrder(string slug, [FromBody] TransactionGridPreviewBody? body, Canc
 227    {
 0228        if (!Definition.SupplierCarts)
 229        {
 0230            return BadRequest(new { error = "Корзины поставщиков недоступны для этой транзакции." });
 231        }
 232
 0233        var processor = HttpContext.RequestServices.GetRequiredService<WebServices.Transactions.Grid.SupplierCarts.Suppl
 0234        var (ok, error, previews) = await processor.PreviewArmtekAsync(body?.QueueJson, cancellationToken);
 0235        return ok ? Json(new { orders = previews }) : BadRequest(new { error });
 0236    }
 237
 238    /// <summary>Настройки грида: клиентские ключи OrdersGrid ремапятся в общий слой tx.*.</summary>
 239    [HttpPost("SaveSetting")]
 240    [ValidateAntiForgeryToken]
 241    [PageAuthorize(OrdersPageCode, AccessType.View)]
 242    public async Task<IActionResult> SaveSetting(string? slug, [FromBody] OrdersGridSaveSettingRequest? request, Cancell
 243    {
 0244        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
 0245        if (string.IsNullOrEmpty(userId))
 246        {
 0247            return Unauthorized();
 248        }
 249
 0250        if (request is null || string.IsNullOrWhiteSpace(request.Key) || string.IsNullOrWhiteSpace(slug))
 251        {
 0252            return BadRequest(new { message = "Не указан ключ настройки." });
 253        }
 254
 0255        if (TransactionDefinitionCatalog.BySlug(slug) == null)
 256        {
 0257            return NotFound();
 258        }
 259
 0260        var storageKey = TransactionGridSettingsMapper.ToStorageKey(request.Key, slug);
 0261        if (storageKey == null)
 262        {
 263            // lastView/presets/computedColumns не сохраняются со страницы транзакций.
 0264            return Ok(new { ok = true, ignored = true });
 265        }
 266
 267        try
 268        {
 0269            var saved = await _userSettings.SaveAsync(userId, storageKey, request.Value, request.Version, cancellationTo
 0270            return Ok(new { ok = true, stale = !saved });
 271        }
 0272        catch (ArgumentException ex)
 273        {
 0274            return BadRequest(new { message = ex.Message });
 275        }
 0276    }
 277
 278    private static List<int> ParseIds(string? ids)
 0279        => (ids ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
 0280            .Select(s => int.TryParse(s, out var v) ? v : 0)
 0281            .Where(v => v > 0)
 0282            .Distinct()
 0283            .ToList();
 284
 285    /// <summary>Возврат только на OrdersGrid того же сайта: относительный путь с известным префиксом.</summary>
 286    internal static string? SanitizeReturnUrl(string? returnUrl)
 287    {
 7288        if (string.IsNullOrWhiteSpace(returnUrl))
 289        {
 1290            return null;
 291        }
 292
 6293        var value = returnUrl.Trim();
 6294        if (!value.StartsWith('/') || value.StartsWith("//", StringComparison.Ordinal) || value.Contains('\\'))
 295        {
 2296            return null;
 297        }
 298
 4299        var path = value.Split('?', 2)[0].TrimEnd('/');
 4300        return string.Equals(path, ReturnUrlPrefix, StringComparison.OrdinalIgnoreCase)
 4301               || string.Equals(path, ReturnUrlPrefix + "/Index", StringComparison.OrdinalIgnoreCase)
 4302            ? value
 4303            : null;
 304    }
 305}
 306
 307public sealed class TransactionGridLoadBody
 308{
 309    public List<int> Ids { get; set; } = new();
 310}
 311
 312public sealed class TransactionGridPreviewBody
 313{
 314    public string? ProviderKey { get; set; }
 315    public string? QueueJson { get; set; }
 316}