< Summary

Information
Class: Services.PartnerServices.PartnerOrderScope
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/PartnerServices/PartnerOrderScope.cs
Line coverage
95%
Covered lines: 69
Uncovered lines: 3
Coverable lines: 72
Total lines: 160
Line coverage: 95.8%
Branch coverage
79%
Covered branches: 43
Total branches: 54
Branch coverage: 79.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Apply(...)100%22100%
BuildPredicate(...)100%88100%
BuildRuleBody(...)100%66100%
BuildCondition(...)68.75%161695.83%
IntIn(...)100%44100%
Period(...)70%1010100%
ParseDate(...)100%22100%
Upper(...)100%11100%
ParseValues(...)50%6450%
Rebind(...)100%11100%
.ctor(...)100%11100%
VisitParameter(...)50%22100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/PartnerServices/PartnerOrderScope.cs

#LineLine coverage
 1using System.Linq.Expressions;
 2using System.Globalization;
 3using System.Text.Json;
 4using OzonDomains;
 5using OzonDomains.Models;
 6using OzonDomains.Models.Partners;
 7
 8namespace Services.PartnerServices;
 9
 10/// <summary>
 11/// Превращает правила партнёра в фильтр заказов. Правила объединяются через OR, условия внутри правила
 12/// через AND, значения одного поля через OR. Нет активных правил — партнёр не видит ничего. Архивные заказы не видны ни
 13/// </summary>
 14public static class PartnerOrderScope
 15{
 16    public static IQueryable<Order> Apply(IQueryable<Order> orders, IEnumerable<PartnerRule> rules)
 17    {
 4018        var predicate = BuildPredicate(rules);
 4419        if (predicate is null) return orders.Where(_ => false);
 20
 21        // Архивные заказы скрыты от партнёров всегда: архив значит «убран из работы», как и в основном списке заказов.
 3622        return orders.Where(o => o.OrderArchive == null).Where(predicate);
 23    }
 24
 25    public static Expression<Func<Order, bool>>? BuildPredicate(IEnumerable<PartnerRule> rules)
 26    {
 4027        var parameter = Expression.Parameter(typeof(Order), "o");
 4028        Expression? any = null;
 29
 19830        foreach (var rule in rules.Where(r => r.IsActive))
 31        {
 3932            var ruleBody = BuildRuleBody(rule, parameter);
 33            // Правило, из которого не удалось построить ни одного условия, пропускаем:
 34            // оно бы открыло все заказы. Валидация сохранения такие правила не пропускает, это вторая линия защиты.
 3935            if (ruleBody is null) continue;
 3936            any = any is null ? ruleBody : Expression.OrElse(any, ruleBody);
 37        }
 38
 4039        return any is null ? null : Expression.Lambda<Func<Order, bool>>(any, parameter);
 40    }
 41
 42    private static Expression? BuildRuleBody(PartnerRule rule, ParameterExpression o)
 43    {
 3944        Expression? all = null;
 15845        foreach (var condition in rule.Conditions)
 46        {
 4047            var body = BuildCondition(condition, o);
 48            // Условие с непонятными значениями превращается в «ничего не подходит», а не в «подходит всё».
 4049            body ??= Expression.Constant(false);
 4050            all = all is null ? body : Expression.AndAlso(all, body);
 51        }
 52
 3953        return all;
 54    }
 55
 56    private static Expression? BuildCondition(PartnerRuleCondition condition, ParameterExpression o)
 57    {
 4058        var values = ParseValues(condition.ValuesJson);
 4059        switch (condition.Field)
 60        {
 61            case PartnerRuleField.Status:
 162                return IntIn(o, nameof(Order.AppStatusId), values);
 63            case PartnerRuleField.Warehouse:
 964                return IntIn(o, nameof(Order.ShipmentWarehouseId), values);
 65            case PartnerRuleField.Client:
 166                return IntIn(o, nameof(Order.OzonClientId), values);
 67            case PartnerRuleField.Marketplace:
 68            {
 169                var types = values
 170                    .Select(v => Enum.TryParse<ClientType>(v, true, out var t) ? (ClientType?)t : null)
 371                    .Where(t => t is not null).Select(t => t!.Value).Distinct().ToList();
 172                if (types.Count == 0) return null;
 173                Expression<Func<Order, bool>> e = x => x.OzonClient != null && types.Contains(x.OzonClient.ClientType);
 174                return Rebind(e, o);
 75            }
 76            case PartnerRuleField.DatePeriod:
 1477                return Period(values, o);
 78            case PartnerRuleField.Article:
 79            {
 1380                var articles = Upper(values);
 1381                if (articles.Count == 0) return null;
 82                // Владелец может ввести артикул и с кодом бренда (122RU19=HSH), и без него (122RU19).
 1383                Expression<Func<Order, bool>> e = x =>
 1384                    (x.Article != null && articles.Contains(x.Article.ToUpper())) ||
 1385                    (x.ProductKey != null && articles.Contains(x.ProductKey.ToUpper()));
 1386                return Rebind(e, o);
 87            }
 88            case PartnerRuleField.Brand:
 89            {
 190                var brands = Upper(values);
 191                if (brands.Count == 0) return null;
 192                Expression<Func<Order, bool>> e =
 193                    x => x.Manufacturer != null && x.Manufacturer.Name != null && brands.Contains(x.Manufacturer.Name.To
 194                return Rebind(e, o);
 95            }
 96            default:
 097                return null;
 98        }
 99    }
 100
 101    private static Expression? IntIn(ParameterExpression o, string property, List<string> values)
 102    {
 11103        var ids = values
 12104            .Select(v => int.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? (int?)i : null
 34105            .Where(i => i is not null).Select(i => i!.Value).Distinct().ToList();
 12106        if (ids.Count == 0) return null;
 107
 10108        var member = Expression.Property(o, property); // int?
 10109        var hasValue = Expression.Property(member, nameof(Nullable<int>.HasValue));
 10110        var value = Expression.Property(member, nameof(Nullable<int>.Value));
 10111        var contains = Expression.Call(
 10112            typeof(Enumerable), nameof(Enumerable.Contains), [typeof(int)],
 10113            Expression.Constant(ids), value);
 10114        return Expression.AndAlso(hasValue, contains);
 115    }
 116
 117    private static Expression? Period(List<string> values, ParameterExpression o)
 118    {
 14119        if (values.Count != 2) return null;
 14120        var from = ParseDate(values[0]);
 14121        var to = ParseDate(values[1]);
 14122        if (from is null && to is null) return null;
 123
 14124        Expression<Func<Order, bool>> e = x => x.ProcessingDate != null;
 14125        Expression body = Rebind(e, o);
 14126        var date = Expression.Property(o, nameof(Order.ProcessingDate));
 14127        if (from is { } f)
 14128            body = Expression.AndAlso(body, Expression.GreaterThanOrEqual(date, Expression.Constant((DateTime?)f, typeof
 14129        if (to is { } t)
 130            // «по» включает весь последний день.
 1131            body = Expression.AndAlso(body, Expression.LessThan(date, Expression.Constant((DateTime?)t.AddDays(1), typeo
 14132        return body;
 133    }
 134
 135    private static DateTime? ParseDate(string value) =>
 28136        DateTime.TryParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var d) ? d : 
 137
 138    private static List<string> Upper(List<string> values) =>
 52139        values.Select(v => v.Trim().ToUpperInvariant()).Where(v => v.Length > 0).Distinct().ToList();
 140
 141    private static List<string> ParseValues(string json)
 142    {
 143        try
 144        {
 100145            return JsonSerializer.Deserialize<List<string>>(json)?.Select(v => (v ?? string.Empty).Trim()).ToList() ?? n
 146        }
 0147        catch (JsonException)
 148        {
 0149            return new();
 150        }
 40151    }
 152
 153    private static Expression Rebind(Expression<Func<Order, bool>> lambda, ParameterExpression target) =>
 29154        new ParameterReplacer(lambda.Parameters[0], target).Visit(lambda.Body)!;
 155
 29156    private sealed class ParameterReplacer(ParameterExpression from, ParameterExpression to) : ExpressionVisitor
 157    {
 71158        protected override Expression VisitParameter(ParameterExpression node) => node == from ? to : base.VisitParamete
 159    }
 160}