Вынести условия-предикаты в extension-методы

HasUser из 13 endpoint-файлов сведён в AuthHelpers.HasUser;
15 приватных предикатов заменены extension-методами с удалением
дублирующих приватных методов: IsCommunicationFailure,
IsTransportFailure, IsPrivateEndpoint, IsConfigured, IsTrue,
IsExpired, IsFailedLogin/IsSuccessfulLogin, HasChanges, HasBudget,
HasAnyTerm, IsCurrencyLetter, IsEmojiCodePoint, ContainsFooterHint,
IsTypeLabel.
This commit is contained in:
Rustam Khalimov
2026-09-11 13:08:47 +03:00
parent 9e07568ddd
commit 413eaac48c
40 changed files with 1091 additions and 993 deletions
@@ -148,13 +148,13 @@ public static class BudgetNormalizer
return direct;
}
string letters = new string(s.Where(IsCurrencyLetter).ToArray());
string letters = new string(s.Where(c => c.IsCurrencyLetter()).ToArray());
if (CurrencyAliases.TryGetValue(letters, out string? fromLetters))
{
return fromLetters;
}
if (s.Length == 3 && s.All(IsCurrencyLetter))
if (s.Length == 3 && s.All(c => c.IsCurrencyLetter()))
{
return s;
}
@@ -162,14 +162,6 @@ public static class BudgetNormalizer
return null;
}
// Буква кода валюты: латиница A–Z или кириллица А–Я (regex прототипа [^A-ZА-Я], ai.py L286).
// c: Символ (строка уже в верхнем регистре).
// Возвращает: True — буква, участвующая в распознавании валюты.
private static bool IsCurrencyLetter(char c)
{
return c is >= 'A' and <= 'Z' or >= 'А' and <= 'Я';
}
// Конвертация суммы через курсы к рублю; null rates → null (курсов нет — граница не конвертируется).
// amount: Сумма.
// fromCurrency: Исходная валюта (код).
@@ -0,0 +1,15 @@
namespace Deal.Modules.Kanban.Application;
/// <summary>
/// Расширения символов для нормализации бюджетной валюты.
/// </summary>
internal static class CharExtensions
{
/// <summary>
/// Буква кода валюты: латиница A–Z или кириллица А–Я (regex прототипа [^A-ZА-Я], ai.py L286).
/// </summary>
/// <param name="c">Символ (строка уже в верхнем регистре).</param>
/// <returns>True — буква, участвующая в распознавании валюты.</returns>
public static bool IsCurrencyLetter(this char c) =>
c is >= 'A' and <= 'Z' or >= 'А' and <= 'Я';
}
@@ -0,0 +1,21 @@
using Deal.Modules.Kanban.Application.Models;
namespace Deal.Modules.Kanban.Application.ColumnRules;
/// <summary>
/// Расширения бюджетной группы правил колонки.
/// </summary>
internal static class BudgetRangeDtoExtensions
{
/// <summary>
/// Активна ли бюджетная группа: объект есть и не «пустой» (прототип bool(budget) — {} выключен,
/// {cur} без границ включён; см. ColumnMatcher.HasActiveRules).
/// </summary>
/// <param name="budget">Поле budget/prices правил (может быть null).</param>
/// <returns>True — группа бюджета участвует в матчинге.</returns>
public static bool HasBudget(this BudgetRangeDto? budget)
{
return budget is not null
&& (budget.From is not null || budget.To is not null || !string.IsNullOrWhiteSpace(budget.Cur));
}
}
@@ -42,8 +42,8 @@ public static class ColumnMatcher
IReadOnlyList<string> locations = NormalizeTerms(rules.Locations);
IReadOnlyList<string> types = NormalizeTerms(rules.Types);
IReadOnlyList<string> typeTerms = TypeAliases.ExpandTerms(types);
bool budgetEnabled = HasBudget(rules.Budget);
bool pricesEnabled = HasBudget(rules.Prices);
bool budgetEnabled = rules.Budget.HasBudget();
bool pricesEnabled = rules.Prices.HasBudget();
bool anyEnabled = keywords.Count > 0 || stack.Count > 0 || direction.Count > 0 || grade.Count > 0
|| levels.Count > 0 || locations.Count > 0 || types.Count > 0 || budgetEnabled || pricesEnabled;
@@ -157,38 +157,17 @@ public static class ColumnMatcher
return false;
}
return HasAnyTerm(rules.Direction)
|| HasAnyTerm(rules.Keywords)
|| HasAnyTerm(rules.Stack)
|| HasAnyTerm(rules.Grade)
|| HasAnyTerm(rules.Levels)
|| HasAnyTerm(rules.Locations)
|| HasAnyTerm(rules.Types)
return rules.Direction.HasAnyTerm()
|| rules.Keywords.HasAnyTerm()
|| rules.Stack.HasAnyTerm()
|| rules.Grade.HasAnyTerm()
|| rules.Levels.HasAnyTerm()
|| rules.Locations.HasAnyTerm()
|| rules.Types.HasAnyTerm()
|| (rules.Budget is not null && (rules.Budget.From is not null || rules.Budget.To is not null))
|| (rules.Prices is not null && (rules.Prices.From is not null || rules.Prices.To is not null));
}
// Есть ли в списке непустой (после trim) терм.
// terms: Список термов группы.
// Возвращает: True — хотя бы один терм непустой.
private static bool HasAnyTerm(IReadOnlyList<string>? terms)
{
if (terms is null)
{
return false;
}
foreach (string? raw in terms)
{
if (!string.IsNullOrWhiteSpace(raw))
{
return true;
}
}
return false;
}
// Совпал ли хотя бы один терм (подстрока в тексте).
// lower: Текст в нижнем регистре (очищен от ссылок).
// terms: Термы в нижнем регистре.
@@ -252,14 +231,4 @@ public static class ColumnMatcher
return result;
}
// Активна ли бюджетная группа: объект есть и не «пустой» (прототип bool(budget) — {} выключен,
// {cur} без границ включён, но границ не задаёт — группа совпадает всегда).
// budget: Поле budget правил.
// Возвращает: True — группа бюджета участвует в матчинге.
private static bool HasBudget(BudgetRangeDto? budget)
{
return budget is not null
&& (budget.From is not null || budget.To is not null || !string.IsNullOrWhiteSpace(budget.Cur));
}
}
@@ -0,0 +1,30 @@
namespace Deal.Modules.Kanban.Application.ColumnRules;
/// <summary>
/// Расширения списков термов правил колонки.
/// </summary>
internal static class TermListExtensions
{
/// <summary>
/// Есть ли в списке непустой (после trim) терм.
/// </summary>
/// <param name="terms">Список термов группы (может быть null).</param>
/// <returns>True — хотя бы один терм непустой.</returns>
public static bool HasAnyTerm(this IReadOnlyList<string>? terms)
{
if (terms is null)
{
return false;
}
foreach (string? raw in terms)
{
if (!string.IsNullOrWhiteSpace(raw))
{
return true;
}
}
return false;
}
}