using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; using Deal.Modules.Discovery.Application.Abstractions; using Deal.Modules.Discovery.Application.Exceptions; using Deal.Modules.Discovery.Application.Extensions; using Deal.Modules.Discovery.Application.Models; using Deal.Modules.Discovery.Application.Registrars; namespace Deal.Modules.Discovery.Application.Services; /// /// План-бюджет авто-вступлений Discovery: лимит и правило суммы (python L80–111, Ruling 9). /// /// /// Правило 1:1 с discovery.py: суточный лимит discJoinLimit (настройка, дефолт 50 — SettingsDefaults) — верхняя /// граница plan_joins одной задачи (); сумма plan_joins активных задач (status NOT IN /// done/failed) плюс новая/увеличиваемая задача не должна превышать лимит (). /// Итог семантики: задача на 50 занимает весь бюджет (другую создать нельзя); задача на 25 оставляет остаток /// 25 (следующая может быть не больше 25). Тексты 400 — 1:1 с python (L97–110). /// public sealed class DiscoveryPlanGuard(IDiscoveryStore store, ISettingsStore settings) { /// /// 400: план меньше 1 (python L97 «plan_joins должен быть не меньше 1»). /// public const string PlanTooSmallDetail = "plan_joins должен быть не меньше 1"; /// /// 400: план больше суточного лимита (python L100). Формат: {plan}, {limit}. /// public const string PlanTooBigFormat = "plan_joins {0} больше суточного лимита авто-вступлений ({1})"; /// /// 400: бюджет исчерпан (python L108–110). Формат: {used}, {limit}, {plan}. /// public const string BudgetExceededFormat = "Бюджет авто-вступлений исчерпан: задачи уже занимают {0} из {1} в сутки, ещё {2} не влезает"; /// /// Верхняя граница plan_joins: суточный лимит авто-вступлений (python _plan_limit L80–82). /// /// Токен отмены. /// discJoinLimit настройки (дефолт 50) с нижней границей 1. public async Task PlanLimitAsync(CancellationToken ct) { int limit = (await TenantSettingsSnapshot.LoadAsync(settings, ct)) .GetInt(SettingsKeys.DiscJoinLimit, SettingsDefaults.DiscJoinLimit); return Math.Max(1, limit); } /// /// Проверяет план новой задачи: 1..discJoinLimit (python _assert_plan L94–100). /// /// Запрашиваемый план авто-вступлений. /// Токен отмены. /// План меньше 1 или больше суточного лимита (тексты python). public async Task AssertPlanAsync(int planJoins, CancellationToken ct) { if (planJoins < 1) { throw new DiscoveryValidationException(PlanTooSmallDetail); } int limit = await PlanLimitAsync(ct).ConfigureAwait(false); if (planJoins > limit) { throw new DiscoveryValidationException(string.Format(PlanTooBigFormat, planJoins, limit)); } } /// /// Проверяет бюджет: занято активными задачами + новая ≤ discJoinLimit (python _assert_budget L103–111). /// /// План создаваемой/итоговый план увеличиваемой задачи. /// Id задачи, исключаемой из занятого бюджета (patch-рост плана); null — создание. /// Токен отмены. /// Бюджет исчерпан (текст python L108–110). public async Task AssertBudgetAsync( int planJoins, string? excludeTaskId, CancellationToken ct) { int limit = await PlanLimitAsync(ct).ConfigureAwait(false); int used = await store.SumActivePlanAsync(excludeTaskId, ct).ConfigureAwait(false); if (used + planJoins > limit) { throw new DiscoveryValidationException(string.Format(BudgetExceededFormat, used, limit, planJoins)); } } }