< Summary

Line coverage
90%
Covered lines: 248
Uncovered lines: 25
Coverable lines: 273
Total lines: 2517
Line coverage: 90.8%
Branch coverage
66%
Covered branches: 137
Total branches: 206
Branch coverage: 66.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: TryParseRegex(...)60.9%11011097%
File 1: IsReadOnlyOrderSearchIntent(...)100%22100%
File 1: LooksLikeOrdersFilterQuery(...)0%7280%
File 1: ContainsOrderSearchWord(...)87.5%8885.71%
File 1: NormalizeCriteria(...)100%1212100%
File 1: IsCancelledAppStatus(...)100%22100%
File 1: TrimWarehouseName(...)87.5%88100%
File 1: SplitWarehouseAndOzonClient(...)100%1010100%
File 1: TrimSupplierName(...)87.5%88100%
File 1: IsOrderSearchMessage(...)50%22100%
File 1: IsGenericFindOrders(...)100%210%
File 1: IsEmptyPriceIntent(...)50%66100%
File 1: TryParseMinQuantity(...)62.5%9880%
File 1: TryParseDecimal(...)25%13416.66%
File 1: TryParseLastMonths(...)66.66%141277.77%
File 1: CleanCaptured(...)50%22100%
File 1: ContainsAny(...)100%44100%
File 2: MinQuantityRegex()100%11100%
File 2: MinQuantityPiecesRegex()100%11100%
File 2: TransactionUserRegex()100%11100%
File 2: OzonClientRegex()100%11100%
File 2: SupplierRegex()100%11100%
File 2: ChangedByUserRegex()100%11100%
File 2: CoStatusRegex()100%11100%
File 2: AppStatusRegex()100%11100%
File 2: ClientStatusRegex()100%11100%
File 2: ArticleRegex()100%11100%
File 2: ProductNameRegex()100%11100%
File 2: ManufacturerRegex()100%11100%
File 2: CategoryRegex()100%11100%
File 2: NaKlienteRegex()100%11100%
File 2: PoSkladuRegex()100%11100%
File 2: WarehouseRegex()100%11100%
File 2: DeliveryProviderRegex()100%11100%
File 2: DeliveryCityRegex()100%11100%
File 2: DeliveryNameRegex()100%11100%
File 2: PriceMinRegex()100%11100%
File 2: PriceMaxRegex()100%11100%
File 2: ProfitMinRegex()100%11100%
File 2: ProfitMaxRegex()100%11100%
File 2: PurchasePriceMaxRegex()100%11100%
File 2: LastMonthsRegex()100%11100%
File 2: LastOneMonthRegex()100%11100%
File 2: ZaMonthRegex()100%11100%
File 2: LastWeeksRegex()100%11100%
File 2: FieldChangedNameRegex()100%11100%
File 2: FieldChangedToValueRegex()100%11100%
File 2: TemporalSuffixRegex()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/AiAssistant/AiOrderFilterParser.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2
 3namespace Services.AiAssistant;
 4
 5public static partial class AiOrderFilterParser
 6{
 7    // ── Количество ────────────────────────────────────────────────────────────
 8    [GeneratedRegex(
 9        @"(?:количеств\w*|qty)\s*(?:>|больше|более|от)\s*(\d+)",
 10        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 11    private static partial Regex MinQuantityRegex();
 12
 13    [GeneratedRegex(
 14        @"(\d+)\s*шт\.?\s*(?:и\s*)?(?:больше|более|>|от)",
 15        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 16    private static partial Regex MinQuantityPiecesRegex();
 17
 18    // ── Участники ─────────────────────────────────────────────────────────────
 19    [GeneratedRegex(
 20        @"(?:провел\w*|провёл|провела|проводил\w*|оформил\w*|создал\w*)\s+(?:пользовател\w+\s+)?[«""]?([^»""\n,;.]+)[»""
 21        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 22    private static partial Regex TransactionUserRegex();
 23
 24    [GeneratedRegex(
 25        @"(?:ozon[-\s]?клиент\w*|клиент\w*\s+ozon)\s+[«""]?([^»""\n,;.]+)[»""]?",
 26        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 27    private static partial Regex OzonClientRegex();
 28
 29    [GeneratedRegex(
 30        @"поставщик\w*\s+[«""]?([^»""\n,;.]+?)(?=\s+со\s+статус|\s+(?:внутренн\w+\s+)?статус|\s+склад|\s+артикул|\s+това
 31        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 32    private static partial Regex SupplierRegex();
 33
 34    [GeneratedRegex(
 35        @"(?:изменил\w*|менял\w*|редактировал\w*|менял\w*)\s+(?:пользовател\w+\s+)?[«""]?([^»""\n,;.]{2,50})[»""]?",
 36        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 37    private static partial Regex ChangedByUserRegex();
 38
 39    // ── Статусы ───────────────────────────────────────────────────────────────
 40    [GeneratedRegex(
 41        @"со\s+статусом\s+[«""]?([^»""\n,;.]+)[»""]?",
 42        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 43    private static partial Regex CoStatusRegex();
 44
 45    [GeneratedRegex(
 46        @"(?:внутренн\w+\s+)?статус\w*\s+[«""]?([^»""\n,;.]+)[»""]?",
 47        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 48    private static partial Regex AppStatusRegex();
 49
 50    [GeneratedRegex(
 51        @"(?:(?:клиентский|клиент)\s+)?статус\s+(?:ozon|озон)\s+[«""]?([^»""\n,;.]+)[»""]?|(?:статус\s+ozon|статус\s+озо
 52        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 53    private static partial Regex ClientStatusRegex();
 54
 55    // ── Товар ─────────────────────────────────────────────────────────────────
 56    [GeneratedRegex(
 57        @"(?:артикул\w*|артикл\w*)\s+[«""]?([^\s»""\n,;.]+)[»""]?",
 58        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 59    private static partial Regex ArticleRegex();
 60
 61    [GeneratedRegex(
 62        @"(?:товар\w*|наименован\w*|продукт\w*)\s+[«""]?([^»""\n,;.]+)[»""]?",
 63        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 64    private static partial Regex ProductNameRegex();
 65
 66    [GeneratedRegex(
 67        @"(?:производител\w*|бренд\w*)\s+[«""]?([^»""\n,;.]+)[»""]?",
 68        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 69    private static partial Regex ManufacturerRegex();
 70
 71    [GeneratedRegex(
 72        @"(?:категори\w*)\s+[«""]?([^»""\n,;.]+)[»""]?",
 73        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 74    private static partial Regex CategoryRegex();
 75
 76    // ── Логистика ─────────────────────────────────────────────────────────────
 77    [GeneratedRegex(
 78        @"на\s+клиент\w*\s+[«""]?([^»""\n,;.]+?)(?=\s+за\s+|\s+со\s+статус|\s+котор|\s+минус|$|[»""])",
 79        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 80    private static partial Regex NaKlienteRegex();
 81
 82    [GeneratedRegex(
 83        @"по\s+склад\w*\s+[«""]?([^\s»""\n,;.]+)[»""]?",
 84        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 85    private static partial Regex PoSkladuRegex();
 86
 87    [GeneratedRegex(
 88        @"(?:склад\w*)\s+[«""]?([^»""\n,;.]+?)(?=\s+на\s+клиент|\s+у\s+клиент|\s+за\s+|\s+со\s+статус|\s+котор|\s+минус|
 89        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 90    private static partial Regex WarehouseRegex();
 91
 92    [GeneratedRegex(
 93        @"(?:провайдер\w*|служб\w+\s+доставки)\s+[«""]?([^»""\n,;.]+)[»""]?",
 94        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 95    private static partial Regex DeliveryProviderRegex();
 96
 97    [GeneratedRegex(
 98        @"(?:город\w*|доставк\w+\s+в|в\s+город\w*)\s+[«""]?([^»""\n,;.]+)[»""]?",
 99        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 100    private static partial Regex DeliveryCityRegex();
 101
 102    [GeneratedRegex(
 103        @"(?:тип\s+доставки|доставк\w+\s+типа?)\s+[«""]?([^»""\n,;.]+)[»""]?",
 104        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 105    private static partial Regex DeliveryNameRegex();
 106
 107    // ── Финансы ───────────────────────────────────────────────────────────────
 108    [GeneratedRegex(
 109        @"цен\w*\s+(?:от|>=|>|больше|более)\s*([\d\s]+)(?:\s*(?:₽|руб\w*|р\.))?",
 110        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 111    private static partial Regex PriceMinRegex();
 112
 113    [GeneratedRegex(
 114        @"цен\w*\s+(?:до|<=|<|меньше|менее)\s*([\d\s]+)(?:\s*(?:₽|руб\w*|р\.))?",
 115        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 116    private static partial Regex PriceMaxRegex();
 117
 118    [GeneratedRegex(
 119        @"прибыл\w*\s+(?:от|>=|>|больше|более)\s*([\d\s-]+)(?:\s*(?:₽|руб\w*|р\.))?",
 120        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 121    private static partial Regex ProfitMinRegex();
 122
 123    [GeneratedRegex(
 124        @"прибыл\w*\s+(?:до|<=|<|меньше|менее)\s*([\d\s-]+)(?:\s*(?:₽|руб\w*|р\.))?",
 125        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 126    private static partial Regex ProfitMaxRegex();
 127
 128    [GeneratedRegex(
 129        @"закупк\w*\s+(?:до|<=|<|меньше|менее|не\s+дороже)\s*([\d\s]+)(?:\s*(?:₽|руб\w*|р\.))?",
 130        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 131    private static partial Regex PurchasePriceMaxRegex();
 132
 133    // ── Даты ─────────────────────────────────────────────────────────────────
 134    [GeneratedRegex(
 135        @"за\s+последни\w+\s+(\d+)\s+месяц\w*",
 136        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 137    private static partial Regex LastMonthsRegex();
 138
 139    [GeneratedRegex(
 140        @"за\s+последний\s+месяц",
 141        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 142    private static partial Regex LastOneMonthRegex();
 143
 144    [GeneratedRegex(
 145        @"за\s+месяц(?:\b|$)",
 146        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 147    private static partial Regex ZaMonthRegex();
 148
 149    [GeneratedRegex(
 150        @"за\s+последни\w+\s+(\d+)\s+(?:нед\w*|week)",
 151        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 152    private static partial Regex LastWeeksRegex();
 153
 154    // ── История изменений ─────────────────────────────────────────────────────
 155    [GeneratedRegex(
 156        @"(?:поле|поменял\w*|изменил\w*|сменил\w*)\s+[«""]?([^»""\n,;.]+)[»""]?\s+(?:на|стал\w*|изменил\w*\s+на)",
 157        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 158    private static partial Regex FieldChangedNameRegex();
 159
 160    [GeneratedRegex(
 161        @"(?:стал\w*|новое\s+значение|изменил\w*\s+на|поменял\w*\s+на)\s+[«""]?([^»""\n,;.]+)[»""]?",
 162        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 163    private static partial Regex FieldChangedToValueRegex();
 164
 165    // ═══════════════════════════════════════════════════════════════════════════
 166
 167    /// <summary>Regex-fallback парсер (используется если LLM недоступен или не распознал запрос).</summary>
 168    public static bool TryParseRegex(string message, out AiOrderFilterCriteria criteria)
 169    {
 4170        criteria = null!;
 4171        var trimmed = message.Trim();
 4172        var lower   = trimmed.ToLowerInvariant();
 173
 4174        if (!IsOrderSearchMessage(lower))
 0175            return false;
 176
 4177        var actionable = ContainsAny(lower,
 4178            "актуальн", "обработать", "к обработке", "требуют обработки",
 4179            "нужно обработать", "нужно взять", "в работе", "требуют внимания");
 180
 4181        var overdue          = ContainsAny(lower, "просроч");
 4182        var emptyPrice       = IsEmptyPriceIntent(lower);
 4183        var includeArchived  = ContainsAny(lower, "в архиве", "включая архив", "из архива", "с архивными");
 4184        var onlyNegProfit    = ContainsAny(lower, "убыточн", "отрицательн", "прибыль < 0", "минус", "минусят", "минусуют
 4185        var onlyWithReturns  = ContainsAny(lower, "с возврат", "только возврат", "есть возврат");
 4186        var onlyWithTracking = ContainsAny(lower, "с трек", "с трекинг", "с трек-номером", "с номером отслеживания");
 187
 188        // ── числа ──────────────────────────────────────────────────────────────
 4189        var minQty         = TryParseMinQuantity(lower);
 4190        var priceMin       = TryParseDecimal(PriceMinRegex().Match(lower));
 4191        var priceMax       = TryParseDecimal(PriceMaxRegex().Match(lower));
 4192        var profitMin      = TryParseDecimal(ProfitMinRegex().Match(lower));
 4193        var profitMax      = TryParseDecimal(ProfitMaxRegex().Match(lower));
 4194        var purchPriceMax  = TryParseDecimal(PurchasePriceMaxRegex().Match(lower));
 195
 196        // ── даты ───────────────────────────────────────────────────────────────
 4197        var lastMonths = TryParseLastMonths(lower);
 198
 199        // ── строковые поля ────────────────────────────────────────────────────
 4200        var txUser         = TransactionUserRegex().Match(trimmed);
 4201        var changedBy      = ChangedByUserRegex().Match(trimmed);
 4202        var ozonClient     = OzonClientRegex().Match(trimmed);
 4203        var naKliente      = NaKlienteRegex().Match(trimmed);
 4204        var supplier       = SupplierRegex().Match(trimmed);
 4205        var coStatus       = CoStatusRegex().Match(trimmed);
 4206        var appStatus      = AppStatusRegex().Match(trimmed);
 4207        var clientStatus   = ClientStatusRegex().Match(trimmed);
 4208        var article        = ArticleRegex().Match(trimmed);
 4209        var productName    = ProductNameRegex().Match(trimmed);
 4210        var manufacturer   = ManufacturerRegex().Match(trimmed);
 4211        var category       = CategoryRegex().Match(trimmed);
 4212        var warehouse      = PoSkladuRegex().Match(trimmed);
 4213        if (!warehouse.Success)
 1214            warehouse = WarehouseRegex().Match(trimmed);
 4215        var provider       = DeliveryProviderRegex().Match(trimmed);
 4216        var city           = DeliveryCityRegex().Match(trimmed);
 4217        var deliveryType   = DeliveryNameRegex().Match(trimmed);
 4218        var fieldName      = FieldChangedNameRegex().Match(trimmed);
 4219        var fieldValue     = FieldChangedToValueRegex().Match(trimmed);
 220
 4221        var hasNamedFilter = txUser.Success || changedBy.Success || ozonClient.Success || naKliente.Success
 4222            || supplier.Success || coStatus.Success || appStatus.Success || clientStatus.Success || article.Success || p
 4223            || manufacturer.Success || category.Success || warehouse.Success || provider.Success
 4224            || city.Success || deliveryType.Success || fieldName.Success || fieldValue.Success
 4225            || minQty.HasValue || emptyPrice || overdue || onlyNegProfit || onlyWithReturns
 4226            || onlyWithTracking || priceMin.HasValue || priceMax.HasValue || profitMin.HasValue
 4227            || profitMax.HasValue || purchPriceMax.HasValue || lastMonths.HasValue || includeArchived;
 228
 4229        if (!actionable && !hasNamedFilter && !IsGenericFindOrders(lower))
 0230            return false;
 231
 4232        if (!actionable && !hasNamedFilter && IsGenericFindOrders(lower))
 0233            actionable = true;
 234
 4235        var supplierName  = supplier.Success ? TrimSupplierName(CleanCaptured(supplier.Groups[1].Value)) : null;
 4236        var warehouseName = warehouse.Success ? TrimWarehouseName(CleanCaptured(warehouse.Groups[1].Value)) : null;
 4237        var ozonClientName = naKliente.Success
 4238            ? CleanCaptured(naKliente.Groups[1].Value)
 4239            : ozonClient.Success ? CleanCaptured(ozonClient.Groups[1].Value) : null;
 4240        (warehouseName, ozonClientName) = SplitWarehouseAndOzonClient(warehouseName, ozonClientName);
 4241        var appStatusName = coStatus.Success
 4242            ? CleanCaptured(coStatus.Groups[1].Value)
 4243            : appStatus.Success ? CleanCaptured(appStatus.Groups[1].Value) : null;
 244
 4245        if (IsCancelledAppStatus(appStatusName))
 246        {
 1247            includeArchived = true;
 1248            actionable      = false;
 249        }
 250
 4251        criteria = new AiOrderFilterCriteria
 4252        {
 4253            ActionableOnly          = actionable,
 4254            OverdueOnly             = overdue,
 4255            EmptyPrice              = emptyPrice,
 4256            IncludeArchived         = includeArchived,
 4257            OnlyNegativeProfit      = onlyNegProfit ? true : null,
 4258            OnlyWithReturns         = onlyWithReturns ? true : null,
 4259            OnlyWithTrackingNumber  = onlyWithTracking ? true : null,
 4260
 4261            MinQuantity             = minQty,
 4262            PriceMin                = priceMin,
 4263            PriceMax                = priceMax,
 4264            ProfitMin               = profitMin,
 4265            ProfitMax               = profitMax,
 4266            PurchasePriceMax        = purchPriceMax,
 4267            LastMonths              = lastMonths,
 4268
 4269            TransactionConductedBy  = txUser.Success    ? CleanCaptured(txUser.Groups[1].Value)    : null,
 4270            ChangedByUser           = changedBy.Success  ? CleanCaptured(changedBy.Groups[1].Value) : null,
 4271            OzonClientName          = ozonClientName,
 4272            SupplierName            = supplierName,
 4273            AppStatusName           = appStatusName,
 4274            ClientStatusName        = clientStatus.Success ? CleanCaptured(clientStatus.Groups[1].Success ? clientStatus
 4275            Article                 = article.Success    ? CleanCaptured(article.Groups[1].Value)   : null,
 4276            ProductName             = productName.Success? CleanCaptured(productName.Groups[1].Value): null,
 4277            ManufacturerName        = manufacturer.Success ? CleanCaptured(manufacturer.Groups[1].Value) : null,
 4278            NewCategory             = category.Success   ? CleanCaptured(category.Groups[1].Value)  : null,
 4279            ShipmentWarehouseName   = warehouseName,
 4280            DeliveryProviderName    = provider.Success   ? CleanCaptured(provider.Groups[1].Value)  : null,
 4281            DeliveryCity            = city.Success       ? CleanCaptured(city.Groups[1].Value)      : null,
 4282            DeliveryName            = deliveryType.Success ? CleanCaptured(deliveryType.Groups[1].Value) : null,
 4283            FieldChangedName        = fieldName.Success  ? CleanCaptured(fieldName.Groups[1].Value) : null,
 4284            FieldChangedToValue     = fieldValue.Success ? CleanCaptured(fieldValue.Groups[1].Value): null,
 4285        };
 286
 4287        return criteria.HasCriteria;
 288    }
 289
 290    /// <summary>Запрос на просмотр/поиск заказов, не write-операция.</summary>
 291    public static bool IsReadOnlyOrderSearchIntent(string lower)
 292    {
 27293        if (!ContainsOrderSearchWord(lower))
 20294            return false;
 295
 7296        return ContainsAny(lower, "покажи", "показать", "найди", "найти", "список", "какие",
 7297            "выведи", "отбери", "подбери", "выбери", "ищи", "поиск", "отфильтр", "фильтр");
 298    }
 299
 300    /// <summary>Быстрая эвристика: запрос похож на orders_filter (без LLM).</summary>
 301    public static bool LooksLikeOrdersFilterQuery(string message)
 302    {
 0303        var lower = message.Trim().ToLowerInvariant();
 0304        if (!IsOrderSearchMessage(lower))
 0305            return false;
 306
 0307        if (TryParseRegex(message, out var criteria) && criteria.HasCriteria)
 0308            return true;
 309
 0310        if (!IsReadOnlyOrderSearchIntent(lower))
 0311            return false;
 312
 0313        return ContainsAny(lower,
 0314            "убыточ", "невыгод", "минус", "просроч", "поставщик", "supplier",
 0315            "склад", "клиент", "ozon", "статус", "артикул", "товар",
 0316            "за месяц", "за последн", "без цен", "возврат", "трек");
 317    }
 318
 319    /// <summary>«заказ» в поиске, но не «заказан поставщику» (тип транзакции).</summary>
 320    private static bool ContainsOrderSearchWord(string lower)
 321    {
 27322        if (lower.Contains("order"))
 0323            return true;
 27324        if (!lower.Contains("заказ"))
 17325            return false;
 10326        if (lower.Contains("заказан поставщик") || lower.Contains("заказан реализатор"))
 3327            return false;
 7328        return true;
 329    }
 330
 331    /// <summary>Нормализует критерии после LLM/regex (отменённые статусы, поставщик).</summary>
 332    public static AiOrderFilterCriteria NormalizeCriteria(AiOrderFilterCriteria criteria)
 333    {
 12334        var supplier = TrimSupplierName(criteria.SupplierName);
 12335        var (warehouse, ozonClient) = SplitWarehouseAndOzonClient(
 12336            TrimWarehouseName(criteria.ShipmentWarehouseName),
 12337            criteria.OzonClientName);
 12338        var includeArchived = criteria.IncludeArchived;
 12339        var actionable = criteria.ActionableOnly;
 340
 12341        if (IsCancelledAppStatus(criteria.AppStatusName))
 342        {
 1343            includeArchived = true;
 1344            actionable      = false;
 345        }
 346
 12347        if (supplier == criteria.SupplierName
 12348            && warehouse == criteria.ShipmentWarehouseName
 12349            && ozonClient == criteria.OzonClientName
 12350            && includeArchived == criteria.IncludeArchived
 12351            && actionable == criteria.ActionableOnly)
 9352            return criteria;
 353
 3354        return new AiOrderFilterCriteria
 3355        {
 3356            ActionableOnly         = actionable,
 3357            OverdueOnly            = criteria.OverdueOnly,
 3358            EmptyPrice             = criteria.EmptyPrice,
 3359            IncludeArchived        = includeArchived,
 3360            OzonClientName         = ozonClient,
 3361            SupplierName           = supplier,
 3362            TransactionConductedBy = criteria.TransactionConductedBy,
 3363            ChangedByUser          = criteria.ChangedByUser,
 3364            AppStatusName          = criteria.AppStatusName,
 3365            ClientStatusName       = criteria.ClientStatusName,
 3366            OnlyWithReturns        = criteria.OnlyWithReturns,
 3367            Article                = criteria.Article,
 3368            ProductName            = criteria.ProductName,
 3369            ManufacturerName       = criteria.ManufacturerName,
 3370            NewCategory            = criteria.NewCategory,
 3371            MinQuantity            = criteria.MinQuantity,
 3372            MaxQuantity            = criteria.MaxQuantity,
 3373            PriceMin               = criteria.PriceMin,
 3374            PriceMax               = criteria.PriceMax,
 3375            ProfitMin              = criteria.ProfitMin,
 3376            ProfitMax              = criteria.ProfitMax,
 3377            OnlyNegativeProfit     = criteria.OnlyNegativeProfit,
 3378            PurchasePriceMax       = criteria.PurchasePriceMax,
 3379            ShipmentWarehouseName  = warehouse,
 3380            DeliveryProviderName   = criteria.DeliveryProviderName,
 3381            DeliveryName           = criteria.DeliveryName,
 3382            DeliveryCity           = criteria.DeliveryCity,
 3383            OnlyWithTrackingNumber = criteria.OnlyWithTrackingNumber,
 3384            ShippingDateFrom       = criteria.ShippingDateFrom,
 3385            ShippingDateTo         = criteria.ShippingDateTo,
 3386            ProcessingDateFrom     = criteria.ProcessingDateFrom,
 3387            ProcessingDateTo       = criteria.ProcessingDateTo,
 3388            LastMonths             = criteria.LastMonths,
 3389            ChangedFrom            = criteria.ChangedFrom,
 3390            ChangedTo              = criteria.ChangedTo,
 3391            FieldChangedName       = criteria.FieldChangedName,
 3392            FieldChangedToValue    = criteria.FieldChangedToValue,
 3393        };
 394    }
 395
 396    internal static bool IsCancelledAppStatus(string? statusName) =>
 16397        !string.IsNullOrWhiteSpace(statusName)
 16398        && statusName.Contains("отмен", StringComparison.OrdinalIgnoreCase);
 399
 400    private static string? TrimWarehouseName(string? value)
 401    {
 15402        if (string.IsNullOrWhiteSpace(value))
 9403            return null;
 404
 6405        var s = value.Trim();
 72406        foreach (var stop in new[] { " на клиенте", " у клиента", " на клиент", " за месяц", " за последн" })
 407        {
 30408            var idx = s.IndexOf(stop, StringComparison.OrdinalIgnoreCase);
 30409            if (idx > 0)
 1410                s = s[..idx].Trim();
 411        }
 412
 6413        return string.IsNullOrWhiteSpace(s) ? null : s;
 414    }
 415
 416    /// <summary>LLM иногда склеивает «склад M2 на клиенте Interparts» в одно поле.</summary>
 417    internal static (string? Warehouse, string? OzonClient) SplitWarehouseAndOzonClient(
 418        string? warehouse,
 419        string? ozonClient)
 420    {
 21421        if (string.IsNullOrWhiteSpace(warehouse))
 10422            return (warehouse, ozonClient);
 423
 11424        var markers = new[] { " на клиенте ", " у клиента ", " на клиент " };
 78425        foreach (var marker in markers)
 426        {
 29427            var idx = warehouse.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
 29428            if (idx <= 0)
 429                continue;
 430
 2431            var tail = warehouse[(idx + marker.Length)..].Trim();
 2432            warehouse = warehouse[..idx].Trim();
 2433            if (string.IsNullOrWhiteSpace(ozonClient) && !string.IsNullOrWhiteSpace(tail))
 2434                ozonClient = tail;
 2435            break;
 436        }
 437
 11438        return (warehouse, ozonClient);
 439    }
 440
 441    private static string? TrimSupplierName(string? value)
 442    {
 13443        if (string.IsNullOrWhiteSpace(value))
 10444            return null;
 445
 3446        var s = value.Trim();
 30447        foreach (var stop in new[] { " со статус", " статус", " склад", " артикул" })
 448        {
 12449            var idx = s.IndexOf(stop, StringComparison.OrdinalIgnoreCase);
 12450            if (idx > 0)
 1451                s = s[..idx].Trim();
 452        }
 453
 3454        return string.IsNullOrWhiteSpace(s) ? null : s;
 455    }
 456
 457    private static bool IsOrderSearchMessage(string lower) =>
 4458        lower.Contains("заказ") || lower.Contains("order");
 459
 460    private static bool IsGenericFindOrders(string lower) =>
 0461        ContainsAny(lower, "найди", "найти", "покажи", "показать", "список", "какие",
 0462            "выведи", "отбери", "подбери", "выбери", "ищи", "поиск");
 463
 464    private static bool IsEmptyPriceIntent(string lower) =>
 4465        (ContainsAny(lower, "пуст", "без", "нет", "не заполнен", "не указан", "null")
 4466         && lower.Contains("цен"))
 4467        || lower.Contains("без цены")
 4468        || lower.Contains("без цен");
 469
 470    private static int? TryParseMinQuantity(string lower)
 471    {
 24472        foreach (var rx in new[] { MinQuantityRegex(), MinQuantityPiecesRegex() })
 473        {
 8474            var m = rx.Match(lower);
 8475            if (m.Success && int.TryParse(m.Groups[1].Value, out var n) && n > 0)
 0476                return n;
 477        }
 4478        return null;
 479    }
 480
 481    private static decimal? TryParseDecimal(Match m)
 482    {
 40483        if (!m.Success) return null;
 0484        var raw = m.Groups[1].Value.Replace(" ", "").Trim();
 0485        return decimal.TryParse(raw, System.Globalization.NumberStyles.Number,
 0486            System.Globalization.CultureInfo.InvariantCulture, out var v)
 0487            ? v
 0488            : null;
 489    }
 490
 491    private static int? TryParseLastMonths(string lower)
 492    {
 4493        var m = LastMonthsRegex().Match(lower);
 4494        if (m.Success && int.TryParse(m.Groups[1].Value, out var n))
 0495            return n;
 4496        if (LastOneMonthRegex().IsMatch(lower) || ZaMonthRegex().IsMatch(lower))
 3497            return 1;
 1498        var w = LastWeeksRegex().Match(lower);
 499        // конвертируем недели в месяцы (округляем вверх)
 1500        if (w.Success && int.TryParse(w.Groups[1].Value, out var weeks))
 0501            return Math.Max(1, (int)Math.Ceiling(weeks / 4.0));
 1502        return null;
 503    }
 504
 505    // Temporal boundary words — stop capturing name before these
 506    [GeneratedRegex(@"\s+(?:за\s+последн\w*|за\s+прошл\w*|с\s+\d|по\s+\d|\d{2}\.\d{2}).+$",
 507        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 508    private static partial Regex TemporalSuffixRegex();
 509
 510    private static string? CleanCaptured(string value)
 511    {
 8512        var s = value.Trim();
 513        // Strip trailing temporal phrases like "за последние 3 месяца", "за прошлый месяц"
 8514        s = TemporalSuffixRegex().Replace(s, "").Trim();
 8515        s = s.TrimEnd('?', '!', '.', ',', ';').Trim();
 8516        return string.IsNullOrWhiteSpace(s) ? null : s;
 517    }
 518
 519    private static bool ContainsAny(string haystack, params string[] needles)
 520    {
 413521        foreach (var n in needles)
 522        {
 175523            if (haystack.Contains(n, StringComparison.Ordinal))
 7524                return true;
 525        }
 28526        return false;
 527    }
 528}

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/obj/Release/net8.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/obj/Release/net8.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).