< Summary

Information
Class: OzonOrdersWeb.Filters.SupplierOrderGuardFilter
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Filters/SupplierOrderGuardFilter.cs
Line coverage
70%
Covered lines: 41
Uncovered lines: 17
Coverable lines: 58
Total lines: 126
Line coverage: 70.6%
Branch coverage
52%
Covered branches: 24
Total branches: 46
Branch coverage: 52.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)50%22100%
OnActionExecutionAsync()72.22%321864.86%
ResolveCartItemIds(...)36.36%402266.66%
ReadConfirmedIds(...)50%44100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Filters/SupplierOrderGuardFilter.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Mvc;
 2using Microsoft.AspNetCore.Mvc.Filters;
 3using OzonOrdersWeb.TransactionServices;
 4
 5namespace OzonOrdersWeb.Filters;
 6
 7/// <summary>
 8/// Защита от повторных заказов для эндпоинтов, которые отправляют реальный заказ поставщику из его корзины.
 9/// Ставится на действие вида «Создать заказ»: фильтр определяет заказы Studio2 по выбранным строкам корзины и,
 10/// если какой-то из них уже заказан у поставщика, отвечает <c>409</c> со структурированным списком конфликтов
 11/// (<c>conflicts</c>) вместо вызова действия. Повторить можно только запросом с полем формы
 12/// <see cref="ConfirmedFieldName"/> — отпечатками конфликтов, которые пользователь увидел и разрешил.
 13/// </summary>
 14[AttributeUsage(AttributeTargets.Method)]
 15public sealed class SupplierOrderGuardAttribute : TypeFilterAttribute
 16{
 17    public SupplierOrderGuardAttribute(string? providerKey = null, bool nullMeansAll = true)
 18        : base(typeof(SupplierOrderGuardFilter))
 19    {
 20        Arguments = [providerKey ?? string.Empty, nullMeansAll];
 21    }
 22}
 23
 24public sealed class SupplierOrderGuardFilter : IAsyncActionFilter
 25{
 26    public const string ConfirmedFieldName = "confirmedConflictIds";
 27
 128    private static readonly string[] IdArguments = ["cartItemIds", "basketItemIds", "basketIds"];
 129    private static readonly string[] AllFlagArguments = ["createAll", "checkoutAll"];
 30
 31    private readonly ISupplierOrderGuard _guard;
 32    private readonly string? _providerKey;
 33    private readonly bool _nullMeansAll;
 34
 835    public SupplierOrderGuardFilter(ISupplierOrderGuard guard, string? providerKey, bool nullMeansAll)
 36    {
 837        _guard = guard;
 838        _providerKey = string.IsNullOrWhiteSpace(providerKey) ? null : providerKey;
 839        _nullMeansAll = nullMeansAll;
 840    }
 41
 42    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
 43    {
 844        var provider = _providerKey ?? (context.ActionArguments.TryGetValue("provider", out var value) ? value as string
 845        if (string.IsNullOrWhiteSpace(provider))
 46        {
 47            // Провайдер неизвестен — действие само вернёт ошибку валидации.
 048            await next();
 049            return;
 50        }
 51
 852        var ct = context.HttpContext.RequestAborted;
 853        var user = context.HttpContext.User.Identity?.Name;
 854        var targets = await _guard.ResolveCartTargetsAsync(provider, ResolveCartItemIds(context), ct);
 855        if (targets.Count == 0)
 56        {
 057            await next();
 058            return;
 59        }
 60
 861        var confirmation = SupplierOrderConfirmation.FromIds(ReadConfirmedIds(context.HttpContext.Request));
 862        var result = await _guard.EvaluateAsync(targets, confirmation, user, ct);
 863        if (!result.IsAllowed)
 64        {
 465            context.Result = new ObjectResult(new
 466            {
 467                success = false,
 468                requiresConfirmation = true,
 469                error = result.BlockingMessage(),
 470                conflicts = result.Blocking
 471            })
 472            { StatusCode = StatusCodes.Status409Conflict };
 473            return;
 74        }
 75
 76        // Намерение фиксируется ДО обращения к поставщику; если другой запрос успел раньше — заказ не отправляется.
 477        var start = await _guard.BeginDispatchAsync(Guid.NewGuid().ToString("N"), "SupplierCart", targets, result, user,
 478        if (!start.CanProceed)
 79        {
 080            context.Result = new ObjectResult(new
 081            {
 082                success = false,
 083                requiresConfirmation = false,
 084                error = start.Message,
 085                conflicts = start.Conflicts
 086            })
 087            { StatusCode = StatusCodes.Status409Conflict };
 088            return;
 89        }
 90
 491        var executed = await next();
 92
 93        // Исключение (в том числе обрыв запроса) — исход неизвестен: намерение остаётся Placing и блокирует повтор до с
 494        if (executed.Exception is null || executed.ExceptionHandled)
 395            await _guard.CompleteDispatchAsync(start.Dispatch!);
 896    }
 97
 98    /// <returns><c>null</c> — вся активная корзина; иначе выбранные строки (может быть пустым).</returns>
 99    private IReadOnlyCollection<string>? ResolveCartItemIds(ActionExecutingContext context)
 100    {
 48101        foreach (var name in AllFlagArguments)
 102        {
 16103            if (context.ActionArguments.TryGetValue(name, out var flag) && flag is true) return null;
 104        }
 105
 24106        foreach (var name in IdArguments)
 107        {
 8108            if (!context.ActionArguments.TryGetValue(name, out var ids)) continue;
 8109            return ids switch
 8110            {
 16111                long[] numbers => numbers.Select(x => x.ToString(System.Globalization.CultureInfo.InvariantCulture)).ToA
 0112                string[] texts => texts,
 0113                null => _nullMeansAll ? null : [],
 0114                _ => []
 8115            };
 116        }
 117
 0118        return _nullMeansAll ? null : [];
 119    }
 120
 121    private static IEnumerable<string> ReadConfirmedIds(HttpRequest request)
 122    {
 8123        if (!request.HasFormContentType) return [];
 10124        return request.Form[ConfirmedFieldName].SelectMany(x => (x ?? string.Empty).Split(',', StringSplitOptions.Remove
 125    }
 126}