< Summary

Information
Class: OzonOrdersWeb.Filters.SupplierOrderGuardAttribute
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Filters/SupplierOrderGuardFilter.cs
Line coverage
100%
Covered lines: 3
Uncovered lines: 0
Coverable lines: 3
Total lines: 126
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%

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)
 14418        : base(typeof(SupplierOrderGuardFilter))
 19    {
 14420        Arguments = [providerKey ?? string.Empty, nullMeansAll];
 14421    }
 22}
 23
 24public sealed class SupplierOrderGuardFilter : IAsyncActionFilter
 25{
 26    public const string ConfirmedFieldName = "confirmedConflictIds";
 27
 28    private static readonly string[] IdArguments = ["cartItemIds", "basketItemIds", "basketIds"];
 29    private static readonly string[] AllFlagArguments = ["createAll", "checkoutAll"];
 30
 31    private readonly ISupplierOrderGuard _guard;
 32    private readonly string? _providerKey;
 33    private readonly bool _nullMeansAll;
 34
 35    public SupplierOrderGuardFilter(ISupplierOrderGuard guard, string? providerKey, bool nullMeansAll)
 36    {
 37        _guard = guard;
 38        _providerKey = string.IsNullOrWhiteSpace(providerKey) ? null : providerKey;
 39        _nullMeansAll = nullMeansAll;
 40    }
 41
 42    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
 43    {
 44        var provider = _providerKey ?? (context.ActionArguments.TryGetValue("provider", out var value) ? value as string
 45        if (string.IsNullOrWhiteSpace(provider))
 46        {
 47            // Провайдер неизвестен — действие само вернёт ошибку валидации.
 48            await next();
 49            return;
 50        }
 51
 52        var ct = context.HttpContext.RequestAborted;
 53        var user = context.HttpContext.User.Identity?.Name;
 54        var targets = await _guard.ResolveCartTargetsAsync(provider, ResolveCartItemIds(context), ct);
 55        if (targets.Count == 0)
 56        {
 57            await next();
 58            return;
 59        }
 60
 61        var confirmation = SupplierOrderConfirmation.FromIds(ReadConfirmedIds(context.HttpContext.Request));
 62        var result = await _guard.EvaluateAsync(targets, confirmation, user, ct);
 63        if (!result.IsAllowed)
 64        {
 65            context.Result = new ObjectResult(new
 66            {
 67                success = false,
 68                requiresConfirmation = true,
 69                error = result.BlockingMessage(),
 70                conflicts = result.Blocking
 71            })
 72            { StatusCode = StatusCodes.Status409Conflict };
 73            return;
 74        }
 75
 76        // Намерение фиксируется ДО обращения к поставщику; если другой запрос успел раньше — заказ не отправляется.
 77        var start = await _guard.BeginDispatchAsync(Guid.NewGuid().ToString("N"), "SupplierCart", targets, result, user,
 78        if (!start.CanProceed)
 79        {
 80            context.Result = new ObjectResult(new
 81            {
 82                success = false,
 83                requiresConfirmation = false,
 84                error = start.Message,
 85                conflicts = start.Conflicts
 86            })
 87            { StatusCode = StatusCodes.Status409Conflict };
 88            return;
 89        }
 90
 91        var executed = await next();
 92
 93        // Исключение (в том числе обрыв запроса) — исход неизвестен: намерение остаётся Placing и блокирует повтор до с
 94        if (executed.Exception is null || executed.ExceptionHandled)
 95            await _guard.CompleteDispatchAsync(start.Dispatch!);
 96    }
 97
 98    /// <returns><c>null</c> — вся активная корзина; иначе выбранные строки (может быть пустым).</returns>
 99    private IReadOnlyCollection<string>? ResolveCartItemIds(ActionExecutingContext context)
 100    {
 101        foreach (var name in AllFlagArguments)
 102        {
 103            if (context.ActionArguments.TryGetValue(name, out var flag) && flag is true) return null;
 104        }
 105
 106        foreach (var name in IdArguments)
 107        {
 108            if (!context.ActionArguments.TryGetValue(name, out var ids)) continue;
 109            return ids switch
 110            {
 111                long[] numbers => numbers.Select(x => x.ToString(System.Globalization.CultureInfo.InvariantCulture)).ToA
 112                string[] texts => texts,
 113                null => _nullMeansAll ? null : [],
 114                _ => []
 115            };
 116        }
 117
 118        return _nullMeansAll ? null : [];
 119    }
 120
 121    private static IEnumerable<string> ReadConfirmedIds(HttpRequest request)
 122    {
 123        if (!request.HasFormContentType) return [];
 124        return request.Form[ConfirmedFieldName].SelectMany(x => (x ?? string.Empty).Split(',', StringSplitOptions.Remove
 125    }
 126}