< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.TransactionGridPreviewBody
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/TransactionsGridController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 2
Coverable lines: 2
Total lines: 316
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_ProviderKey()100%210%
get_QueueJson()100%210%

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
 42    public TransactionsGridController(
 43        ITransactionDefinitionProvider definitions,
 44        ITransactionGridLoadService load,
 45        ITransactionCommitService commit,
 46        IOrdersGridQueryService gridQueryService,
 47        IOrdersGridFilterOptionsService filterOptionsService,
 48        IOrdersGridUserSettingsService userSettings,
 49        ITransactionGridQuotationSourcesProvider quotationSources,
 50        OneCDataManager? oneC = null)
 51    {
 52        _definitions = definitions;
 53        _load = load;
 54        _commit = commit;
 55        _gridQueryService = gridQueryService;
 56        _filterOptionsService = filterOptionsService;
 57        _userSettings = userSettings;
 58        _quotationSources = quotationSources;
 59        _oneC = oneC;
 60    }
 61
 62    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    {
 68        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
 69        if (string.IsNullOrEmpty(userId))
 70        {
 71            return Challenge();
 72        }
 73
 74        var d = Definition;
 75        var allowedColumns = await _gridQueryService.GetAllowedColumnsAsync(User, cancellationToken);
 76        var options = await _filterOptionsService.BuildAsync(User, cancellationToken);
 77        var stored = await _userSettings.GetAllAsync(userId, cancellationToken);
 78
 79        var model = new TransactionsGridIndexViewModel
 80        {
 81            Definition = TransactionDefinitionDto.From(d, null),
 82            OrderIds = ParseIds(ids),
 83            ReturnUrl = SanitizeReturnUrl(returnUrl),
 84            AllowedColumns = allowedColumns,
 85            FormulaFields = OrdersGridFormulaFields.ForAllowed(allowedColumns),
 86            UserSettings = TransactionGridSettingsMapper.BuildClientSettings(stored, d.Slug),
 87            StatusColors = options.StatusColors,
 88            ClientStatusColors = options.ClientStatusColors,
 89            IsAdmin = User.IsInRole("Admin"),
 90            QuotationSources = d.SupplierOffers ? await _quotationSources.GetAsync(Url, cancellationToken) : []
 91        };
 92
 93        return View(model);
 94    }
 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    {
 102        var ids = body?.Ids ?? [];
 103        var request = new TransactionGridLoadRequest(
 104            ids,
 105            User,
 106            Request.Headers.Referer.FirstOrDefault() ?? Request.Path.Value,
 107            HttpContext.Connection.RemoteIpAddress?.ToString(),
 108            Request.Headers.UserAgent.FirstOrDefault());
 109        var page = await _load.LoadAsync(Definition, request, cancellationToken);
 110        return Json(page);
 111    }
 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    {
 119        if (request is null)
 120        {
 121            return BadRequest(new { message = "Пустой запрос." });
 122        }
 123
 124        var result = await _commit.CommitAsync(Definition, request, User, cancellationToken);
 125        var redirect = SanitizeReturnUrl(request.ReturnUrl) ?? Url.Action("Index", "OrdersGrid", new { area = "Studio2" 
 126        return Json(new
 127        {
 128            result.Success,
 129            result.TransactionId,
 130            result.ChangedCount,
 131            result.SummaryHtml,
 132            result.Warnings,
 133            result.Errors,
 134            result.RowErrors,
 135            result.SupplierOrderConflicts,
 136            RequiresConfirmation = result.SupplierOrderConflicts.Count > 0,
 137            ErrorCode = result.ErrorCode?.ToString(),
 138            RedirectUrl = result.Success ? redirect : null
 139        });
 140    }
 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    {
 151        if (Definition.Type != OzonDomains.TransactionType.ShippedToSeller)
 152        {
 153            return BadRequest(new { message = "Предпросмотр 1С доступен только для отгрузки реализатору." });
 154        }
 155
 156        var preview = HttpContext.RequestServices.GetRequiredService<IShipToSellerPreviewService>();
 157        var dto = await preview.BuildAsync(body ?? new ShipToSellerPreviewBody(), User, cancellationToken);
 158        return Json(dto);
 159    }
 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    {
 169        if (_oneC == null)
 170        {
 171            return Json(new { ok = false, message = "1С не настроена." });
 172        }
 173
 174        try
 175        {
 176            switch (kind)
 177            {
 178                case "treaties":
 179                    // Без ИНН контрагента — договоры всей организации по всем контрагентам («Контрагент — договор», пои
 180                    var hasInn = !string.IsNullOrWhiteSpace(inn);
 181                    var treaties = await _oneC.GetTreatyListAsync(hasInn ? inn!.Trim() : null, string.IsNullOrWhiteSpace
 182                    Response.Headers.CacheControl = "private, max-age=120";
 183                    return Json(new
 184                    {
 185                        ok = true,
 186                        allCounterparties = !hasInn,
 187                        items = treaties.Select(t =>
 188                        {
 189                            var name = string.IsNullOrWhiteSpace(t.Name) ? $"Договор без названия ({t.Link?[..Math.Min(8
 190                            return new
 191                            {
 192                                id = t.Link,
 193                                name = hasInn || string.IsNullOrWhiteSpace(t.Partner) ? name : $"{t.Partner} — {name}",
 194                                hint = hasInn ? null : (string.IsNullOrWhiteSpace(t.Inn) ? null : "ИНН " + t.Inn)
 195                            };
 196                        })
 197                    });
 198                case "organizations":
 199                    var organizations = await _oneC.GetOrganizationListAsync();
 200                    Response.Headers.CacheControl = "private, max-age=300";
 201                    return Json(new
 202                    {
 203                        ok = true,
 204                        items = organizations.Select(o => new { id = o.Inn, name = o.Name, inn = o.Inn, isDefault = o.In
 205                    });
 206                case "oneCWarehouses":
 207                    var warehouses = await _oneC.GetWarehouseListAsync();
 208                    return Json(new { ok = true, items = warehouses.Select(w => w.Name).Where(n => !string.IsNullOrWhite
 209                case "countries":
 210                    var countries = await _oneC.GetCountryListAsync();
 211                    return Json(new { ok = true, items = countries.Where(n => !string.IsNullOrWhiteSpace(n)).OrderBy(n =
 212                default:
 213                    return BadRequest(new { message = "Неизвестный справочник." });
 214            }
 215        }
 216        catch (Exception ex)
 217        {
 218            return Json(new { ok = false, message = ex.Message });
 219        }
 220    }
 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    {
 228        if (!Definition.SupplierCarts)
 229        {
 230            return BadRequest(new { error = "Корзины поставщиков недоступны для этой транзакции." });
 231        }
 232
 233        var processor = HttpContext.RequestServices.GetRequiredService<WebServices.Transactions.Grid.SupplierCarts.Suppl
 234        var (ok, error, previews) = await processor.PreviewArmtekAsync(body?.QueueJson, cancellationToken);
 235        return ok ? Json(new { orders = previews }) : BadRequest(new { error });
 236    }
 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    {
 244        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
 245        if (string.IsNullOrEmpty(userId))
 246        {
 247            return Unauthorized();
 248        }
 249
 250        if (request is null || string.IsNullOrWhiteSpace(request.Key) || string.IsNullOrWhiteSpace(slug))
 251        {
 252            return BadRequest(new { message = "Не указан ключ настройки." });
 253        }
 254
 255        if (TransactionDefinitionCatalog.BySlug(slug) == null)
 256        {
 257            return NotFound();
 258        }
 259
 260        var storageKey = TransactionGridSettingsMapper.ToStorageKey(request.Key, slug);
 261        if (storageKey == null)
 262        {
 263            // lastView/presets/computedColumns не сохраняются со страницы транзакций.
 264            return Ok(new { ok = true, ignored = true });
 265        }
 266
 267        try
 268        {
 269            var saved = await _userSettings.SaveAsync(userId, storageKey, request.Value, request.Version, cancellationTo
 270            return Ok(new { ok = true, stale = !saved });
 271        }
 272        catch (ArgumentException ex)
 273        {
 274            return BadRequest(new { message = ex.Message });
 275        }
 276    }
 277
 278    private static List<int> ParseIds(string? ids)
 279        => (ids ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
 280            .Select(s => int.TryParse(s, out var v) ? v : 0)
 281            .Where(v => v > 0)
 282            .Distinct()
 283            .ToList();
 284
 285    /// <summary>Возврат только на OrdersGrid того же сайта: относительный путь с известным префиксом.</summary>
 286    internal static string? SanitizeReturnUrl(string? returnUrl)
 287    {
 288        if (string.IsNullOrWhiteSpace(returnUrl))
 289        {
 290            return null;
 291        }
 292
 293        var value = returnUrl.Trim();
 294        if (!value.StartsWith('/') || value.StartsWith("//", StringComparison.Ordinal) || value.Contains('\\'))
 295        {
 296            return null;
 297        }
 298
 299        var path = value.Split('?', 2)[0].TrimEnd('/');
 300        return string.Equals(path, ReturnUrlPrefix, StringComparison.OrdinalIgnoreCase)
 301               || string.Equals(path, ReturnUrlPrefix + "/Index", StringComparison.OrdinalIgnoreCase)
 302            ? value
 303            : null;
 304    }
 305}
 306
 307public sealed class TransactionGridLoadBody
 308{
 309    public List<int> Ids { get; set; } = new();
 310}
 311
 312public sealed class TransactionGridPreviewBody
 313{
 0314    public string? ProviderKey { get; set; }
 0315    public string? QueueJson { get; set; }
 316}

Methods/Properties

get_ProviderKey()
get_QueueJson()