< Summary

Information
Class: OzonOrdersWeb.Areas.Admin.Services.DataScopeFilterSummaryService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Admin/Services/DataScopeFilterSummaryService.cs
Line coverage
98%
Covered lines: 218
Uncovered lines: 3
Coverable lines: 221
Total lines: 388
Line coverage: 98.6%
Branch coverage
86%
Covered branches: 155
Total branches: 180
Branch coverage: 86.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(...)100%11100%
PopulateFilterSummariesAsync()97.5%404097.5%
FormatRange(...)87.5%8883.33%
FormatNum(...)100%22100%
FormatInList(...)92.85%1414100%
LoadNamesAsync()64.58%4848100%
IsLookupField(...)100%11100%
IsIntegerValueField(...)100%22100%
GetLookupFieldLabel(...)50%22100%
GetValueFieldLabel(...)100%22100%
TryParseOrderExpression(...)100%1414100%
TryParseRange(...)90%2020100%
TryParseInList(...)83.33%66100%
TryParseEquality(...)75%44100%
UnwrapParens(...)100%1818100%
.ctor(...)100%11100%
get_Field()100%11100%
get_Min()100%11100%
get_Max()100%11100%
.ctor(...)100%11100%
get_Groups()100%11100%
.cctor()100%11100%
.ctor()100%11100%
.cctor()100%11100%
.ctor()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Admin/Services/DataScopeFilterSummaryService.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Text.RegularExpressions;
 3using Microsoft.EntityFrameworkCore;
 4using OzonOrdersWeb.Areas.Admin.Models;
 5using OzonRepositories.Context;
 6
 7namespace OzonOrdersWeb.Areas.Admin.Services;
 8
 9/// <summary>
 10/// Строит человекочитаемую расшифровку Dynamic LINQ выражений DataScope (Order: FK → имена, числовые диапазоны).
 11/// </summary>
 12public sealed class DataScopeFilterSummaryService
 13{
 114    private static readonly TimeSpan RxTimeout = TimeSpan.FromSeconds(1);
 15
 116    private static readonly Regex RangeFull = new(
 117        @"^\s*(\w+)\s*!=\s*null\s*&&\s*\1\s*>=\s*([-0-9.]+)\s*&&\s*\1\s*<=\s*([-0-9.]+)\s*$",
 118        RegexOptions.CultureInvariant, RxTimeout);
 19
 120    private static readonly Regex RangeMinOnly = new(
 121        @"^\s*(\w+)\s*!=\s*null\s*&&\s*\1\s*>=\s*([-0-9.]+)\s*$",
 122        RegexOptions.CultureInvariant, RxTimeout);
 23
 124    private static readonly Regex RangeMaxOnly = new(
 125        @"^\s*(\w+)\s*!=\s*null\s*&&\s*\1\s*<=\s*([-0-9.]+)\s*$",
 126        RegexOptions.CultureInvariant, RxTimeout);
 27
 128    private static readonly Regex IntEq = new(
 129        @"^\s*(\w+)\s*==\s*(\d+)\s*$",
 130        RegexOptions.CultureInvariant, RxTimeout);
 31
 32    private readonly ApplicationDbContext _db;
 33
 12034    public DataScopeFilterSummaryService(ApplicationDbContext db) => _db = db;
 35
 36    public async Task PopulateFilterSummariesAsync(IList<DataScopeListItemVm> items, CancellationToken ct)
 37    {
 6138        if (items.Count == 0) return;
 39
 5940        var orderParseByItem = new Dictionary<DataScopeListItemVm, OrderParse?>();
 5941        var idsByField = new Dictionary<string, HashSet<int>>(StringComparer.OrdinalIgnoreCase);
 42
 27243        foreach (var s in items)
 44        {
 7745            if (string.IsNullOrWhiteSpace(s.FilterExpression))
 46            {
 547                s.FilterSummary = "—";
 548                continue;
 49            }
 50
 7251            if (!string.Equals(s.EntityName, "Order", StringComparison.OrdinalIgnoreCase))
 52            {
 353                s.FilterSummary = s.FilterExpression.Length > 200
 354                    ? s.FilterExpression[..200] + "…"
 355                    : s.FilterExpression;
 356                continue;
 57            }
 58
 6959            var parsed = TryParseOrderExpression(s.FilterExpression);
 6960            orderParseByItem[s] = parsed;
 61
 6962            if (parsed is OrderParse.InList inList)
 63            {
 10664                foreach (var g in inList.Groups)
 65                {
 2766                    if (!IsLookupField(g.Field)) continue;
 2667                    if (!idsByField.TryGetValue(g.Field, out var set))
 68                    {
 2569                        set = new HashSet<int>();
 2570                        idsByField[g.Field] = set;
 71                    }
 72
 14573                    foreach (var id in g.Ids) set.Add(id);
 74                }
 75            }
 76        }
 77
 5978        var nameByField = new Dictionary<string, IReadOnlyDictionary<int, string>>(StringComparer.OrdinalIgnoreCase);
 16879        foreach (var (field, ids) in idsByField)
 80        {
 2581            if (ids.Count == 0) continue;
 2582            nameByField[field] = await LoadNamesAsync(field, ids, ct);
 83        }
 84
 27285        foreach (var s in items)
 86        {
 7787            if (s.FilterSummary is not null) continue;
 6788            if (!orderParseByItem.TryGetValue(s, out var parsed) || parsed is null)
 89            {
 990                s.FilterSummary = "Составное/произвольное выражение (см. Dynamic LINQ)";
 991                continue;
 92            }
 93
 5894            if (ReferenceEquals(parsed, OrderParse.LiteralFalse.Value))
 95            {
 1096                s.FilterSummary = "Пусто (всегда false)";
 97            }
 4898            else if (parsed is OrderParse.Range r)
 99            {
 16100                s.FilterSummary = FormatRange(r);
 101            }
 32102            else if (parsed is OrderParse.InList list)
 103            {
 25104                s.FilterSummary = FormatInList(list, nameByField);
 105            }
 7106            else if (ReferenceEquals(parsed, OrderParse.Complex.Value))
 107            {
 7108                s.FilterSummary = "Составное/произвольное выражение (см. Dynamic LINQ)";
 109            }
 110            else
 111            {
 0112                s.FilterSummary = "—";
 113            }
 114        }
 60115    }
 116
 117    private static string FormatRange(OrderParse.Range r)
 118    {
 16119        var label = GetValueFieldLabel(r.Field);
 16120        if (r.Min.HasValue && r.Max.HasValue)
 3121            return $"{label}: от {FormatNum(r.Field, r.Min.Value)} до {FormatNum(r.Field, r.Max.Value)}";
 20122        if (r.Min.HasValue) return $"{label}: от {FormatNum(r.Field, r.Min.Value)}";
 12123        if (r.Max.HasValue) return $"{label}: до {FormatNum(r.Field, r.Max.Value)}";
 0124        return $"{label}: (пусто)";
 125    }
 126
 127    private static string FormatNum(string field, decimal v)
 128    {
 23129        if (IsIntegerValueField(field)) return ((long)Math.Truncate(v)).ToString(CultureInfo.InvariantCulture);
 15130        return v.ToString(CultureInfo.InvariantCulture);
 131    }
 132
 133    private static string FormatInList(
 134        OrderParse.InList list,
 135        IReadOnlyDictionary<string, IReadOnlyDictionary<int, string>> nameByField)
 136    {
 25137        var parts = new List<string>();
 102138        foreach (var g in list.Groups)
 139        {
 26140            var field = g.Field;
 26141            var label = IsLookupField(field) ? GetLookupFieldLabel(field) : field;
 26142            if (nameByField.TryGetValue(field, out var map))
 143            {
 25144                var names = g.Ids
 30145                    .Select(id => map.TryGetValue(id, out var n) && !string.IsNullOrWhiteSpace(n) ? n! : $"#{id}")
 25146                    .ToList();
 25147                parts.Add($"{label}: {string.Join(", ", names)}");
 148            }
 149            else
 150            {
 1151                parts.Add(
 3152                    $"{label}: {string.Join(", ", g.Ids.Select(i => i.ToString(CultureInfo.InvariantCulture)))}");
 153            }
 154        }
 155
 25156        if (parts.Count == 0) return "—";
 49157        if (parts.Count == 1) return parts[0];
 1158        return string.Join(" или ", parts);
 159    }
 160
 161    private async Task<IReadOnlyDictionary<int, string>> LoadNamesAsync(string field, HashSet<int> ids, CancellationToke
 162    {
 25163        var idArr = ids.ToArray();
 25164        return field switch
 25165        {
 13166            "SupplierId" => await _db.Suppliers.AsNoTracking()
 13167                .Where(x => idArr.Contains(x.Id))
 45168                .ToDictionaryAsync(x => x.Id, x => x.Name ?? $"#{x.Id}", ct),
 3169            "ClientStatusId" => await _db.ClientStatuses.AsNoTracking()
 3170                .Where(x => idArr.Contains(x.Id))
 9171                .ToDictionaryAsync(x => x.Id, x => x.Name ?? $"#{x.Id}", ct),
 1172            "AppStatusId" => await _db.AppStatuses.AsNoTracking()
 1173                .Where(x => idArr.Contains(x.Id))
 3174                .ToDictionaryAsync(x => x.Id, x => x.Name ?? $"#{x.Id}", ct),
 2175            "OzonClientId" => await _db.OzonClients.AsNoTracking()
 2176                .Where(x => idArr.Contains(x.Id))
 6177                .ToDictionaryAsync(x => x.Id, x => x.Name, ct),
 1178            "ManufacturerId" => await _db.Manufacturers.AsNoTracking()
 1179                .Where(x => idArr.Contains(x.Id))
 3180                .ToDictionaryAsync(x => x.Id, x => x.Name ?? $"#{x.Id}", ct),
 1181            "ShipmentWarehouseId" => await _db.Warehouses.AsNoTracking()
 1182                .Where(x => idArr.Contains(x.Id))
 3183                .ToDictionaryAsync(x => x.Id, x => x.Name ?? $"#{x.Id}", ct),
 1184            "DeliveryId" => await _db.Deliveries.AsNoTracking()
 1185                .Where(x => idArr.Contains(x.Id))
 3186                .ToDictionaryAsync(x => x.Id, x => x.Name ?? $"#{x.Id}", ct),
 1187            "EtProducerId" => await _db.EtProducers.AsNoTracking()
 1188                .Where(x => idArr.Contains(x.Id))
 3189                .ToDictionaryAsync(x => x.Id, x => x.Name, ct),
 2190            "СurrencyId" or "CurrencyId" => await _db.Currencys.AsNoTracking()
 2191                .Where(x => idArr.Contains(x.Id))
 6192                .ToDictionaryAsync(x => x.Id, x => x.Name ?? $"#{x.Id}", ct),
 0193            _ => idArr.ToDictionary(id => id, id => $"#{id}")
 25194        };
 25195    }
 196
 53197    private static bool IsLookupField(string field) => LookupFieldLabels.ContainsKey(field);
 198
 199    private static bool IsIntegerValueField(string field) =>
 19200        string.Equals(field, "Quantity", StringComparison.OrdinalIgnoreCase)
 19201        || string.Equals(field, "TrueQuantity", StringComparison.OrdinalIgnoreCase);
 202
 1203    private static readonly Dictionary<string, string> LookupFieldLabels = new(StringComparer.OrdinalIgnoreCase)
 1204    {
 1205        ["SupplierId"] = "Поставщик",
 1206        ["ClientStatusId"] = "Статус клиента",
 1207        ["AppStatusId"] = "Статус в отчёте",
 1208        ["OzonClientId"] = "Клиент Ozon",
 1209        ["ManufacturerId"] = "Производитель",
 1210        ["ShipmentWarehouseId"] = "Склад отгрузки",
 1211        ["DeliveryId"] = "Доставка",
 1212        ["EtProducerId"] = "Производитель JCEtalon",
 1213        ["СurrencyId"] = "Валюта",
 1214        ["CurrencyId"] = "Валюта"
 1215    };
 216
 217    private static string GetLookupFieldLabel(string field) =>
 25218        LookupFieldLabels.TryGetValue(field, out var l) ? l : field;
 219
 1220    private static readonly Dictionary<string, string> ValueFieldLabels = new(StringComparer.OrdinalIgnoreCase)
 1221    {
 1222        ["Price"] = "Цена",
 1223        ["Quantity"] = "Количество",
 1224        ["TrueQuantity"] = "TrueQuantity",
 1225        ["ShipmentAmount"] = "Сумма отправления",
 1226        ["PurchasePrice"] = "Закупочная цена",
 1227        ["OriginalPurchasePrice"] = "Исх. закупочная",
 1228        ["CostPrice"] = "Себестоимость",
 1229        ["MinProfit"] = "MinProfit",
 1230        ["MaxProfit"] = "MaxProfit",
 1231        ["MinOzonCommission"] = "Min комиссия Ozon",
 1232        ["MaxOzonCommission"] = "Max комиссия Ozon",
 1233        ["MinDiscount"] = "Min скидка",
 1234        ["MaxDiscount"] = "Max скидка"
 1235    };
 236
 237    private static string GetValueFieldLabel(string field) =>
 16238        ValueFieldLabels.TryGetValue(field, out var l) ? l : field;
 239
 240    private static OrderParse? TryParseOrderExpression(string expr)
 241    {
 69242        var s = expr.Trim();
 69243        if (s.Equals("false", StringComparison.OrdinalIgnoreCase) || s.Equals("1 == 0", StringComparison.Ordinal))
 10244            return OrderParse.LiteralFalse.Value;
 245
 59246        if (s.Contains("&&", StringComparison.Ordinal))
 247        {
 41248            if (TryParseRange(s) is { } range) return range;
 14249            if (s.Contains("||", StringComparison.Ordinal)) return OrderParse.Complex.Value;
 250            return OrderParse.Complex.Value;
 251        }
 252
 35253        s = UnwrapParens(s);
 38254        if (string.IsNullOrEmpty(s)) return null;
 40255        if (s.Contains("||", StringComparison.Ordinal)) return TryParseInList(s);
 24256        if (TryParseEquality(s) is { } t)
 20257            return new OrderParse.InList(
 20258                new[] { (t.Field, (IReadOnlyList<int>)new List<int> { t.Id }) });
 259
 4260        return null;
 261    }
 262
 263    private static OrderParse.Range? TryParseRange(string s)
 264    {
 24265        var t = s.Trim();
 25266        if (t.Length >= 2 && t[0] == '(' && t[^1] == ')') t = t[1..^1].Trim();
 267
 24268        var m = RangeFull.Match(t);
 24269        if (m.Success)
 270        {
 5271            var field = m.Groups[1].Value;
 6272            if (!decimal.TryParse(m.Groups[2].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var min)) retur
 5273            if (!decimal.TryParse(m.Groups[3].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var max)) retur
 3274            return new OrderParse.Range(field, min, max);
 275        }
 276
 19277        m = RangeMinOnly.Match(t);
 19278        if (m.Success)
 279        {
 8280            var field = m.Groups[1].Value;
 8281            if (!decimal.TryParse(m.Groups[2].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var min)) retur
 8282            return new OrderParse.Range(field, min, null);
 283        }
 284
 11285        m = RangeMaxOnly.Match(t);
 11286        if (m.Success)
 287        {
 6288            var field = m.Groups[1].Value;
 6289            if (!decimal.TryParse(m.Groups[2].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var max)) retur
 6290            return new OrderParse.Range(field, null, max);
 291        }
 292
 5293        return null;
 294    }
 295
 296    private static OrderParse.InList? TryParseInList(string s)
 297    {
 8298        var parts = s.Split("||", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
 8299        var list = new List<(string Field, int Id)>();
 48300        foreach (var p in parts)
 301        {
 17302            var seg = p.Trim();
 19303            if (TryParseEquality(seg) is not { } eq) return null;
 15304            list.Add((eq.Field, eq.Id));
 305        }
 306
 6307        if (list.Count == 0) return null;
 6308        var groups = list
 14309            .GroupBy(x => x.Field, StringComparer.OrdinalIgnoreCase)
 34310            .Select(g => (g.Key, (IReadOnlyList<int>)g.Select(x => x.Id).Distinct().OrderBy(x => x).ToList()))
 6311            .ToList();
 6312        return new OrderParse.InList(groups);
 313    }
 314
 315    private static (string Field, int Id)? TryParseEquality(string seg)
 316    {
 41317        var m = IntEq.Match(seg.Trim());
 47318        if (!m.Success) return null;
 35319        var field = m.Groups[1].Value;
 35320        if (!int.TryParse(m.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id)) return nul
 35321        return (field, id);
 322    }
 323
 324    private static string UnwrapParens(string s)
 325    {
 35326        s = s.Trim();
 46327        while (s.Length >= 2 && s[0] == '(' && s[^1] == ')')
 328        {
 13329            var inner = s.AsSpan(1, s.Length - 2);
 13330            var depth = 0;
 13331            var ok = true;
 364332            for (var i = 0; i < inner.Length; i++)
 333            {
 176334                if (inner[i] == '(') depth++;
 164335                else if (inner[i] == ')')
 336                {
 6337                    depth--;
 6338                    if (depth < 0)
 339                    {
 1340                        ok = false;
 1341                        break;
 342                    }
 343                }
 344            }
 345
 13346            if (!ok || depth != 0) break;
 11347            s = inner.ToString().Trim();
 348        }
 349
 35350        return s;
 351    }
 352
 353    private abstract class OrderParse
 354    {
 355        public sealed class Range : OrderParse
 356        {
 17357            public Range(string field, decimal? min, decimal? max)
 358            {
 17359                Field = field;
 17360                Min = min;
 17361                Max = max;
 17362            }
 363
 35364            public string Field { get; }
 39365            public decimal? Min { get; }
 25366            public decimal? Max { get; }
 367        }
 368
 369        public sealed class InList : OrderParse
 370        {
 52371            public InList(IReadOnlyList<(string Field, IReadOnlyList<int> Ids)> groups) => Groups = groups;
 372
 51373            public IReadOnlyList<(string Field, IReadOnlyList<int> Ids)> Groups { get; }
 374        }
 375
 376        public sealed class Complex : OrderParse
 377        {
 1378            public static readonly Complex Value = new();
 2379            private Complex() { }
 380        }
 381
 382        public sealed class LiteralFalse : OrderParse
 383        {
 1384            public static readonly LiteralFalse Value = new();
 2385            private LiteralFalse() { }
 386        }
 387    }
 388}