diff --git a/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs b/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs
index 281c8b3..72646f2 100644
--- a/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs
+++ b/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs
@@ -1,10 +1,11 @@
using System.Text.Json;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
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.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/AiSuggestEndpoints.cs b/src/core/Deal.Api/Endpoints/AiSuggestEndpoints.cs
index 80e4394..85e6b21 100644
--- a/src/core/Deal.Api/Endpoints/AiSuggestEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/AiSuggestEndpoints.cs
@@ -1,8 +1,9 @@
using Deal.Api.Events;
-using Deal.Api.Http;
-using Deal.Contracts.Integrations;
-using Deal.Contracts.Integrations.Models;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
using Deal.Api.Services;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/AuthEndpoints.cs b/src/core/Deal.Api/Endpoints/AuthEndpoints.cs
index 0fef7d7..2dcfa90 100644
--- a/src/core/Deal.Api/Endpoints/AuthEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/AuthEndpoints.cs
@@ -1,4 +1,6 @@
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Api.Middleware;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
@@ -6,7 +8,6 @@ using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
using Microsoft.Extensions.Options;
-using Deal.Api.Services;
using Deal.Api.Dtos;
// Имя конфигурационного типа совпадает с Microsoft.AspNetCore.Http.CookieOptions — фиксируем алиасом.
using CookieOptions = Deal.Api.Configuration.CookieOptions;
diff --git a/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs
index b2815e0..2a6d8bf 100644
--- a/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs
@@ -1,7 +1,9 @@
using System.Text.Json;
using Deal.Api.Endpoints.RequestModels;
-using Deal.Api.Http;
-using Deal.Contracts.Integrations;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
@@ -13,7 +15,6 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs
index eb2ca1d..f8f3635 100644
--- a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs
@@ -1,6 +1,8 @@
using Deal.Api.Endpoints.RequestModels;
using Deal.Api.Events;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Cards.Application.Abstractions;
using Deal.Modules.Cards.Application.Dtos;
using Deal.Modules.Cards.Application.Models;
@@ -18,7 +20,6 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs b/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs
index 18ccdca..3fe2f57 100644
--- a/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs
@@ -1,6 +1,8 @@
using System.Text.Json;
using Deal.Api.Endpoints.RequestModels;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
using Deal.Modules.Kanban.Application.Models;
@@ -11,7 +13,6 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs b/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs
index c84f691..2b0af03 100644
--- a/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs
@@ -1,7 +1,9 @@
using System.Text.Json;
using Deal.Api.Endpoints.RequestModels;
-using Deal.Api.Http;
-using Deal.Contracts.Integrations;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Discovery.Application.Abstractions;
using Deal.Modules.Discovery.Application.Exceptions;
@@ -14,7 +16,6 @@ using Deal.Modules.Settings.Application.Models;
using Deal.Modules.Settings.Application.Registrars;
using Deal.Modules.Settings.Application.Services;
using Deal.Modules.Telegram.Application;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/EventsEndpoint.cs b/src/core/Deal.Api/Endpoints/EventsEndpoint.cs
index ee18cd2..ab93445 100644
--- a/src/core/Deal.Api/Endpoints/EventsEndpoint.cs
+++ b/src/core/Deal.Api/Endpoints/EventsEndpoint.cs
@@ -1,5 +1,6 @@
using Deal.Api.Events;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
using Deal.Api.Services;
using Deal.Api.Dtos;
diff --git a/src/core/Deal.Api/Endpoints/FilterTesterEndpoints.cs b/src/core/Deal.Api/Endpoints/FilterTesterEndpoints.cs
index 0d9ecd4..33a4427 100644
--- a/src/core/Deal.Api/Endpoints/FilterTesterEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/FilterTesterEndpoints.cs
@@ -1,9 +1,10 @@
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
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.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/JoinEndpoint.cs b/src/core/Deal.Api/Endpoints/JoinEndpoint.cs
index d340780..c39877d 100644
--- a/src/core/Deal.Api/Endpoints/JoinEndpoint.cs
+++ b/src/core/Deal.Api/Endpoints/JoinEndpoint.cs
@@ -1,10 +1,11 @@
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/MlEndpoints.cs b/src/core/Deal.Api/Endpoints/MlEndpoints.cs
index c063fa4..02d8d4d 100644
--- a/src/core/Deal.Api/Endpoints/MlEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/MlEndpoints.cs
@@ -1,11 +1,12 @@
-using Deal.Api.Http;
-using Deal.Contracts.Integrations;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Pipeline.Application.Abstractions;
using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Registrars;
using Deal.Modules.Pipeline.Application.Services;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs
index a3f7a2a..ec01601 100644
--- a/src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs
@@ -1,10 +1,11 @@
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/OperatorAuditEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorAuditEndpoints.cs
index 67ef3dd..4a909e6 100644
--- a/src/core/Deal.Api/Endpoints/OperatorAuditEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorAuditEndpoints.cs
@@ -1,10 +1,11 @@
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs
index 5202f38..854ba92 100644
--- a/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs
@@ -1,4 +1,6 @@
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Api.Middleware;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
@@ -6,7 +8,6 @@ using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
using Microsoft.Extensions.Options;
-using Deal.Api.Services;
using Deal.Api.Dtos;
using AspNetCoreCookieOptions = Microsoft.AspNetCore.Http.CookieOptions;
// Имя конфигурационного типа совпадает с Microsoft.AspNetCore.Http.CookieOptions — фиксируем алиасами.
diff --git a/src/core/Deal.Api/Endpoints/OperatorHealthEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorHealthEndpoints.cs
index 67a6b35..25ab469 100644
--- a/src/core/Deal.Api/Endpoints/OperatorHealthEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorHealthEndpoints.cs
@@ -1,4 +1,6 @@
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Api.Observability;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
@@ -9,7 +11,6 @@ using Deal.Infrastructure.Integrations.Services;
using Deal.Infrastructure.Persistence;
using Deal.Infrastructure.Persistence.Configurations;
using Microsoft.EntityFrameworkCore;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/OperatorInvitesEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorInvitesEndpoints.cs
index 7a2c2a2..c627794 100644
--- a/src/core/Deal.Api/Endpoints/OperatorInvitesEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorInvitesEndpoints.cs
@@ -1,10 +1,11 @@
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/OperatorLimitsEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorLimitsEndpoints.cs
index 6a82f7d..e627b29 100644
--- a/src/core/Deal.Api/Endpoints/OperatorLimitsEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorLimitsEndpoints.cs
@@ -1,10 +1,11 @@
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/OperatorMaintenanceEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorMaintenanceEndpoints.cs
index 4bd1715..b1dc9e1 100644
--- a/src/core/Deal.Api/Endpoints/OperatorMaintenanceEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorMaintenanceEndpoints.cs
@@ -1,66 +1,67 @@
-using Deal.Api.Http;
-using Deal.Infrastructure.Tenancy;
-using Deal.Api.Services;
-using Deal.Api.Dtos;
-
-namespace Deal.Api.Endpoints;
-
-///
-/// Операторские maintenance-ручки (этап 12, пакет C): пакетная миграция схем всех тенантов.
-///
-///
-/// Ручка — только оператору (401 «Требуется вход оператора» без операторской сессии).
-/// POST /api/operator/maintenance/tenants/migrate — идемпотентно проводит провижининг/миграции схем ВСЕХ
-/// тенантов реестра (CREATE SCHEMA IF NOT EXISTS + EF Migrate, применяющий только неприменённые миграции)
-/// с ограниченным параллелизмом и логированием прогресса ().
-/// Ответ {ok, total, migrated, failed, failedSchemas, durationMs}; ok=false, если хотя бы одна схема
-/// не мигрирована (сбой одной не прерывает остальные — оператор видит список проблемных схем).
-///
-public static class OperatorMaintenanceEndpoints
-{
- // Префикс группы операторских maintenance-ручек.
- private const string MaintenanceGroupPrefix = "/api/operator/maintenance";
-
- // Относительный путь пакетной миграции схем тенантов.
- private const string MigrateTenantsPath = "/tenants/migrate";
-
- // OpenAPI-тег группы.
- private const string MaintenanceOpenApiTag = "operator-maintenance";
-
- ///
- /// Регистрирует группу /api/operator/maintenance: пакетная миграция схем тенантов.
- ///
- /// Построитель маршрутов приложения.
- /// Построитель маршрутов для цепочки вызовов.
- public static IEndpointRouteBuilder MapOperatorMaintenanceEndpoints(this IEndpointRouteBuilder app)
- {
- app.MapGroup(MaintenanceGroupPrefix)
- .WithTags(MaintenanceOpenApiTag)
- .MapPost(MigrateTenantsPath, MigrateTenantsAsync);
- return app;
- }
-
- // POST /api/operator/maintenance/tenants/migrate: миграция схем всех тенантов (сводка прогресса).
- private static async Task MigrateTenantsAsync(
- HttpContext context,
- TenantSchemaMigrationService migrationService,
- CancellationToken ct)
- {
- var operatorIdentity = context.GetCurrentOperator();
- if (operatorIdentity is null)
- {
- return EndpointResults.Unauthorized(AuthHelpers.OperatorUnauthorizedDetail);
- }
-
- TenantMigrationSummary summary = await migrationService.MigrateAllAsync(ct);
- return Results.Ok(new
- {
- ok = summary.Ok,
- total = summary.Total,
- migrated = summary.Migrated,
- failed = summary.Failed,
- failedSchemas = summary.FailedSchemas,
- durationMs = summary.DurationMs,
- });
- }
-}
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
+using Deal.Infrastructure.Tenancy;
+using Deal.Api.Dtos;
+
+namespace Deal.Api.Endpoints;
+
+///
+/// Операторские maintenance-ручки (этап 12, пакет C): пакетная миграция схем всех тенантов.
+///
+///
+/// Ручка — только оператору (401 «Требуется вход оператора» без операторской сессии).
+/// POST /api/operator/maintenance/tenants/migrate — идемпотентно проводит провижининг/миграции схем ВСЕХ
+/// тенантов реестра (CREATE SCHEMA IF NOT EXISTS + EF Migrate, применяющий только неприменённые миграции)
+/// с ограниченным параллелизмом и логированием прогресса ().
+/// Ответ {ok, total, migrated, failed, failedSchemas, durationMs}; ok=false, если хотя бы одна схема
+/// не мигрирована (сбой одной не прерывает остальные — оператор видит список проблемных схем).
+///
+public static class OperatorMaintenanceEndpoints
+{
+ // Префикс группы операторских maintenance-ручек.
+ private const string MaintenanceGroupPrefix = "/api/operator/maintenance";
+
+ // Относительный путь пакетной миграции схем тенантов.
+ private const string MigrateTenantsPath = "/tenants/migrate";
+
+ // OpenAPI-тег группы.
+ private const string MaintenanceOpenApiTag = "operator-maintenance";
+
+ ///
+ /// Регистрирует группу /api/operator/maintenance: пакетная миграция схем тенантов.
+ ///
+ /// Построитель маршрутов приложения.
+ /// Построитель маршрутов для цепочки вызовов.
+ public static IEndpointRouteBuilder MapOperatorMaintenanceEndpoints(this IEndpointRouteBuilder app)
+ {
+ app.MapGroup(MaintenanceGroupPrefix)
+ .WithTags(MaintenanceOpenApiTag)
+ .MapPost(MigrateTenantsPath, MigrateTenantsAsync);
+ return app;
+ }
+
+ // POST /api/operator/maintenance/tenants/migrate: миграция схем всех тенантов (сводка прогресса).
+ private static async Task MigrateTenantsAsync(
+ HttpContext context,
+ TenantSchemaMigrationService migrationService,
+ CancellationToken ct)
+ {
+ var operatorIdentity = context.GetCurrentOperator();
+ if (operatorIdentity is null)
+ {
+ return EndpointResults.Unauthorized(AuthHelpers.OperatorUnauthorizedDetail);
+ }
+
+ TenantMigrationSummary summary = await migrationService.MigrateAllAsync(ct);
+ return Results.Ok(new
+ {
+ ok = summary.Ok,
+ total = summary.Total,
+ migrated = summary.Migrated,
+ failed = summary.Failed,
+ failedSchemas = summary.FailedSchemas,
+ durationMs = summary.DurationMs,
+ });
+ }
+}
diff --git a/src/core/Deal.Api/Endpoints/OperatorSettingsEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorSettingsEndpoints.cs
index ad3a881..7aa7886 100644
--- a/src/core/Deal.Api/Endpoints/OperatorSettingsEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorSettingsEndpoints.cs
@@ -1,12 +1,13 @@
using Deal.Api.Endpoints.RequestModels;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Api.Telegram;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs
index 989fc16..7e70f48 100644
--- a/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs
@@ -1,11 +1,12 @@
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
using Microsoft.Extensions.Options;
-using Deal.Api.Services;
using Deal.Api.Dtos;
using CookieOptions = Deal.Api.Configuration.CookieOptions;
diff --git a/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs b/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs
index f1eb701..9840e56 100644
--- a/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs
@@ -1,10 +1,11 @@
using Deal.Api.Endpoints.RequestModels;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Pipeline.Application.Abstractions;
using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Registrars;
using Deal.Modules.Pipeline.Application.Services;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/RatesEndpoints.cs b/src/core/Deal.Api/Endpoints/RatesEndpoints.cs
index 666eece..16c242c 100644
--- a/src/core/Deal.Api/Endpoints/RatesEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/RatesEndpoints.cs
@@ -1,9 +1,10 @@
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
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.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs b/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs
index 56e6504..c2d35b1 100644
--- a/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs
@@ -2,7 +2,8 @@ using System.Text.Json;
using Deal.Api;
using Deal.Api.Dtos;
using Deal.Api.Services;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
using Deal.Modules.Settings.Application.Abstractions;
using Deal.Modules.Settings.Application.Models;
using Deal.Modules.Settings.Application.Registrars;
diff --git a/src/core/Deal.Api/Endpoints/StorageEndpoints.cs b/src/core/Deal.Api/Endpoints/StorageEndpoints.cs
index 9b137fd..de79cb2 100644
--- a/src/core/Deal.Api/Endpoints/StorageEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/StorageEndpoints.cs
@@ -1,6 +1,7 @@
-using Deal.Api.Http;
-using Deal.Infrastructure.Services;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
using Deal.Api.Services;
+using Deal.Infrastructure.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs b/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs
index f2d905a..e27196f 100644
--- a/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs
@@ -1,7 +1,9 @@
using Deal.Api.Endpoints.RequestModels;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Api.Telegram;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Telegram.Application;
using Deal.Modules.Telegram.Application.Models;
@@ -10,7 +12,6 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Endpoints/TelegramQrImageEndpoint.cs b/src/core/Deal.Api/Endpoints/TelegramQrImageEndpoint.cs
index 8ea3e26..354b5d0 100644
--- a/src/core/Deal.Api/Endpoints/TelegramQrImageEndpoint.cs
+++ b/src/core/Deal.Api/Endpoints/TelegramQrImageEndpoint.cs
@@ -1,8 +1,9 @@
-using Deal.Api.Http;
-using Deal.Contracts.Integrations;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Net.Codecrete.QrCodeGenerator;
-using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Endpoints;
diff --git a/src/core/Deal.Api/Http/AuthHelpers.cs b/src/core/Deal.Api/Extensions/AuthHelpers.cs
similarity index 98%
rename from src/core/Deal.Api/Http/AuthHelpers.cs
rename to src/core/Deal.Api/Extensions/AuthHelpers.cs
index 0d3fa8f..f32d05b 100644
--- a/src/core/Deal.Api/Http/AuthHelpers.cs
+++ b/src/core/Deal.Api/Extensions/AuthHelpers.cs
@@ -1,7 +1,8 @@
using Deal.Api.Services;
using Deal.Api.Dtos;
+using Deal.Api.Models;
-namespace Deal.Api.Http;
+namespace Deal.Api.Extensions;
///
/// Хелперы доступа к текущему пользователю запроса (минимальные API).
diff --git a/src/core/Deal.Api/Hosting/DiscoveryWorkerScheduler.cs b/src/core/Deal.Api/Hosting/DiscoveryWorkerScheduler.cs
index 759addb..1da90c2 100644
--- a/src/core/Deal.Api/Hosting/DiscoveryWorkerScheduler.cs
+++ b/src/core/Deal.Api/Hosting/DiscoveryWorkerScheduler.cs
@@ -9,7 +9,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Deal.Api.Services;
using Deal.Api.Dtos;
diff --git a/src/core/Deal.Api/Hosting/MlOutboxFlushScheduler.cs b/src/core/Deal.Api/Hosting/MlOutboxFlushScheduler.cs
index 8081f6f..95bc71f 100644
--- a/src/core/Deal.Api/Hosting/MlOutboxFlushScheduler.cs
+++ b/src/core/Deal.Api/Hosting/MlOutboxFlushScheduler.cs
@@ -14,7 +14,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Deal.Api.Services;
using Deal.Api.Dtos;
diff --git a/src/core/Deal.Api/Hosting/StorageTickScheduler.cs b/src/core/Deal.Api/Hosting/StorageTickScheduler.cs
index e67816b..e732e7e 100644
--- a/src/core/Deal.Api/Hosting/StorageTickScheduler.cs
+++ b/src/core/Deal.Api/Hosting/StorageTickScheduler.cs
@@ -13,7 +13,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Deal.Api.Services;
using Deal.Api.Dtos;
diff --git a/src/core/Deal.Api/Middleware/OperatorSessionMiddleware.cs b/src/core/Deal.Api/Middleware/OperatorSessionMiddleware.cs
index c5eefc9..4881601 100644
--- a/src/core/Deal.Api/Middleware/OperatorSessionMiddleware.cs
+++ b/src/core/Deal.Api/Middleware/OperatorSessionMiddleware.cs
@@ -1,12 +1,13 @@
using Deal.Api.Configuration;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
using Microsoft.Extensions.Options;
-using Deal.Api.Services;
using Deal.Api.Dtos;
// Имя конфигурационного типа совпадает с Microsoft.AspNetCore.Http.CookieOptions — фиксируем алиасом.
using OperatorCookieOptions = Deal.Api.Configuration.OperatorCookieOptions;
diff --git a/src/core/Deal.Api/Middleware/RateLimitPolicies.cs b/src/core/Deal.Api/Middleware/RateLimitPolicies.cs
index b91e191..40d086f 100644
--- a/src/core/Deal.Api/Middleware/RateLimitPolicies.cs
+++ b/src/core/Deal.Api/Middleware/RateLimitPolicies.cs
@@ -1,154 +1,155 @@
-using System.Threading.RateLimiting;
-using Deal.Api.Configuration;
-using Deal.Api.Http;
-using Microsoft.AspNetCore.RateLimiting;
-using Microsoft.Extensions.DependencyInjection;
-using Deal.Api.Services;
-using Deal.Api.Dtos;
-
-namespace Deal.Api.Middleware;
-
-///
-/// Регистрация встроенного rate limiter ASP.NET Core (план Task 11, Ruling 5; этап 12, пакет B — хранилище
-/// на Postgres) и его политики: "auth" — фиксированное окно на IP клиента (ручки входа /api/auth/login и
-/// /api/operator/auth/login), "api" — на тенанта из либо IP анонима
-/// (Session/OperatorSessionMiddleware отрабатывают раньше — порядок Session → Operator → RateLimiter).
-/// API-партиция выставляется и глобальным лимитером: все /api-эндпоинты без собственной политики
-/// ограничены 600/мин на тенанта/IP. Отказ любого лимитера — HTTP 429 с телом {"detail": "…"}.
-///
-///
-/// Регистрируется только при (в dev/тестах middleware и политики
-/// не создаются — Ruling 5). gRPC-ингресс (:5082) HTTP-лимитером освобождён (DisableRateLimiting на
-/// MapGrpcService): лимит по tenant-id там считает IngressRateLimitInterceptor — иначе входящий поток
-/// telegram-service резался бы общим окном на IP.
-///
-/// Счётчики окон живут в Postgres ( через scoped
-/// ) — лимиты общие для всех инстансов core. Пороги/окна
-/// не менялись: фиксированные 1 минута, PermitLimit из .
-///
-///
-public static class RateLimitPolicies
-{
- ///
- /// Имя политики входа: фиксированное окно по IP (RateLimit:AuthPerMinute).
- ///
- public const string AuthPolicy = "auth";
-
- ///
- /// Имя API-политики: фиксированное окно по CurrentUser.TenantId либо IP анонима (RateLimit:ApiPerMinute).
- ///
- public const string ApiPolicy = "api";
-
- ///
- /// Текст 429 rate limiter (Ruling 5): все отказы лимитов запросов — единый detail.
- ///
- public const string RejectedDetail = "Слишком много запросов. Повторите позже";
-
- // Префикс ключа хранилища для HTTP-политик: изолирует счётчики от gRPC-ингресса и login-guard.
- private const string HttpKeyPrefix = "http:";
-
- // Пространство ключа глобального лимитера: отдельные счётчики от именованной "api"-политики
- // (как и раньше — независимые FixedWindowRateLimiter'ы, лимиты не схлопываются в один бакет).
- private const string GlobalScope = "global";
-
- // Префикс партиции API-политики для анонима (IP не разрешён в сессию).
- private const string IpKeyPrefix = "ip:";
-
- // Префикс партиции API-политики для аутентифицированного пользователя (по тенанту).
- private const string TenantKeyPrefix = "tenant:";
-
- // Ключ партиции, когда IP клиента недоступен (служебные/тестовые вызовы).
- private const string UnknownClientKey = "unknown";
-
- // Длина окна политик — 1 минута (имена настроек — «PerMinute»).
- private static readonly TimeSpan Window = TimeSpan.FromMinutes(1);
-
- ///
- /// Регистрирует AddRateLimiter с политиками "auth"/"api" и глобальным лимитером API-партиции.
- ///
- /// Коллекция сервисов хоста.
- /// Настройки rate limiting (секция RateLimit).
- /// Коллекция сервисов для цепочки вызовов.
- public static IServiceCollection AddDealRateLimiter(this IServiceCollection services, RateLimitOptions options)
- {
- ArgumentNullException.ThrowIfNull(services);
- ArgumentNullException.ThrowIfNull(options);
-
- int apiPerMinute = options.ApiPerMinute;
- services.AddRateLimiter(limiter =>
- {
- limiter.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
- limiter.OnRejected = OnRejectedAsync;
- limiter.AddPolicy(AuthPolicy, context => AuthPartition(context, options.AuthPerMinute));
- limiter.AddPolicy(ApiPolicy, context => ApiPartition(context, ApiPolicy, apiPerMinute));
- // Глобальный лимитер — та же API-партиция (своё пространство ключей): эндпоинты без
- // RequireRateLimiting ограничены по тенанту/IP (весь /api, кроме ручек входа и gRPC).
- limiter.GlobalLimiter = PartitionedRateLimiter.Create(
- context => ApiPartition(context, GlobalScope, apiPerMinute));
- });
-
- return services;
- }
-
- // Партиция политики "auth": фиксированное окно в минуту на IP клиента.
- // context: Контекст запроса (адрес клиента).
- // permitsPerMinute: Разрешено запросов в минуту на партицию (RateLimit:AuthPerMinute).
- // Возвращает: Партиция лимитера с ключом IP.
- private static RateLimitPartition AuthPartition(HttpContext context, int permitsPerMinute)
- => StorePartition(context, AuthPolicy, ClientKey(context), permitsPerMinute);
-
- // Партиция политики "api": ключ — тенант сессии (CurrentUser) либо IP анонима.
- // context: Контекст запроса (CurrentUser кладёт SessionMiddleware до UseRateLimiter).
- // scope: Пространство ключей (имя политики или "global" для глобального лимитера).
- // permitsPerMinute: Разрешено запросов в минуту на партицию (RateLimit:ApiPerMinute).
- // Возвращает: Партиция лимитера с ключом tenant:{id} либо IP.
- private static RateLimitPartition ApiPartition(
- HttpContext context,
- string scope,
- int permitsPerMinute)
- {
- string partitionKey = context.GetCurrentUser() is { } user
- ? TenantKeyPrefix + user.TenantId.ToString("N")
- : ClientKey(context);
- return StorePartition(context, scope, partitionKey, permitsPerMinute);
- }
-
- // Строит партицию на хранилище Postgres: ключ = http:{scope}:{partitionKey}, лимитер — лениво.
- // context: Контекст запроса (источник IServiceScopeFactory для лениво создаваемого лимитера).
- // scope: Пространство ключей (политика/global).
- // partitionKey: Ключ партиции (IP/тенант).
- // permitsPerMinute: Разрешено запросов в минуту.
- // Возвращает: Партиция store-backed лимитера (ленивое создание и кеш — менеджер партиций).
- private static RateLimitPartition StorePartition(
- HttpContext context,
- string scope,
- string partitionKey,
- int permitsPerMinute)
- {
- string storeKey = HttpKeyPrefix + scope + ":" + partitionKey;
- return RateLimitPartition.Get(storeKey, key => new StoreBackedFixedWindowRateLimiter(
- context.RequestServices.GetRequiredService(),
- key,
- permitsPerMinute,
- Window,
- () => DateTimeOffset.UtcNow));
- }
-
- // Ключ клиента: строка IP (или unknown, если адрес недоступен).
- // context: Контекст запроса.
- // Возвращает: Ключ партиции по IP.
- private static string ClientKey(HttpContext context)
- {
- string? ip = context.Connection.RemoteIpAddress?.ToString();
- return string.IsNullOrEmpty(ip) ? UnknownClientKey : IpKeyPrefix + ip;
- }
-
- // Пишет ответ 429 формата прототипа: {"detail": "…"} (Ruling 10/5).
- // context: Контекст отклонённого запроса.
- // cancellationToken: Токен отмены ответа.
- private static async ValueTask OnRejectedAsync(OnRejectedContext context, CancellationToken cancellationToken)
- {
- context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
- await context.HttpContext.Response.WriteAsJsonAsync(new { detail = RejectedDetail }, cancellationToken);
- }
-}
+using System.Threading.RateLimiting;
+using Deal.Api.Configuration;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
+using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.Extensions.DependencyInjection;
+using Deal.Api.Dtos;
+
+namespace Deal.Api.Middleware;
+
+///
+/// Регистрация встроенного rate limiter ASP.NET Core (план Task 11, Ruling 5; этап 12, пакет B — хранилище
+/// на Postgres) и его политики: "auth" — фиксированное окно на IP клиента (ручки входа /api/auth/login и
+/// /api/operator/auth/login), "api" — на тенанта из либо IP анонима
+/// (Session/OperatorSessionMiddleware отрабатывают раньше — порядок Session → Operator → RateLimiter).
+/// API-партиция выставляется и глобальным лимитером: все /api-эндпоинты без собственной политики
+/// ограничены 600/мин на тенанта/IP. Отказ любого лимитера — HTTP 429 с телом {"detail": "…"}.
+///
+///
+/// Регистрируется только при (в dev/тестах middleware и политики
+/// не создаются — Ruling 5). gRPC-ингресс (:5082) HTTP-лимитером освобождён (DisableRateLimiting на
+/// MapGrpcService): лимит по tenant-id там считает IngressRateLimitInterceptor — иначе входящий поток
+/// telegram-service резался бы общим окном на IP.
+///
+/// Счётчики окон живут в Postgres ( через scoped
+/// ) — лимиты общие для всех инстансов core. Пороги/окна
+/// не менялись: фиксированные 1 минута, PermitLimit из .
+///
+///
+public static class RateLimitPolicies
+{
+ ///
+ /// Имя политики входа: фиксированное окно по IP (RateLimit:AuthPerMinute).
+ ///
+ public const string AuthPolicy = "auth";
+
+ ///
+ /// Имя API-политики: фиксированное окно по CurrentUser.TenantId либо IP анонима (RateLimit:ApiPerMinute).
+ ///
+ public const string ApiPolicy = "api";
+
+ ///
+ /// Текст 429 rate limiter (Ruling 5): все отказы лимитов запросов — единый detail.
+ ///
+ public const string RejectedDetail = "Слишком много запросов. Повторите позже";
+
+ // Префикс ключа хранилища для HTTP-политик: изолирует счётчики от gRPC-ингресса и login-guard.
+ private const string HttpKeyPrefix = "http:";
+
+ // Пространство ключа глобального лимитера: отдельные счётчики от именованной "api"-политики
+ // (как и раньше — независимые FixedWindowRateLimiter'ы, лимиты не схлопываются в один бакет).
+ private const string GlobalScope = "global";
+
+ // Префикс партиции API-политики для анонима (IP не разрешён в сессию).
+ private const string IpKeyPrefix = "ip:";
+
+ // Префикс партиции API-политики для аутентифицированного пользователя (по тенанту).
+ private const string TenantKeyPrefix = "tenant:";
+
+ // Ключ партиции, когда IP клиента недоступен (служебные/тестовые вызовы).
+ private const string UnknownClientKey = "unknown";
+
+ // Длина окна политик — 1 минута (имена настроек — «PerMinute»).
+ private static readonly TimeSpan Window = TimeSpan.FromMinutes(1);
+
+ ///
+ /// Регистрирует AddRateLimiter с политиками "auth"/"api" и глобальным лимитером API-партиции.
+ ///
+ /// Коллекция сервисов хоста.
+ /// Настройки rate limiting (секция RateLimit).
+ /// Коллекция сервисов для цепочки вызовов.
+ public static IServiceCollection AddDealRateLimiter(this IServiceCollection services, RateLimitOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(options);
+
+ int apiPerMinute = options.ApiPerMinute;
+ services.AddRateLimiter(limiter =>
+ {
+ limiter.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
+ limiter.OnRejected = OnRejectedAsync;
+ limiter.AddPolicy(AuthPolicy, context => AuthPartition(context, options.AuthPerMinute));
+ limiter.AddPolicy(ApiPolicy, context => ApiPartition(context, ApiPolicy, apiPerMinute));
+ // Глобальный лимитер — та же API-партиция (своё пространство ключей): эндпоинты без
+ // RequireRateLimiting ограничены по тенанту/IP (весь /api, кроме ручек входа и gRPC).
+ limiter.GlobalLimiter = PartitionedRateLimiter.Create(
+ context => ApiPartition(context, GlobalScope, apiPerMinute));
+ });
+
+ return services;
+ }
+
+ // Партиция политики "auth": фиксированное окно в минуту на IP клиента.
+ // context: Контекст запроса (адрес клиента).
+ // permitsPerMinute: Разрешено запросов в минуту на партицию (RateLimit:AuthPerMinute).
+ // Возвращает: Партиция лимитера с ключом IP.
+ private static RateLimitPartition AuthPartition(HttpContext context, int permitsPerMinute)
+ => StorePartition(context, AuthPolicy, ClientKey(context), permitsPerMinute);
+
+ // Партиция политики "api": ключ — тенант сессии (CurrentUser) либо IP анонима.
+ // context: Контекст запроса (CurrentUser кладёт SessionMiddleware до UseRateLimiter).
+ // scope: Пространство ключей (имя политики или "global" для глобального лимитера).
+ // permitsPerMinute: Разрешено запросов в минуту на партицию (RateLimit:ApiPerMinute).
+ // Возвращает: Партиция лимитера с ключом tenant:{id} либо IP.
+ private static RateLimitPartition ApiPartition(
+ HttpContext context,
+ string scope,
+ int permitsPerMinute)
+ {
+ string partitionKey = context.GetCurrentUser() is { } user
+ ? TenantKeyPrefix + user.TenantId.ToString("N")
+ : ClientKey(context);
+ return StorePartition(context, scope, partitionKey, permitsPerMinute);
+ }
+
+ // Строит партицию на хранилище Postgres: ключ = http:{scope}:{partitionKey}, лимитер — лениво.
+ // context: Контекст запроса (источник IServiceScopeFactory для лениво создаваемого лимитера).
+ // scope: Пространство ключей (политика/global).
+ // partitionKey: Ключ партиции (IP/тенант).
+ // permitsPerMinute: Разрешено запросов в минуту.
+ // Возвращает: Партиция store-backed лимитера (ленивое создание и кеш — менеджер партиций).
+ private static RateLimitPartition StorePartition(
+ HttpContext context,
+ string scope,
+ string partitionKey,
+ int permitsPerMinute)
+ {
+ string storeKey = HttpKeyPrefix + scope + ":" + partitionKey;
+ return RateLimitPartition.Get(storeKey, key => new StoreBackedFixedWindowRateLimiter(
+ context.RequestServices.GetRequiredService(),
+ key,
+ permitsPerMinute,
+ Window,
+ () => DateTimeOffset.UtcNow));
+ }
+
+ // Ключ клиента: строка IP (или unknown, если адрес недоступен).
+ // context: Контекст запроса.
+ // Возвращает: Ключ партиции по IP.
+ private static string ClientKey(HttpContext context)
+ {
+ string? ip = context.Connection.RemoteIpAddress?.ToString();
+ return string.IsNullOrEmpty(ip) ? UnknownClientKey : IpKeyPrefix + ip;
+ }
+
+ // Пишет ответ 429 формата прототипа: {"detail": "…"} (Ruling 10/5).
+ // context: Контекст отклонённого запроса.
+ // cancellationToken: Токен отмены ответа.
+ private static async ValueTask OnRejectedAsync(OnRejectedContext context, CancellationToken cancellationToken)
+ {
+ context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
+ await context.HttpContext.Response.WriteAsJsonAsync(new { detail = RejectedDetail }, cancellationToken);
+ }
+}
diff --git a/src/core/Deal.Api/Middleware/SessionMiddleware.cs b/src/core/Deal.Api/Middleware/SessionMiddleware.cs
index 6e17e9e..ca5b847 100644
--- a/src/core/Deal.Api/Middleware/SessionMiddleware.cs
+++ b/src/core/Deal.Api/Middleware/SessionMiddleware.cs
@@ -1,13 +1,15 @@
using Deal.Api.Configuration;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.Options;
-using Deal.Api.Services;
using Deal.Api.Dtos;
// Имя конфигурационного типа совпадает с Microsoft.AspNetCore.Http.CookieOptions — фиксируем алиасом.
using CookieOptions = Deal.Api.Configuration.CookieOptions;
diff --git a/src/core/Deal.Api/Http/CurrentOperator.cs b/src/core/Deal.Api/Models/CurrentOperator.cs
similarity index 91%
rename from src/core/Deal.Api/Http/CurrentOperator.cs
rename to src/core/Deal.Api/Models/CurrentOperator.cs
index 12dd4af..91f6cfa 100644
--- a/src/core/Deal.Api/Http/CurrentOperator.cs
+++ b/src/core/Deal.Api/Models/CurrentOperator.cs
@@ -1,7 +1,8 @@
using Deal.Api.Services;
using Deal.Api.Dtos;
+using Deal.Api.Extensions;
-namespace Deal.Api.Http;
+namespace Deal.Api.Models;
///
/// Текущий оператор запроса — кладёт OperatorSessionMiddleware в HttpContext.Items (Ruling 1).
diff --git a/src/core/Deal.Api/Http/CurrentUser.cs b/src/core/Deal.Api/Models/CurrentUser.cs
similarity index 92%
rename from src/core/Deal.Api/Http/CurrentUser.cs
rename to src/core/Deal.Api/Models/CurrentUser.cs
index cdff07d..86ce773 100644
--- a/src/core/Deal.Api/Http/CurrentUser.cs
+++ b/src/core/Deal.Api/Models/CurrentUser.cs
@@ -1,7 +1,8 @@
using Deal.Api.Services;
using Deal.Api.Dtos;
+using Deal.Api.Extensions;
-namespace Deal.Api.Http;
+namespace Deal.Api.Models;
///
/// Текущий пользователь запроса — кладётся SessionMiddleware в HttpContext.Items.
diff --git a/src/core/Deal.Api/Observability/RuntimeDepthsCollector.cs b/src/core/Deal.Api/Observability/RuntimeDepthsCollector.cs
index e76e474..1d5ae9d 100644
--- a/src/core/Deal.Api/Observability/RuntimeDepthsCollector.cs
+++ b/src/core/Deal.Api/Observability/RuntimeDepthsCollector.cs
@@ -14,7 +14,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.EntityFrameworkCore;
using Deal.Api.Services;
using Deal.Api.Dtos;
diff --git a/src/core/Deal.Api/Program.cs b/src/core/Deal.Api/Program.cs
index 603285a..30c25de 100644
--- a/src/core/Deal.Api/Program.cs
+++ b/src/core/Deal.Api/Program.cs
@@ -8,12 +8,14 @@ using Deal.Api.Configuration;
using Deal.Api.Endpoints;
using Deal.Api.Events;
using Deal.Api.Hosting;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
using Deal.Api.Logging;
using Deal.Api.Middleware;
using Deal.Api.Observability;
using Deal.Api.Telegram;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure;
using Deal.Infrastructure.Data;
using Deal.Infrastructure.Integrations.Abstractions;
@@ -53,7 +55,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
diff --git a/src/core/Deal.Api/Http/AuditAppender.cs b/src/core/Deal.Api/Services/AuditAppender.cs
similarity index 98%
rename from src/core/Deal.Api/Http/AuditAppender.cs
rename to src/core/Deal.Api/Services/AuditAppender.cs
index 185f8ed..b6a6e2d 100644
--- a/src/core/Deal.Api/Http/AuditAppender.cs
+++ b/src/core/Deal.Api/Services/AuditAppender.cs
@@ -6,8 +6,10 @@ using Deal.Modules.Tenants.Application.Services;
using Microsoft.Extensions.DependencyInjection;
using Deal.Api.Services;
using Deal.Api.Dtos;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
-namespace Deal.Api.Http;
+namespace Deal.Api.Services;
///
/// Хелпер записи аудита действий пользователей тенанта и операторов (единая точка — ).
diff --git a/src/core/Deal.Api/Http/EndpointResults.cs b/src/core/Deal.Api/Services/EndpointResults.cs
similarity index 97%
rename from src/core/Deal.Api/Http/EndpointResults.cs
rename to src/core/Deal.Api/Services/EndpointResults.cs
index 246bd3f..2d1678d 100644
--- a/src/core/Deal.Api/Http/EndpointResults.cs
+++ b/src/core/Deal.Api/Services/EndpointResults.cs
@@ -1,7 +1,9 @@
using Deal.Api.Services;
using Deal.Api.Dtos;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
-namespace Deal.Api.Http;
+namespace Deal.Api.Services;
///
/// Общие ответы ошибок минимальных API: HTTP-код + {"detail":"…"} (формат прототипа, Ruling 10).
diff --git a/src/core/Deal.Api/Http/LoginAttemptGuard.cs b/src/core/Deal.Api/Services/LoginAttemptGuard.cs
similarity index 99%
rename from src/core/Deal.Api/Http/LoginAttemptGuard.cs
rename to src/core/Deal.Api/Services/LoginAttemptGuard.cs
index 4f6c75b..d30ba42 100644
--- a/src/core/Deal.Api/Http/LoginAttemptGuard.cs
+++ b/src/core/Deal.Api/Services/LoginAttemptGuard.cs
@@ -6,8 +6,10 @@ using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
using Deal.Api.Services;
using Deal.Api.Dtos;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
-namespace Deal.Api.Http;
+namespace Deal.Api.Services;
///
/// Прикладной guard неудачных попыток входа (план Task 11, Ruling 5; этап 12, пакет B — хранилище на
diff --git a/src/core/Deal.Api/Services/PipelineWorkerScheduler.cs b/src/core/Deal.Api/Services/PipelineWorkerScheduler.cs
index deaf2a3..267b87b 100644
--- a/src/core/Deal.Api/Services/PipelineWorkerScheduler.cs
+++ b/src/core/Deal.Api/Services/PipelineWorkerScheduler.cs
@@ -9,7 +9,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Deal.Api.Dtos;
using Deal.Api;
using Deal.Api.Services;
diff --git a/src/core/Deal.Api/Http/SessionCookieWriter.cs b/src/core/Deal.Api/Services/SessionCookieWriter.cs
similarity index 93%
rename from src/core/Deal.Api/Http/SessionCookieWriter.cs
rename to src/core/Deal.Api/Services/SessionCookieWriter.cs
index cbef70d..0a2ebc2 100644
--- a/src/core/Deal.Api/Http/SessionCookieWriter.cs
+++ b/src/core/Deal.Api/Services/SessionCookieWriter.cs
@@ -3,8 +3,10 @@ using Deal.Api.Dtos;
using AspNetCoreCookieOptions = Microsoft.AspNetCore.Http.CookieOptions;
using CookieOptions = Deal.Api.Configuration.CookieOptions;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
-namespace Deal.Api.Http;
+namespace Deal.Api.Services;
///
/// Единая запись httpOnly-куки сессии тенант-пользователя (login/change-password/impersonation).
diff --git a/src/core/Deal.Api/Http/StoreBackedFixedWindowRateLimiter.cs b/src/core/Deal.Api/Services/StoreBackedFixedWindowRateLimiter.cs
similarity index 97%
rename from src/core/Deal.Api/Http/StoreBackedFixedWindowRateLimiter.cs
rename to src/core/Deal.Api/Services/StoreBackedFixedWindowRateLimiter.cs
index 4f3b3e1..f03932b 100644
--- a/src/core/Deal.Api/Http/StoreBackedFixedWindowRateLimiter.cs
+++ b/src/core/Deal.Api/Services/StoreBackedFixedWindowRateLimiter.cs
@@ -7,8 +7,10 @@ using Deal.Modules.Tenants.Application.Services;
using Microsoft.Extensions.DependencyInjection;
using Deal.Api.Services;
using Deal.Api.Dtos;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
-namespace Deal.Api.Http;
+namespace Deal.Api.Services;
///
/// RateLimiter фиксированного окна с состоянием в Postgres (этап 12, пакет B): лимиты переживают
diff --git a/src/core/Deal.Api/Telegram/IngressRateLimitInterceptor.cs b/src/core/Deal.Api/Telegram/IngressRateLimitInterceptor.cs
index c690d98..263f4a7 100644
--- a/src/core/Deal.Api/Telegram/IngressRateLimitInterceptor.cs
+++ b/src/core/Deal.Api/Telegram/IngressRateLimitInterceptor.cs
@@ -1,110 +1,111 @@
-using System.Threading.RateLimiting;
-using Deal.Api.Http;
-using Deal.Api.Middleware;
-using Grpc.Core;
-using Grpc.Core.Interceptors;
-using Microsoft.Extensions.DependencyInjection;
-using Deal.Api.Services;
-using Deal.Api.Dtos;
-
-namespace Deal.Api.Telegram;
-
-///
-/// Серверный интерцептор rate limit gRPC-ингресса (план Task 11, Ruling 5; этап 12, пакет B — хранилище
-/// на Postgres): фиксированное окно (GrpcIngressPerMinute в минуту) по gRPC-metadata «tenant-id» —
-/// каждый тенант имеет собственный счётчик входящего потока telegram-service, общий для всех инстансов
-/// core. Стандартный grpc.health.v1.Health не ограничивается (инфраструктурный liveness, как у
-/// IngressServiceTokenInterceptor). Превышение лимита — RPC-отказ RESOURCE_EXHAUSTED (gRPC-аналог
-/// HTTP 429) до вызова метода сервиса.
-///
-///
-/// Окно считает общий (регистрируется в DI singleton'ом
-/// рядом с AddGrpc): экземпляры интерцептора создаются фреймворком, но партиции окон живут в одном
-/// лимитере — иначе каждая регистрация/вызов получил бы собственное окно и лимит не работал бы.
-/// Счётчики партиций — в Postgres через . Лимитер
-/// регистрируется и интерцептор добавляется только при RateLimit:Enabled=true (dev — plaintext-поток
-/// без лимита, как HTTP-политики, Ruling 5).
-///
-public sealed class IngressRateLimitInterceptor : Interceptor
-{
- // Ключ партиции вызовов без metadata tenant-id (общий «мусорный» бакет — сервис всё равно отклонит).
- private const string MissingTenantIdKey = "missing-tenant-id";
-
- // Префикс ключа хранилища: изолирует счётчики ингресса от HTTP-политик и login-guard.
- private const string StoreKeyPrefix = "grpc:ingress:";
-
- // Окно политики ингресса — 1 минута (имя настройки — GrpcIngressPerMinute).
- private static readonly TimeSpan Window = TimeSpan.FromMinutes(1);
-
- private readonly PartitionedRateLimiter _limiter;
-
- ///
- /// Создаёт интерцептор с общим лимитером ингресса (см. CreateLimiter).
- ///
- /// Singleton-лимитер, зарегистрированный хостом.
- public IngressRateLimitInterceptor(PartitionedRateLimiter limiter)
- {
- _limiter = limiter ?? throw new ArgumentNullException(nameof(limiter));
- }
-
- ///
- /// Создаёт лимитер ингресса: фиксированное окно в минуту, партиция на каждый tenant-id,
- /// счётчики — в общем хранилище Postgres.
- ///
- /// Фабрика scope: store-backed лимитер резолвит хранилище на каждое приобретение.
- /// Разрешено вызовов на тенанта в минуту (RateLimit:GrpcIngressPerMinute).
- /// Общий PartitionedRateLimiter по строковому tenant-id.
- public static PartitionedRateLimiter CreateLimiter(IServiceScopeFactory scopeFactory, int permitsPerMinute)
- {
- ArgumentNullException.ThrowIfNull(scopeFactory);
- ArgumentOutOfRangeException.ThrowIfLessThan(permitsPerMinute, 1);
- return PartitionedRateLimiter.Create(
- tenantId =>
- {
- string key = StoreKeyPrefix + tenantId;
- return RateLimitPartition.Get(key, storeKey => new StoreBackedFixedWindowRateLimiter(
- scopeFactory,
- storeKey,
- permitsPerMinute,
- Window,
- () => DateTimeOffset.UtcNow));
- });
- }
-
- ///
- /// Проверка лимита для unary-RPC: health-методы пропускаются, остальные получают разрешение
- /// партиции tenant-id; исчерпание окна — RESOURCE_EXHAUSTED.
- ///
- /// Тип запроса gRPC.
- /// Тип ответа gRPC.
- /// Тело запроса.
- /// Контекст вызова (metadata tenant-id/service-token).
- /// Следующий обработчик в цепочке.
- /// Ответ нижестоящего обработчика.
- public override async Task UnaryServerHandler(
- TRequest request,
- ServerCallContext context,
- UnaryServerMethod continuation)
- {
- if (context.Method.StartsWith(IngressServiceTokenInterceptor.HealthMethodPrefix, StringComparison.Ordinal))
- {
- return await continuation(request, context).ConfigureAwait(false);
- }
-
- string? tenantId = context.RequestHeaders.GetValue(TelegramIngressService.TenantIdMetadataKey);
- // permitCount указывается явно: AcquireAsync(TResource, int permitCount, CancellationToken).
- using RateLimitLease lease = await _limiter
- .AcquireAsync(tenantId ?? MissingTenantIdKey, permitCount: 1, context.CancellationToken)
- .ConfigureAwait(false);
- if (!lease.IsAcquired)
- {
- throw Rejection();
- }
-
- return await continuation(request, context).ConfigureAwait(false);
- }
-
- // Создаёт отказ RESOURCE_EXHAUSTED с текстом 429-детали политик (единый detail rate limit).
- private static RpcException Rejection()
- => new(new Status(StatusCode.ResourceExhausted, RateLimitPolicies.RejectedDetail));
-}
+using System.Threading.RateLimiting;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
+using Deal.Api.Middleware;
+using Grpc.Core;
+using Grpc.Core.Interceptors;
+using Microsoft.Extensions.DependencyInjection;
+using Deal.Api.Dtos;
+
+namespace Deal.Api.Telegram;
+
+///
+/// Серверный интерцептор rate limit gRPC-ингресса (план Task 11, Ruling 5; этап 12, пакет B — хранилище
+/// на Postgres): фиксированное окно (GrpcIngressPerMinute в минуту) по gRPC-metadata «tenant-id» —
+/// каждый тенант имеет собственный счётчик входящего потока telegram-service, общий для всех инстансов
+/// core. Стандартный grpc.health.v1.Health не ограничивается (инфраструктурный liveness, как у
+/// IngressServiceTokenInterceptor). Превышение лимита — RPC-отказ RESOURCE_EXHAUSTED (gRPC-аналог
+/// HTTP 429) до вызова метода сервиса.
+///
+///
+/// Окно считает общий (регистрируется в DI singleton'ом
+/// рядом с AddGrpc): экземпляры интерцептора создаются фреймворком, но партиции окон живут в одном
+/// лимитере — иначе каждая регистрация/вызов получил бы собственное окно и лимит не работал бы.
+/// Счётчики партиций — в Postgres через . Лимитер
+/// регистрируется и интерцептор добавляется только при RateLimit:Enabled=true (dev — plaintext-поток
+/// без лимита, как HTTP-политики, Ruling 5).
+///
+public sealed class IngressRateLimitInterceptor : Interceptor
+{
+ // Ключ партиции вызовов без metadata tenant-id (общий «мусорный» бакет — сервис всё равно отклонит).
+ private const string MissingTenantIdKey = "missing-tenant-id";
+
+ // Префикс ключа хранилища: изолирует счётчики ингресса от HTTP-политик и login-guard.
+ private const string StoreKeyPrefix = "grpc:ingress:";
+
+ // Окно политики ингресса — 1 минута (имя настройки — GrpcIngressPerMinute).
+ private static readonly TimeSpan Window = TimeSpan.FromMinutes(1);
+
+ private readonly PartitionedRateLimiter _limiter;
+
+ ///
+ /// Создаёт интерцептор с общим лимитером ингресса (см. CreateLimiter).
+ ///
+ /// Singleton-лимитер, зарегистрированный хостом.
+ public IngressRateLimitInterceptor(PartitionedRateLimiter limiter)
+ {
+ _limiter = limiter ?? throw new ArgumentNullException(nameof(limiter));
+ }
+
+ ///
+ /// Создаёт лимитер ингресса: фиксированное окно в минуту, партиция на каждый tenant-id,
+ /// счётчики — в общем хранилище Postgres.
+ ///
+ /// Фабрика scope: store-backed лимитер резолвит хранилище на каждое приобретение.
+ /// Разрешено вызовов на тенанта в минуту (RateLimit:GrpcIngressPerMinute).
+ /// Общий PartitionedRateLimiter по строковому tenant-id.
+ public static PartitionedRateLimiter CreateLimiter(IServiceScopeFactory scopeFactory, int permitsPerMinute)
+ {
+ ArgumentNullException.ThrowIfNull(scopeFactory);
+ ArgumentOutOfRangeException.ThrowIfLessThan(permitsPerMinute, 1);
+ return PartitionedRateLimiter.Create(
+ tenantId =>
+ {
+ string key = StoreKeyPrefix + tenantId;
+ return RateLimitPartition.Get(key, storeKey => new StoreBackedFixedWindowRateLimiter(
+ scopeFactory,
+ storeKey,
+ permitsPerMinute,
+ Window,
+ () => DateTimeOffset.UtcNow));
+ });
+ }
+
+ ///
+ /// Проверка лимита для unary-RPC: health-методы пропускаются, остальные получают разрешение
+ /// партиции tenant-id; исчерпание окна — RESOURCE_EXHAUSTED.
+ ///
+ /// Тип запроса gRPC.
+ /// Тип ответа gRPC.
+ /// Тело запроса.
+ /// Контекст вызова (metadata tenant-id/service-token).
+ /// Следующий обработчик в цепочке.
+ /// Ответ нижестоящего обработчика.
+ public override async Task UnaryServerHandler(
+ TRequest request,
+ ServerCallContext context,
+ UnaryServerMethod continuation)
+ {
+ if (context.Method.StartsWith(IngressServiceTokenInterceptor.HealthMethodPrefix, StringComparison.Ordinal))
+ {
+ return await continuation(request, context).ConfigureAwait(false);
+ }
+
+ string? tenantId = context.RequestHeaders.GetValue(TelegramIngressService.TenantIdMetadataKey);
+ // permitCount указывается явно: AcquireAsync(TResource, int permitCount, CancellationToken).
+ using RateLimitLease lease = await _limiter
+ .AcquireAsync(tenantId ?? MissingTenantIdKey, permitCount: 1, context.CancellationToken)
+ .ConfigureAwait(false);
+ if (!lease.IsAcquired)
+ {
+ throw Rejection();
+ }
+
+ return await continuation(request, context).ConfigureAwait(false);
+ }
+
+ // Создаёт отказ RESOURCE_EXHAUSTED с текстом 429-детали политик (единый detail rate limit).
+ private static RpcException Rejection()
+ => new(new Status(StatusCode.ResourceExhausted, RateLimitPolicies.RejectedDetail));
+}
diff --git a/src/core/Deal.Api/Telegram/TelegramIngressService.cs b/src/core/Deal.Api/Telegram/TelegramIngressService.cs
index e327372..2705d1c 100644
--- a/src/core/Deal.Api/Telegram/TelegramIngressService.cs
+++ b/src/core/Deal.Api/Telegram/TelegramIngressService.cs
@@ -16,7 +16,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Grpc.Core;
using Deal.Api.Services;
using Deal.Api.Dtos;
diff --git a/src/core/Deal.Api/Telegram/TgStatusService.cs b/src/core/Deal.Api/Telegram/TgStatusService.cs
index 08d96c6..1380afb 100644
--- a/src/core/Deal.Api/Telegram/TgStatusService.cs
+++ b/src/core/Deal.Api/Telegram/TgStatusService.cs
@@ -1,5 +1,5 @@
using System.Text.Json;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Settings.Application.Abstractions;
using Deal.Modules.Settings.Application.Models;
diff --git a/src/core/Deal.Contracts/Integrations/IAiClassifier.cs b/src/core/Deal.Contracts/Integrations/Abstractions/IAiClassifier.cs
similarity index 98%
rename from src/core/Deal.Contracts/Integrations/IAiClassifier.cs
rename to src/core/Deal.Contracts/Integrations/Abstractions/IAiClassifier.cs
index 2293e63..8c61e00 100644
--- a/src/core/Deal.Contracts/Integrations/IAiClassifier.cs
+++ b/src/core/Deal.Contracts/Integrations/Abstractions/IAiClassifier.cs
@@ -1,6 +1,6 @@
using Deal.Contracts.Integrations.Models;
-namespace Deal.Contracts.Integrations;
+namespace Deal.Contracts.Integrations.Abstractions;
///
/// Порт ИИ-классификатора входящих сообщений (Ruling 5, план Task 6 L370–388).
diff --git a/src/core/Deal.Contracts/Integrations/IAiTools.cs b/src/core/Deal.Contracts/Integrations/Abstractions/IAiTools.cs
similarity index 98%
rename from src/core/Deal.Contracts/Integrations/IAiTools.cs
rename to src/core/Deal.Contracts/Integrations/Abstractions/IAiTools.cs
index 72ae040..1c685ce 100644
--- a/src/core/Deal.Contracts/Integrations/IAiTools.cs
+++ b/src/core/Deal.Contracts/Integrations/Abstractions/IAiTools.cs
@@ -1,6 +1,6 @@
using Deal.Contracts.Integrations.Models;
-namespace Deal.Contracts.Integrations;
+namespace Deal.Contracts.Integrations.Abstractions;
///
/// Порт ИИ-инструментов Discovery и генерации ключевых слов (Ruling 9, план Task 15/18/19).
diff --git a/src/core/Deal.Contracts/Integrations/IColumnSuggester.cs b/src/core/Deal.Contracts/Integrations/Abstractions/IColumnSuggester.cs
similarity index 98%
rename from src/core/Deal.Contracts/Integrations/IColumnSuggester.cs
rename to src/core/Deal.Contracts/Integrations/Abstractions/IColumnSuggester.cs
index df8b846..c57b2c1 100644
--- a/src/core/Deal.Contracts/Integrations/IColumnSuggester.cs
+++ b/src/core/Deal.Contracts/Integrations/Abstractions/IColumnSuggester.cs
@@ -1,6 +1,6 @@
using Deal.Contracts.Integrations.Models;
-namespace Deal.Contracts.Integrations;
+namespace Deal.Contracts.Integrations.Abstractions;
///
/// Порт ИИ-предложений колонок и ключей — план Task 14 (L462–491), Ruling 3.
diff --git a/src/core/Deal.Contracts/Integrations/IFileStorage.cs b/src/core/Deal.Contracts/Integrations/Abstractions/IFileStorage.cs
similarity index 99%
rename from src/core/Deal.Contracts/Integrations/IFileStorage.cs
rename to src/core/Deal.Contracts/Integrations/Abstractions/IFileStorage.cs
index f4fb07c..06c3fc7 100644
--- a/src/core/Deal.Contracts/Integrations/IFileStorage.cs
+++ b/src/core/Deal.Contracts/Integrations/Abstractions/IFileStorage.cs
@@ -1,6 +1,6 @@
using Deal.Contracts.Integrations.Models;
-namespace Deal.Contracts.Integrations;
+namespace Deal.Contracts.Integrations.Abstractions;
///
/// Порт файлового хранилища вложений (Ruling 4, план Task 6; 1:1 backend/app/services/object_store.py L61–107).
diff --git a/src/core/Deal.Contracts/Integrations/IMlClient.cs b/src/core/Deal.Contracts/Integrations/Abstractions/IMlClient.cs
similarity index 98%
rename from src/core/Deal.Contracts/Integrations/IMlClient.cs
rename to src/core/Deal.Contracts/Integrations/Abstractions/IMlClient.cs
index a10e4ba..f837901 100644
--- a/src/core/Deal.Contracts/Integrations/IMlClient.cs
+++ b/src/core/Deal.Contracts/Integrations/Abstractions/IMlClient.cs
@@ -1,6 +1,6 @@
using Deal.Contracts.Integrations.Models;
-namespace Deal.Contracts.Integrations;
+namespace Deal.Contracts.Integrations.Abstractions;
///
/// Порт клиента автономного ML-сервиса (Ruling 4, план Task 9 L333–367).
diff --git a/src/core/Deal.Contracts/Integrations/ITelegramGateway.cs b/src/core/Deal.Contracts/Integrations/Abstractions/ITelegramGateway.cs
similarity index 99%
rename from src/core/Deal.Contracts/Integrations/ITelegramGateway.cs
rename to src/core/Deal.Contracts/Integrations/Abstractions/ITelegramGateway.cs
index 795326f..dc979b1 100644
--- a/src/core/Deal.Contracts/Integrations/ITelegramGateway.cs
+++ b/src/core/Deal.Contracts/Integrations/Abstractions/ITelegramGateway.cs
@@ -1,6 +1,6 @@
using Deal.Contracts.Integrations.Models;
-namespace Deal.Contracts.Integrations;
+namespace Deal.Contracts.Integrations.Abstractions;
///
/// Порт-гейт к автономному telegram-service: команды ядра наружу (Ruling 7, план Task 13/14/18).
diff --git a/src/core/Deal.Contracts/Integrations/Models/FileMeta.cs b/src/core/Deal.Contracts/Integrations/Models/FileMeta.cs
index 9f39919..663e084 100644
--- a/src/core/Deal.Contracts/Integrations/Models/FileMeta.cs
+++ b/src/core/Deal.Contracts/Integrations/Models/FileMeta.cs
@@ -4,7 +4,7 @@ namespace Deal.Contracts.Integrations.Models;
/// Дескриптор объекта файлового хранилища: ключ, размер и MIME-тип (Ruling 4, Task 6).
///
///
-/// Тип-описатель порта : в нём хранилище отдаёт
+/// Тип-описатель порта : в нём хранилище отдаёт
/// метаданные объекта (аналог stat object_store.py — MinIO StatObject / размер файла на диске) методом
/// StatAsync. Потребитель — download-эндпоинт файлов проектных карточек (Task 9, Ruling T6):
/// Content-Length/Content-Type ответа берутся из дескриптора, а null (объекта нет) мапится в 404
diff --git a/src/core/Deal.Contracts/Integrations/MlLearningLabels.cs b/src/core/Deal.Contracts/Integrations/Models/MlLearningLabels.cs
similarity index 94%
rename from src/core/Deal.Contracts/Integrations/MlLearningLabels.cs
rename to src/core/Deal.Contracts/Integrations/Models/MlLearningLabels.cs
index 895f85a..348d8e1 100644
--- a/src/core/Deal.Contracts/Integrations/MlLearningLabels.cs
+++ b/src/core/Deal.Contracts/Integrations/Models/MlLearningLabels.cs
@@ -1,4 +1,6 @@
-namespace Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+
+namespace Deal.Contracts.Integrations.Models;
///
/// Общие метки обучения ML-модели (значения 1:1 с ml_client.py). Единый источник для всех модулей,
diff --git a/src/core/Deal.Contracts/Integrations/Models/MlStatusResponseDto.cs b/src/core/Deal.Contracts/Integrations/Models/MlStatusResponseDto.cs
index 879fca0..e2e73eb 100644
--- a/src/core/Deal.Contracts/Integrations/Models/MlStatusResponseDto.cs
+++ b/src/core/Deal.Contracts/Integrations/Models/MlStatusResponseDto.cs
@@ -4,7 +4,7 @@ namespace Deal.Contracts.Integrations.Models;
/// Тело ответа GET /api/ml/status (ml_routes.py L66–75 + ml_client.snapshot() L138–150).
///
///
-/// Собирается реализацией из статуса
+/// Собирается реализацией из статуса
/// ML-сервиса () и локальных настроек/счётчиков тенанта (KV, Ruling 1):
/// enabled — выключатель mlEnabled (семантика «не false», ml_routes.py L71), reachable —
/// доступность сервиса, stats — локальная статистика. Фронт читает: reachable,
diff --git a/src/core/Deal.Contracts/Integrations/SourceDefaults.cs b/src/core/Deal.Contracts/Integrations/Models/SourceDefaults.cs
similarity index 89%
rename from src/core/Deal.Contracts/Integrations/SourceDefaults.cs
rename to src/core/Deal.Contracts/Integrations/Models/SourceDefaults.cs
index 7e4fe83..b01da82 100644
--- a/src/core/Deal.Contracts/Integrations/SourceDefaults.cs
+++ b/src/core/Deal.Contracts/Integrations/Models/SourceDefaults.cs
@@ -1,4 +1,6 @@
-namespace Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+
+namespace Deal.Contracts.Integrations.Models;
///
/// Общие дефолты источника (канал/группа/кандидат), 1:1 с db.py (hue по умолчанию — «#666»).
diff --git a/src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs b/src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs
index b8ae072..e263477 100644
--- a/src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs
+++ b/src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs
@@ -1,4 +1,5 @@
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.Configuration;
namespace Deal.Infrastructure.Data;
diff --git a/src/core/Deal.Infrastructure/Data/TenantContext.cs b/src/core/Deal.Infrastructure/Data/TenantContext.cs
index 2abd0ec..3250384 100644
--- a/src/core/Deal.Infrastructure/Data/TenantContext.cs
+++ b/src/core/Deal.Infrastructure/Data/TenantContext.cs
@@ -1,4 +1,5 @@
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
namespace Deal.Infrastructure.Data;
diff --git a/src/core/Deal.Infrastructure/Integrations/Abstractions/IMlTrainClient.cs b/src/core/Deal.Infrastructure/Integrations/Abstractions/IMlTrainClient.cs
index 3092080..9482755 100644
--- a/src/core/Deal.Infrastructure/Integrations/Abstractions/IMlTrainClient.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Abstractions/IMlTrainClient.cs
@@ -11,7 +11,7 @@ namespace Deal.Infrastructure.Integrations.Abstractions;
/// Порт отправки батча обучения в ml-service (RPC TrainBatch, ml.proto L52–55) — для MlOutboxFlushScheduler.
///
///
-/// Отдельный от порт: сигнатура IMlClient не меняется
+/// Отдельный от порт: сигнатура IMlClient не меняется
/// (Self-Review плана L530), а выгрузку очереди делает фоновый флашер (Ruling 6), которому нужен только
/// TrainBatch. Реализуется gRPC-адаптером и регистрируется только при
/// Services:Ml:UseLocal=false (Local-режиму ml-service не нужен — очередь копится, как в этапе 3).
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs
index 5dc10fc..e5dc35e 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs
@@ -1,11 +1,12 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.Logging;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs
index 03df614..c7c1057 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs
@@ -1,11 +1,12 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.Logging;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs
index 9e9953f..e8ef5da 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs
@@ -1,11 +1,12 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Grpc.Ai;
using Deal.Modules.Pipeline.Application.Abstractions;
using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Registrars;
using Deal.Modules.Pipeline.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using Deal.Infrastructure.Integrations.Abstractions;
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs
index a662353..f0aa4b4 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs
@@ -1,7 +1,8 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Grpc.Ai;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using Deal.Infrastructure.Integrations.Abstractions;
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs
index 8801dbf..a30e78f 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs
@@ -1,5 +1,5 @@
using System.Text.Json;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Grpc.Ml;
using Deal.Modules.Kanban.Application.Abstractions;
@@ -16,7 +16,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using Deal.Infrastructure.Integrations.Abstractions;
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs
index 31eeb4d..10b5bee 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs
@@ -1,7 +1,8 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Grpc.Telegram;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using Deal.Infrastructure.Integrations.Abstractions;
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalAiClassifier.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalAiClassifier.cs
index 980bb74..f8b4bd7 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/LocalAiClassifier.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalAiClassifier.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Pipeline.Application.Abstractions;
using Deal.Modules.Pipeline.Application.Models;
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalAiTools.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalAiTools.cs
index fa8d7f1..2dde240 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/LocalAiTools.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalAiTools.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs
index 5040b0b..6f4d045 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs
@@ -1,6 +1,6 @@
using System.Globalization;
using System.Text.Json;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Cards.Application.Abstractions;
using Deal.Modules.Cards.Application.Dtos;
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalMlClient.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalMlClient.cs
index 980b1a9..ff04a28 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/LocalMlClient.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalMlClient.cs
@@ -1,5 +1,5 @@
using System.Text.Json;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalTelegramGateway.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalTelegramGateway.cs
index 003da3a..0230bce 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/LocalTelegramGateway.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalTelegramGateway.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/TokenUsageRecorder.cs b/src/core/Deal.Infrastructure/Integrations/Services/TokenUsageRecorder.cs
index 3420c0b..d28113d 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/TokenUsageRecorder.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/TokenUsageRecorder.cs
@@ -11,7 +11,8 @@ using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
using Deal.SharedKernel.Observability;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
using Deal.Infrastructure.Integrations.Extensions;
diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs
index 131d29d..75845e3 100644
--- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs
@@ -1,4 +1,5 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs
index 45fb7e5..2a86a5f 100644
--- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Integrations.Storage.Extensions;
using Deal.Infrastructure.Integrations.Storage.Options;
diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs
index 5865665..45e425b 100644
--- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Microsoft.Extensions.Logging;
using Minio;
diff --git a/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs b/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs
index fc50b79..a29c720 100644
--- a/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs
+++ b/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
using Deal.Infrastructure.Integrations.Extensions;
@@ -39,7 +40,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
diff --git a/src/core/Deal.Infrastructure/Tenancy/TenantProvisioningService.cs b/src/core/Deal.Infrastructure/Tenancy/TenantProvisioningService.cs
index c6cfc51..e83f2dc 100644
--- a/src/core/Deal.Infrastructure/Tenancy/TenantProvisioningService.cs
+++ b/src/core/Deal.Infrastructure/Tenancy/TenantProvisioningService.cs
@@ -8,7 +8,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.EntityFrameworkCore;
using Npgsql;
diff --git a/src/core/Deal.Infrastructure/Tenancy/TenantSchemaMigrationService.cs b/src/core/Deal.Infrastructure/Tenancy/TenantSchemaMigrationService.cs
index 3887445..cf55d42 100644
--- a/src/core/Deal.Infrastructure/Tenancy/TenantSchemaMigrationService.cs
+++ b/src/core/Deal.Infrastructure/Tenancy/TenantSchemaMigrationService.cs
@@ -5,7 +5,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.Logging;
namespace Deal.Infrastructure.Tenancy;
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs
index 98b4beb..fb6293a 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs
@@ -1,4 +1,5 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Modules.Discovery.Application.Models;
using Deal.Modules.Discovery.Application.Abstractions;
using Deal.Modules.Discovery.Application.Exceptions;
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryEvaluator.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryEvaluator.cs
index 8d50c2f..5ea4f45 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryEvaluator.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryEvaluator.cs
@@ -1,337 +1,337 @@
-using System.Text;
-using Deal.Contracts.Integrations;
-using Deal.Contracts.Integrations.Models;
-using Deal.Modules.Discovery.Application.Models;
-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.Registrars;
-
-namespace Deal.Modules.Discovery.Application.Services;
-
-///
-/// Оценка содержания кандидата: фит сообщений под задачу поиска (1:1 discovery_eval.py целиком, план Task 18).
-///
-///
-/// Каскад оценки одного сообщения (python evaluate_message L174–194):
-///
-/// - текст пустой/короче 10 символов → «слишком короткое» (False, эвристика);
-/// - ML при mlEnabled (): take + label=spam → «ML: спам» (False);
-/// сбой прогноза не роняет оценку (python L101–107: «не уверен»);
-/// - ИИ при aiEnabled (): любая ошибка (Local — NotSupportedException,
-/// gRPC — AiUnavailableException/…, Ruling 10) ловится и оценка продолжается эвристикой (python L186–194);
-/// - эвристика: любой ключ задачи входит в очищенный текст без учёта регистра.
-///
-/// — группировка выборки форума по topic_id (null → «main») с заголовками-сниппетами
-/// (python L86–117); — вердикт «источник подходит»: выборка ≥3 сообщений и доля fit ≥
-/// threshold, % (python L229–237). Результаты по сообщениям собирает
-/// в агрегат (fit_count/total/fit_ratio,
-/// python L197–226). Язык выборки — отдельный детектор (зовёт воркер).
-///
-public sealed class DiscoveryEvaluator
-{
- // Минимальная длина сообщения для содержательной оценки (python _MIN_TEXT_LEN L39).
- private const int MinTextLength = 10;
-
- // Лимит текста, уходящего ИИ-провайдеру, в символах (python _AI_TEXT_LIMIT L41).
- private const int AiTextLimit = 4000;
-
- // Потолок причины из ИИ (python _AI_REASON_LIMIT L43).
- private const int AiReasonLimit = 200;
-
- // Минимальный объём содержательной выборки для вердикта оценки (python _MIN_CONTENT L77).
- private const int MinContentMessages = 3;
-
- // topic_id=null в выборке/группировке → общая тема «main» (python _MAIN_TOPIC L47).
- private const string MainTopic = "main";
-
- // Длина заголовка темы-сниппета (python _TITLE_LIMIT L45).
- private const int TopicTitleLimit = 60;
-
- // Причина по умолчанию при фите ИИ (python _ai_reason L170).
- private const string AiFitReasonDefault = "подходит";
-
- // Причина по умолчанию при не-фите ИИ (python _ai_reason L170).
- private const string AiNotFitReasonDefault = "не подходит";
-
- private readonly ISettingsStore _settings;
- private readonly IMlClient _mlClient;
- private readonly IAiTools _aiTools;
-
- ///
- /// Создаёт оценщик содержания кандидатов Discovery.
- ///
- /// KV-настройки тенанта (mlEnabled/aiEnabled — ветки каскада).
- /// ML-порт (спам-отсев при mlEnabled; сбой — «не уверен»).
- /// ИИ-порт (фит при aiEnabled; сбой — эвристика, Ruling 10).
- public DiscoveryEvaluator(
- ISettingsStore settings,
- IMlClient mlClient,
- IAiTools aiTools)
- {
- ArgumentNullException.ThrowIfNull(settings);
- ArgumentNullException.ThrowIfNull(mlClient);
- ArgumentNullException.ThrowIfNull(aiTools);
- _settings = settings;
- _mlClient = mlClient;
- _aiTools = aiTools;
- }
-
- ///
- /// Последовательная оценка всех сообщений выборки (python evaluate_sample L197–226).
- ///
- /// fit_count/total/fit_ratio считаются по всей выборке; per-message повторяет входной порядок
- /// (воркеру нужны только агрегаты; per-message — для тестов и будущих разборов).
- /// Задача поиска (description/keywords — промпт и эвристика).
- /// Тексты сообщений выборки (порядок — входной).
- /// Токен отмены.
- /// Агрегат выборки: fit_count, total, fit_ratio, per-message.
- public async Task EvaluateSampleAsync(
- DiscoveryTaskDto task,
- IReadOnlyList texts,
- CancellationToken ct)
- {
- // Флаги веток (mlEnabled/aiEnabled) не меняются в пределах выборки: один типизированный снимок
- // настроек на выборку (C30) вместо двух GetAsync на каждое сообщение.
- TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct).ConfigureAwait(false);
- var perMessage = new List(texts?.Count ?? 0);
- int fitCount = 0;
- foreach (string text in texts ?? [])
- {
- DiscoveryMessageFit fit =
- await EvaluateMessageCoreAsync(task, text, settingsSnapshot, ct).ConfigureAwait(false);
- perMessage.Add(fit);
- if (fit.Fit)
- {
- fitCount++;
- }
- }
-
- int total = perMessage.Count;
- return new DiscoveryEvalSample(
- fitCount,
- total,
- total > 0 ? (double)fitCount / total : 0.0,
- perMessage);
- }
-
- ///
- /// Оценка фита одного сообщения под задачу: каскад короткое → ML-спам → ИИ → эвристика (python L174–194).
- ///
- /// Задача поиска (description/keywords).
- /// Текст сообщения.
- /// Токен отмены.
- /// Вердикт: fit + причина + источник (heuristic|ml|ai).
- public async Task EvaluateMessageAsync(
- DiscoveryTaskDto task,
- string text,
- CancellationToken ct)
- {
- TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct).ConfigureAwait(false);
- return await EvaluateMessageCoreAsync(task, text, settingsSnapshot, ct).ConfigureAwait(false);
- }
-
- // Чистое ядро оценки одного сообщения по фиксированному снимку настроек (см. EvaluateMessageAsync).
- // task: Задача поиска (description/keywords).
- // text: Текст сообщения.
- // settingsSnapshot: Снимок настроек тенанта (mlEnabled/aiEnabled).
- // ct: Токен отмены.
- // Возвращает: Вердикт: fit + причина + источник (heuristic|ml|ai).
- private async Task EvaluateMessageCoreAsync(
- DiscoveryTaskDto task,
- string text,
- TenantSettingsSnapshot settingsSnapshot,
- CancellationToken ct)
- {
- string raw = text ?? string.Empty;
- if (raw.Trim().Length < MinTextLength)
- {
- return new DiscoveryMessageFit(false, "слишком короткое", "heuristic");
- }
-
- if (settingsSnapshot.GetBool(SettingsKeys.MlEnabled, SettingsDefaults.MlEnabled))
- {
- try
- {
- MlPredictResultDto prediction = await _mlClient.PredictAsync(raw, ct).ConfigureAwait(false);
- if (prediction.Take && string.Equals(prediction.Label, MlLearningLabels.Spam, StringComparison.OrdinalIgnoreCase))
- {
- return new DiscoveryMessageFit(false, "ML: спам", "ml");
- }
- }
- catch (Exception exception) when (exception is not OperationCanceledException)
- {
- // Прогноз недоступен («не уверен», python ml_client.predict L101–107) — решает ИИ/эвристика.
- }
- }
-
- if (settingsSnapshot.GetBool(SettingsKeys.AiEnabled, SettingsDefaults.AiEnabled))
- {
- try
- {
- AiEvaluateFitResultDto result = await _aiTools.EvaluateFitAsync(
- SliceCodePoints(raw, AiTextLimit),
- task.Description ?? string.Empty,
- task.Keywords,
- ct).ConfigureAwait(false);
- string reason = (result.Reason ?? string.Empty).Trim();
- if (reason.Length == 0)
- {
- reason = result.Fit ? AiFitReasonDefault : AiNotFitReasonDefault;
- }
-
- return new DiscoveryMessageFit(result.Fit, SliceCodePoints(reason, AiReasonLimit), "ai");
- }
- catch (Exception exception) when (exception is not OperationCanceledException)
- {
- // Сбой ИИ (нет ключа/сеть/не-JSON; Local — NotSupportedException) не роняет оценку (python L191–192).
- }
- }
-
- return Heuristic(task, raw);
- }
-
- // Эвристика: ключ задачи входит в очищенный текст без учёта регистра (python _heuristic L144–150).
- private static DiscoveryMessageFit Heuristic(DiscoveryTaskDto task, string text)
- {
- string haystack = CleanShort(text);
- foreach (string keyword in task.Keywords)
- {
- string kw = (keyword ?? string.Empty).Trim();
- if (kw.Length > 0 && haystack.Contains(kw.ToLowerInvariant(), StringComparison.Ordinal))
- {
- return new DiscoveryMessageFit(true, $"совпал ключ \"{kw}\"", "heuristic");
- }
- }
-
- return new DiscoveryMessageFit(false, "нет совпадений с ключами", "heuristic");
- }
-
- ///
- /// Вердикт «источник подходит»: выборка ≥3 сообщений и доля fit ≥ threshold, % (python passed L229–237).
- ///
- /// Агрегат оценки выборки/темы.
- /// Порог задачи (1..100; дефолт discEvalThreshold).
- /// True — источник/тема проходят порог (каналы часто разбавляют контент офтопом — доля, не сплошное).
- public static bool Passed(DiscoveryEvalSample sample, int thresholdPercent)
- {
- return sample.Total >= MinContentMessages && sample.FitRatio * 100 >= Math.Max(1, thresholdPercent);
- }
-
- ///
- /// Группирует сообщения выборки по topic_id (null → «main») и сортирует группы по числу сообщений
- /// (убыв.), порядок сообщений внутри группы — входной (python group_by_topic L96–117).
- ///
- /// Заголовок группы — сниппет первого непустого текста (python _topic_title L86–93, ≤60 символов,
- /// whitespace схлопнут). Возвращаемые группы переиспользуют исходные DTO (без копий).
- /// Сообщения выборки (для форумов заполнен TopicId).
- /// Группы от большей к меньшей; пусто — выборки нет.
- public static IReadOnlyList GroupByTopic(IReadOnlyList messages)
- {
- var groups = new Dictionary>(StringComparer.Ordinal);
- var order = new List();
- foreach (TelegramEvalMessageDto message in messages ?? [])
- {
- string key = message.TopicId is long topicId ? topicId.ToString() : MainTopic;
- if (!groups.TryGetValue(key, out List? bucket))
- {
- bucket = [];
- groups[key] = bucket;
- order.Add(key);
- }
-
- bucket.Add(message);
- }
-
- var outGroups = new List(order.Count);
- foreach (string key in order)
- {
- List bucket = groups[key];
- outGroups.Add(new DiscoveryTopicGroup(key, TopicTitle(bucket), bucket));
- }
-
- // python L116: sort(key=len(messages), reverse=True) — стабильная сортировка сохраняет входной порядок равных.
- return outGroups
- .OrderByDescending(group => group.Messages.Count)
- .ToList();
- }
-
- // Сниппет первого непустого текста темы (python _topic_title L86–93).
- private static string TopicTitle(IReadOnlyList messages)
- {
- foreach (TelegramEvalMessageDto message in messages)
- {
- string text = (message.Text ?? string.Empty).Trim();
- if (text.Length > 0)
- {
- return SliceCodePoints(CollapseWhitespace(text), TopicTitleLimit);
- }
- }
-
- return string.Empty;
- }
-
- // Очистка текста для эвристики: схлопывание whitespace (python clean_short ≈ «\n+»→« » + trim).
- private static string CleanShort(string text)
- {
- string collapsed = CollapseWhitespace(text);
- return collapsed.ToLowerInvariant();
- }
-
- // Схлопывает любые пробельные последовательности в один пробел и обрезает края (python " ".join(split())).
- private static string CollapseWhitespace(string text)
- {
- var builder = new StringBuilder(text.Length);
- bool pendingSpace = false;
- foreach (char ch in text)
- {
- if (char.IsWhiteSpace(ch))
- {
- pendingSpace = builder.Length > 0;
- continue;
- }
-
- if (pendingSpace)
- {
- builder.Append(' ');
- pendingSpace = false;
- }
-
- builder.Append(ch);
- }
-
- return builder.ToString();
- }
-
- // Первые max кодовых точек строки (python-срез без разрыва суррогатных пар).
- private static string SliceCodePoints(string text, int max)
- {
- if (text.Length <= max)
- {
- return text;
- }
-
- var builder = new StringBuilder(max);
- int taken = 0;
- for (int index = 0; index < text.Length && taken < max; index++)
- {
- bool pair = char.IsHighSurrogate(text[index])
- && index + 1 < text.Length
- && char.IsLowSurrogate(text[index + 1]);
- builder.Append(text[index]);
- if (pair)
- {
- index++;
- builder.Append(text[index]);
- }
-
- taken++;
- }
-
- return builder.ToString();
- }
-}
+using System.Text;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
+using Deal.Modules.Discovery.Application.Models;
+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.Registrars;
+
+namespace Deal.Modules.Discovery.Application.Services;
+
+///
+/// Оценка содержания кандидата: фит сообщений под задачу поиска (1:1 discovery_eval.py целиком, план Task 18).
+///
+///
+/// Каскад оценки одного сообщения (python evaluate_message L174–194):
+///
+/// - текст пустой/короче 10 символов → «слишком короткое» (False, эвристика);
+/// - ML при mlEnabled (): take + label=spam → «ML: спам» (False);
+/// сбой прогноза не роняет оценку (python L101–107: «не уверен»);
+/// - ИИ при aiEnabled (): любая ошибка (Local — NotSupportedException,
+/// gRPC — AiUnavailableException/…, Ruling 10) ловится и оценка продолжается эвристикой (python L186–194);
+/// - эвристика: любой ключ задачи входит в очищенный текст без учёта регистра.
+///
+/// — группировка выборки форума по topic_id (null → «main») с заголовками-сниппетами
+/// (python L86–117); — вердикт «источник подходит»: выборка ≥3 сообщений и доля fit ≥
+/// threshold, % (python L229–237). Результаты по сообщениям собирает
+/// в агрегат (fit_count/total/fit_ratio,
+/// python L197–226). Язык выборки — отдельный детектор (зовёт воркер).
+///
+public sealed class DiscoveryEvaluator
+{
+ // Минимальная длина сообщения для содержательной оценки (python _MIN_TEXT_LEN L39).
+ private const int MinTextLength = 10;
+
+ // Лимит текста, уходящего ИИ-провайдеру, в символах (python _AI_TEXT_LIMIT L41).
+ private const int AiTextLimit = 4000;
+
+ // Потолок причины из ИИ (python _AI_REASON_LIMIT L43).
+ private const int AiReasonLimit = 200;
+
+ // Минимальный объём содержательной выборки для вердикта оценки (python _MIN_CONTENT L77).
+ private const int MinContentMessages = 3;
+
+ // topic_id=null в выборке/группировке → общая тема «main» (python _MAIN_TOPIC L47).
+ private const string MainTopic = "main";
+
+ // Длина заголовка темы-сниппета (python _TITLE_LIMIT L45).
+ private const int TopicTitleLimit = 60;
+
+ // Причина по умолчанию при фите ИИ (python _ai_reason L170).
+ private const string AiFitReasonDefault = "подходит";
+
+ // Причина по умолчанию при не-фите ИИ (python _ai_reason L170).
+ private const string AiNotFitReasonDefault = "не подходит";
+
+ private readonly ISettingsStore _settings;
+ private readonly IMlClient _mlClient;
+ private readonly IAiTools _aiTools;
+
+ ///
+ /// Создаёт оценщик содержания кандидатов Discovery.
+ ///
+ /// KV-настройки тенанта (mlEnabled/aiEnabled — ветки каскада).
+ /// ML-порт (спам-отсев при mlEnabled; сбой — «не уверен»).
+ /// ИИ-порт (фит при aiEnabled; сбой — эвристика, Ruling 10).
+ public DiscoveryEvaluator(
+ ISettingsStore settings,
+ IMlClient mlClient,
+ IAiTools aiTools)
+ {
+ ArgumentNullException.ThrowIfNull(settings);
+ ArgumentNullException.ThrowIfNull(mlClient);
+ ArgumentNullException.ThrowIfNull(aiTools);
+ _settings = settings;
+ _mlClient = mlClient;
+ _aiTools = aiTools;
+ }
+
+ ///
+ /// Последовательная оценка всех сообщений выборки (python evaluate_sample L197–226).
+ ///
+ /// fit_count/total/fit_ratio считаются по всей выборке; per-message повторяет входной порядок
+ /// (воркеру нужны только агрегаты; per-message — для тестов и будущих разборов).
+ /// Задача поиска (description/keywords — промпт и эвристика).
+ /// Тексты сообщений выборки (порядок — входной).
+ /// Токен отмены.
+ /// Агрегат выборки: fit_count, total, fit_ratio, per-message.
+ public async Task EvaluateSampleAsync(
+ DiscoveryTaskDto task,
+ IReadOnlyList texts,
+ CancellationToken ct)
+ {
+ // Флаги веток (mlEnabled/aiEnabled) не меняются в пределах выборки: один типизированный снимок
+ // настроек на выборку (C30) вместо двух GetAsync на каждое сообщение.
+ TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct).ConfigureAwait(false);
+ var perMessage = new List(texts?.Count ?? 0);
+ int fitCount = 0;
+ foreach (string text in texts ?? [])
+ {
+ DiscoveryMessageFit fit =
+ await EvaluateMessageCoreAsync(task, text, settingsSnapshot, ct).ConfigureAwait(false);
+ perMessage.Add(fit);
+ if (fit.Fit)
+ {
+ fitCount++;
+ }
+ }
+
+ int total = perMessage.Count;
+ return new DiscoveryEvalSample(
+ fitCount,
+ total,
+ total > 0 ? (double)fitCount / total : 0.0,
+ perMessage);
+ }
+
+ ///
+ /// Оценка фита одного сообщения под задачу: каскад короткое → ML-спам → ИИ → эвристика (python L174–194).
+ ///
+ /// Задача поиска (description/keywords).
+ /// Текст сообщения.
+ /// Токен отмены.
+ /// Вердикт: fit + причина + источник (heuristic|ml|ai).
+ public async Task EvaluateMessageAsync(
+ DiscoveryTaskDto task,
+ string text,
+ CancellationToken ct)
+ {
+ TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct).ConfigureAwait(false);
+ return await EvaluateMessageCoreAsync(task, text, settingsSnapshot, ct).ConfigureAwait(false);
+ }
+
+ // Чистое ядро оценки одного сообщения по фиксированному снимку настроек (см. EvaluateMessageAsync).
+ // task: Задача поиска (description/keywords).
+ // text: Текст сообщения.
+ // settingsSnapshot: Снимок настроек тенанта (mlEnabled/aiEnabled).
+ // ct: Токен отмены.
+ // Возвращает: Вердикт: fit + причина + источник (heuristic|ml|ai).
+ private async Task EvaluateMessageCoreAsync(
+ DiscoveryTaskDto task,
+ string text,
+ TenantSettingsSnapshot settingsSnapshot,
+ CancellationToken ct)
+ {
+ string raw = text ?? string.Empty;
+ if (raw.Trim().Length < MinTextLength)
+ {
+ return new DiscoveryMessageFit(false, "слишком короткое", "heuristic");
+ }
+
+ if (settingsSnapshot.GetBool(SettingsKeys.MlEnabled, SettingsDefaults.MlEnabled))
+ {
+ try
+ {
+ MlPredictResultDto prediction = await _mlClient.PredictAsync(raw, ct).ConfigureAwait(false);
+ if (prediction.Take && string.Equals(prediction.Label, MlLearningLabels.Spam, StringComparison.OrdinalIgnoreCase))
+ {
+ return new DiscoveryMessageFit(false, "ML: спам", "ml");
+ }
+ }
+ catch (Exception exception) when (exception is not OperationCanceledException)
+ {
+ // Прогноз недоступен («не уверен», python ml_client.predict L101–107) — решает ИИ/эвристика.
+ }
+ }
+
+ if (settingsSnapshot.GetBool(SettingsKeys.AiEnabled, SettingsDefaults.AiEnabled))
+ {
+ try
+ {
+ AiEvaluateFitResultDto result = await _aiTools.EvaluateFitAsync(
+ SliceCodePoints(raw, AiTextLimit),
+ task.Description ?? string.Empty,
+ task.Keywords,
+ ct).ConfigureAwait(false);
+ string reason = (result.Reason ?? string.Empty).Trim();
+ if (reason.Length == 0)
+ {
+ reason = result.Fit ? AiFitReasonDefault : AiNotFitReasonDefault;
+ }
+
+ return new DiscoveryMessageFit(result.Fit, SliceCodePoints(reason, AiReasonLimit), "ai");
+ }
+ catch (Exception exception) when (exception is not OperationCanceledException)
+ {
+ // Сбой ИИ (нет ключа/сеть/не-JSON; Local — NotSupportedException) не роняет оценку (python L191–192).
+ }
+ }
+
+ return Heuristic(task, raw);
+ }
+
+ // Эвристика: ключ задачи входит в очищенный текст без учёта регистра (python _heuristic L144–150).
+ private static DiscoveryMessageFit Heuristic(DiscoveryTaskDto task, string text)
+ {
+ string haystack = CleanShort(text);
+ foreach (string keyword in task.Keywords)
+ {
+ string kw = (keyword ?? string.Empty).Trim();
+ if (kw.Length > 0 && haystack.Contains(kw.ToLowerInvariant(), StringComparison.Ordinal))
+ {
+ return new DiscoveryMessageFit(true, $"совпал ключ \"{kw}\"", "heuristic");
+ }
+ }
+
+ return new DiscoveryMessageFit(false, "нет совпадений с ключами", "heuristic");
+ }
+
+ ///
+ /// Вердикт «источник подходит»: выборка ≥3 сообщений и доля fit ≥ threshold, % (python passed L229–237).
+ ///
+ /// Агрегат оценки выборки/темы.
+ /// Порог задачи (1..100; дефолт discEvalThreshold).
+ /// True — источник/тема проходят порог (каналы часто разбавляют контент офтопом — доля, не сплошное).
+ public static bool Passed(DiscoveryEvalSample sample, int thresholdPercent)
+ {
+ return sample.Total >= MinContentMessages && sample.FitRatio * 100 >= Math.Max(1, thresholdPercent);
+ }
+
+ ///
+ /// Группирует сообщения выборки по topic_id (null → «main») и сортирует группы по числу сообщений
+ /// (убыв.), порядок сообщений внутри группы — входной (python group_by_topic L96–117).
+ ///
+ /// Заголовок группы — сниппет первого непустого текста (python _topic_title L86–93, ≤60 символов,
+ /// whitespace схлопнут). Возвращаемые группы переиспользуют исходные DTO (без копий).
+ /// Сообщения выборки (для форумов заполнен TopicId).
+ /// Группы от большей к меньшей; пусто — выборки нет.
+ public static IReadOnlyList GroupByTopic(IReadOnlyList messages)
+ {
+ var groups = new Dictionary>(StringComparer.Ordinal);
+ var order = new List();
+ foreach (TelegramEvalMessageDto message in messages ?? [])
+ {
+ string key = message.TopicId is long topicId ? topicId.ToString() : MainTopic;
+ if (!groups.TryGetValue(key, out List? bucket))
+ {
+ bucket = [];
+ groups[key] = bucket;
+ order.Add(key);
+ }
+
+ bucket.Add(message);
+ }
+
+ var outGroups = new List(order.Count);
+ foreach (string key in order)
+ {
+ List bucket = groups[key];
+ outGroups.Add(new DiscoveryTopicGroup(key, TopicTitle(bucket), bucket));
+ }
+
+ // python L116: sort(key=len(messages), reverse=True) — стабильная сортировка сохраняет входной порядок равных.
+ return outGroups
+ .OrderByDescending(group => group.Messages.Count)
+ .ToList();
+ }
+
+ // Сниппет первого непустого текста темы (python _topic_title L86–93).
+ private static string TopicTitle(IReadOnlyList messages)
+ {
+ foreach (TelegramEvalMessageDto message in messages)
+ {
+ string text = (message.Text ?? string.Empty).Trim();
+ if (text.Length > 0)
+ {
+ return SliceCodePoints(CollapseWhitespace(text), TopicTitleLimit);
+ }
+ }
+
+ return string.Empty;
+ }
+
+ // Очистка текста для эвристики: схлопывание whitespace (python clean_short ≈ «\n+»→« » + trim).
+ private static string CleanShort(string text)
+ {
+ string collapsed = CollapseWhitespace(text);
+ return collapsed.ToLowerInvariant();
+ }
+
+ // Схлопывает любые пробельные последовательности в один пробел и обрезает края (python " ".join(split())).
+ private static string CollapseWhitespace(string text)
+ {
+ var builder = new StringBuilder(text.Length);
+ bool pendingSpace = false;
+ foreach (char ch in text)
+ {
+ if (char.IsWhiteSpace(ch))
+ {
+ pendingSpace = builder.Length > 0;
+ continue;
+ }
+
+ if (pendingSpace)
+ {
+ builder.Append(' ');
+ pendingSpace = false;
+ }
+
+ builder.Append(ch);
+ }
+
+ return builder.ToString();
+ }
+
+ // Первые max кодовых точек строки (python-срез без разрыва суррогатных пар).
+ private static string SliceCodePoints(string text, int max)
+ {
+ if (text.Length <= max)
+ {
+ return text;
+ }
+
+ var builder = new StringBuilder(max);
+ int taken = 0;
+ for (int index = 0; index < text.Length && taken < max; index++)
+ {
+ bool pair = char.IsHighSurrogate(text[index])
+ && index + 1 < text.Length
+ && char.IsLowSurrogate(text[index + 1]);
+ builder.Append(text[index]);
+ if (pair)
+ {
+ index++;
+ builder.Append(text[index]);
+ }
+
+ taken++;
+ }
+
+ return builder.ToString();
+ }
+}
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Evaluate.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Evaluate.cs
index a8765a7..8375134 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Evaluate.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Evaluate.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Discovery.Application.Models;
using Deal.Modules.Discovery.Application.Abstractions;
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs
index 54f414e..4bbd755 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Discovery.Application.Models;
using Deal.Modules.Discovery.Application.Abstractions;
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Join.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Join.cs
index 665a934..3c2663b 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Join.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Join.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Discovery.Application.Models;
using Deal.Modules.Discovery.Application.Abstractions;
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs
index eff9ef3..ce7f171 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Discovery.Application.Models;
using Deal.Modules.Discovery.Application.Abstractions;
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs
index 4ce01df..0c39767 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Discovery.Application.Models;
using Deal.Modules.Discovery.Application.Abstractions;
diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs
index e4c5638..4e6e9eb 100644
--- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs
@@ -1,4 +1,5 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Helpers.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Helpers.cs
index a632b21..049fe2a 100644
--- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Helpers.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Helpers.cs
@@ -1,130 +1,130 @@
-using Deal.Contracts.Integrations;
-using Deal.Contracts.Integrations.Models;
-using Deal.Modules.Cards.Application.Abstractions;
-using Deal.Modules.Cards.Application.Dtos;
-using Deal.Modules.Cards.Application.Models;
-using Deal.Modules.Kanban.Application.ColumnRules;
-using Deal.Modules.Kanban.Application.Models;
-using Deal.Modules.Settings.Application.Abstractions;
-using Deal.Modules.Settings.Application.Models;
-using Deal.Modules.Settings.Application.Registrars;
-using Deal.Modules.Settings.Application.Services;
-// Алиас: статический класс ColumnRules лежит в одноимённом пространстве имён — внутри пространства имён
-// Deal.Modules.Kanban.Application имя ColumnRules резолвится в пространство (CS0234), нужен явный алиас.
-using KanbanColumnRules = Deal.Modules.Kanban.Application.ColumnRules.ColumnRules;
-using Deal.Modules.Kanban.Application.Abstractions;
-using Deal.Modules.Kanban.Application.Extensions;
-using Deal.Modules.Kanban.Application.Registrars;
-
-namespace Deal.Modules.Kanban.Application.Services;
-
-///
-/// Приватные помощники — partial-часть (C32: выделено из общего файла,
-/// поведение не менялось): текст обучающего примера, перенос колонки с журналом, hits правил доски и
-/// колонка возврата (leads.py L163–174, L209, L311–319; Ruling 2/4).
-///
-public sealed partial class CardsService
-{
- // ── Внутреннее ─────────────────────────────────────────────────────────
-
- // Текст обучающего примера: source_msg (после Trim) или title (leads.py L167, L189–190, L199–200).
- // card: Карточка.
- // Возвращает: source_msg без краевых пробелов; пустой — title как сохранён (1:1 с (x or "").strip() or (y or "")).
- private static string LearningText(CardDto card)
- {
- string source = card.SourceMsg.Trim();
- return source.Length > 0 ? source : card.Title;
- }
-
- // Меняет колонку карточки и пишет строку журнала CardMoves (leads.py _move L170–174).
- // card: Карточка ДО переноса (для prev_col/from_col журнала).
- // toCol: Новая колонка.
- // hits: matchHits для новой колонки (пересчитаны вызывающим, Ruling 2).
- // action: Действие журнала: move/trash.
- // ct: Токен отмены.
- private async Task MoveToColumnAsync(
- CardDto card,
- string toCol,
- IReadOnlyList hits,
- string action,
- CancellationToken ct)
- {
- await _store.UpdateColumnAsync(new CardColumnUpdateDto(
- CardId: card.Id,
- Col: toCol,
- IsNew: false,
- PrevCol: card.Col,
- ArchivedAt: null,
- MatchHits: hits), ct);
- await LogMoveAsync(card.Id, action, card.Col, toCol, ct);
- }
-
- // Пишет строку журнала действия (leads.py _log_learning L40–44): id lm_ генерирует модуль.
- // cardId: Id карточки.
- // action: Действие: move/trash/restore/comment.
- // fromCol: Прежняя колонка (для comment — null).
- // toCol: Новая колонка (для comment — null).
- // ct: Токен отмены.
- private async Task LogMoveAsync(
- string cardId,
- string action,
- string? fromCol,
- string? toCol,
- CancellationToken ct)
- {
- await _store.AddMoveAsync(new CardMoveDto(
- PrefixId.New(KanbanIdPrefixes.CardMove),
- cardId,
- action,
- fromCol,
- toCol), ct);
- }
-
- // Совпавшие критерии правил доски (hits_for_board L311–319 через ColumnRules, Ruling 2).
- // boardId: Id доски (b_...).
- // text: Текст карточки для правил (source_msg или title).
- // ct: Токен отмены.
- // Возвращает: Список совпавших критериев; доски нет/правил нет → пусто.
- private async Task> ComputeHitsForBoardAsync(
- string boardId,
- string text,
- CancellationToken ct)
- {
- ContainerDto? board = await _store.GetContainerAsync(boardId, ct);
- return board is null
- ? Array.Empty()
- : await ComputeHitsAsync(board.Rules, text, ct);
- }
-
- // Совпавшие критерии правил колонки-доски (ColumnRules.ComputeHits с курсами для бюджета).
- // Резолв имени — через алиас KanbanColumnRules: одноимённые класс и namespace ColumnRules в одном модуле.
- // rules: Правила доски; null («правил нет») → пусто (Ruling 2).
- // text: Текст карточки для правил.
- // ct: Токен отмены.
- // Возвращает: Совпавшие критерии (label/term[/word]); нет активных правил → пусто.
- private async Task> ComputeHitsAsync(
- ContainerRulesDto? rules,
- string text,
- CancellationToken ct)
- {
- // Кэш курсов из типизированного снимка настроек (C30): null → мок-курсы (дефолт RatesService).
- IReadOnlyDictionary rates =
- (await TenantSettingsSnapshot.LoadAsync(_settings, ct)).TryGetRatesCache()?.Rates ?? MockRates.Values;
- return KanbanColumnRules.ComputeHits(rules, text, rates);
- }
-
- // Колонка возврата карточки: prev_col, если inbox или существующая доска, иначе inbox (restore_lead L209).
- // prevCol: Сохранённая prev_col карточки.
- // ct: Токен отмены.
- // Возвращает: Колонка возврата (inbox/доска).
- private async Task ResolveReturnColAsync(string prevCol, CancellationToken ct)
- {
- if (prevCol == CardIds.Inbox)
- {
- return prevCol;
- }
-
- ContainerDto? board = await _store.GetContainerAsync(prevCol, ct);
- return board is null ? CardIds.Inbox : prevCol;
- }
-}
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
+using Deal.Modules.Cards.Application.Abstractions;
+using Deal.Modules.Cards.Application.Dtos;
+using Deal.Modules.Cards.Application.Models;
+using Deal.Modules.Kanban.Application.ColumnRules;
+using Deal.Modules.Kanban.Application.Models;
+using Deal.Modules.Settings.Application.Abstractions;
+using Deal.Modules.Settings.Application.Models;
+using Deal.Modules.Settings.Application.Registrars;
+using Deal.Modules.Settings.Application.Services;
+// Алиас: статический класс ColumnRules лежит в одноимённом пространстве имён — внутри пространства имён
+// Deal.Modules.Kanban.Application имя ColumnRules резолвится в пространство (CS0234), нужен явный алиас.
+using KanbanColumnRules = Deal.Modules.Kanban.Application.ColumnRules.ColumnRules;
+using Deal.Modules.Kanban.Application.Abstractions;
+using Deal.Modules.Kanban.Application.Extensions;
+using Deal.Modules.Kanban.Application.Registrars;
+
+namespace Deal.Modules.Kanban.Application.Services;
+
+///
+/// Приватные помощники — partial-часть (C32: выделено из общего файла,
+/// поведение не менялось): текст обучающего примера, перенос колонки с журналом, hits правил доски и
+/// колонка возврата (leads.py L163–174, L209, L311–319; Ruling 2/4).
+///
+public sealed partial class CardsService
+{
+ // ── Внутреннее ─────────────────────────────────────────────────────────
+
+ // Текст обучающего примера: source_msg (после Trim) или title (leads.py L167, L189–190, L199–200).
+ // card: Карточка.
+ // Возвращает: source_msg без краевых пробелов; пустой — title как сохранён (1:1 с (x or "").strip() or (y or "")).
+ private static string LearningText(CardDto card)
+ {
+ string source = card.SourceMsg.Trim();
+ return source.Length > 0 ? source : card.Title;
+ }
+
+ // Меняет колонку карточки и пишет строку журнала CardMoves (leads.py _move L170–174).
+ // card: Карточка ДО переноса (для prev_col/from_col журнала).
+ // toCol: Новая колонка.
+ // hits: matchHits для новой колонки (пересчитаны вызывающим, Ruling 2).
+ // action: Действие журнала: move/trash.
+ // ct: Токен отмены.
+ private async Task MoveToColumnAsync(
+ CardDto card,
+ string toCol,
+ IReadOnlyList hits,
+ string action,
+ CancellationToken ct)
+ {
+ await _store.UpdateColumnAsync(new CardColumnUpdateDto(
+ CardId: card.Id,
+ Col: toCol,
+ IsNew: false,
+ PrevCol: card.Col,
+ ArchivedAt: null,
+ MatchHits: hits), ct);
+ await LogMoveAsync(card.Id, action, card.Col, toCol, ct);
+ }
+
+ // Пишет строку журнала действия (leads.py _log_learning L40–44): id lm_ генерирует модуль.
+ // cardId: Id карточки.
+ // action: Действие: move/trash/restore/comment.
+ // fromCol: Прежняя колонка (для comment — null).
+ // toCol: Новая колонка (для comment — null).
+ // ct: Токен отмены.
+ private async Task LogMoveAsync(
+ string cardId,
+ string action,
+ string? fromCol,
+ string? toCol,
+ CancellationToken ct)
+ {
+ await _store.AddMoveAsync(new CardMoveDto(
+ PrefixId.New(KanbanIdPrefixes.CardMove),
+ cardId,
+ action,
+ fromCol,
+ toCol), ct);
+ }
+
+ // Совпавшие критерии правил доски (hits_for_board L311–319 через ColumnRules, Ruling 2).
+ // boardId: Id доски (b_...).
+ // text: Текст карточки для правил (source_msg или title).
+ // ct: Токен отмены.
+ // Возвращает: Список совпавших критериев; доски нет/правил нет → пусто.
+ private async Task> ComputeHitsForBoardAsync(
+ string boardId,
+ string text,
+ CancellationToken ct)
+ {
+ ContainerDto? board = await _store.GetContainerAsync(boardId, ct);
+ return board is null
+ ? Array.Empty()
+ : await ComputeHitsAsync(board.Rules, text, ct);
+ }
+
+ // Совпавшие критерии правил колонки-доски (ColumnRules.ComputeHits с курсами для бюджета).
+ // Резолв имени — через алиас KanbanColumnRules: одноимённые класс и namespace ColumnRules в одном модуле.
+ // rules: Правила доски; null («правил нет») → пусто (Ruling 2).
+ // text: Текст карточки для правил.
+ // ct: Токен отмены.
+ // Возвращает: Совпавшие критерии (label/term[/word]); нет активных правил → пусто.
+ private async Task> ComputeHitsAsync(
+ ContainerRulesDto? rules,
+ string text,
+ CancellationToken ct)
+ {
+ // Кэш курсов из типизированного снимка настроек (C30): null → мок-курсы (дефолт RatesService).
+ IReadOnlyDictionary rates =
+ (await TenantSettingsSnapshot.LoadAsync(_settings, ct)).TryGetRatesCache()?.Rates ?? MockRates.Values;
+ return KanbanColumnRules.ComputeHits(rules, text, rates);
+ }
+
+ // Колонка возврата карточки: prev_col, если inbox или существующая доска, иначе inbox (restore_lead L209).
+ // prevCol: Сохранённая prev_col карточки.
+ // ct: Токен отмены.
+ // Возвращает: Колонка возврата (inbox/доска).
+ private async Task ResolveReturnColAsync(string prevCol, CancellationToken ct)
+ {
+ if (prevCol == CardIds.Inbox)
+ {
+ return prevCol;
+ }
+
+ ContainerDto? board = await _store.GetContainerAsync(prevCol, ct);
+ return board is null ? CardIds.Inbox : prevCol;
+ }
+}
diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Operations.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Operations.cs
index 1a9d366..9105ddd 100644
--- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Operations.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Operations.cs
@@ -1,361 +1,361 @@
-using Deal.Contracts.Integrations;
-using Deal.Contracts.Integrations.Models;
-using Deal.Modules.Cards.Application.Abstractions;
-using Deal.Modules.Cards.Application.Dtos;
-using Deal.Modules.Cards.Application.Models;
-using Deal.Modules.Kanban.Application.ColumnRules;
-using Deal.Modules.Kanban.Application.Models;
-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.Kanban.Application.Abstractions;
-using Deal.Modules.Kanban.Application.Extensions;
-using Deal.Modules.Kanban.Application.Registrars;
-
-namespace Deal.Modules.Kanban.Application.Services;
-
-///
-/// Публичные операции карточек — partial-часть (C32: выделено из общего
-/// файла по темам, поведение не менялось): чтение списка/карточки, переносы/корзина/возврат/удаление/
-/// очистка колонки, комментарии, mark-seen, счётчики и поиск (leads.py L151–279, L509–551).
-///
-public sealed partial class CardsService
-{
- // ── Чтение (list_leads/get_lead L151–160) ───────────────────────────────
-
- ///
- /// Карточки колонки или всех колонок дашборда, received_at DESC (list_leads L151–156).
- ///
- /// Колонка-фильтр (inbox/archive/trash/доска); null — все колонки дашборда.
- /// Токен отмены.
- /// Полные карточки (маппинг/комментарии/time — адаптер); пусто — карточек нет.
- public Task> ListCardsAsync(string? col, CancellationToken ct)
- {
- return _store.ListCardsAsync(new CardsQuery(col), ct);
- }
-
- ///
- /// Одна карточка по id (get_lead L159–160; GET /api/cards/{id}).
- ///
- /// Id карточки (c_...).
- /// Токен отмены.
- /// Карточка или null — строки нет (эндпоинт отвечает 404 «Карточка не найдена»).
- public Task GetCardAsync(string cardId, CancellationToken ct)
- {
- return _store.GetCardAsync(cardId, ct);
- }
-
- // ── Переносы / архив / корзина (L163–247) ───────────────────────────────
-
- ///
- /// Перенос карточки на доску или в «Неразобранное» (move_lead L177–191 + _move L163–174).
- ///
- ///
- /// Цель валидируется до чтения карточки: не inbox и не существующая доска → 400
- /// . Исходная колонка archive/trash для MoveLeadAsync недоступна
- /// → 400 : вывод из них — только restore_lead (иначе перенос минует
- /// снятие метки «спам» возврата из корзины, Ruling 4). Перенос «в ту же колонку» — no-op (карточка
- /// возвращается без изменений; журнал и обучение не пишутся). При реальном переносе: колонка меняется
- /// (is_new=FALSE, prev_col = прежняя колонка), matchHits пересчитываются для доски через
- /// (Ruling 2; для inbox — пусто), пишется строка журнала action=move,
- /// а при to≠inbox — обучающий сигнал PushAsync(text, id доски, 1.0) (Ruling 4; текст = source_msg или title;
- /// пустой текст не учим — L188–191).
- ///
- /// Id карточки (c_...).
- /// Цель: inbox либо id доски (b_...).
- /// Токен отмены.
- /// Результат: Error (400) | Card=null (карточки нет, 404) | Card — карточка после переноса.
- public async Task MoveDashboardCardAsync(
- string cardId,
- string toCol,
- CancellationToken ct)
- {
- ContainerDto? board = null;
- if (toCol != CardIds.Inbox)
- {
- board = await _store.GetContainerAsync(toCol, ct);
- if (board is null)
- {
- return new CardResultDto(MoveTargetInvalidDetail, null);
- }
- }
-
- CardDto? card = await _store.GetCardAsync(cardId, ct);
- if (card is null)
- {
- return new CardResultDto(null, null);
- }
-
- // Из archive/trash карточку выводит только restore (L204–222): прямой move в доску
- // прошёл бы мимо снятия у ML веса «спама» возврата из корзины (Ruling 4, L218–221).
- if (card.Col == CardIds.Archive || card.Col == CardIds.Trash)
- {
- return new CardResultDto(MoveSourceRestrictedDetail, null);
- }
-
- if (card.Col == toCol)
- {
- return new CardResultDto(null, card);
- }
-
- string text = LearningText(card);
- IReadOnlyList hits = toCol == CardIds.Inbox
- ? Array.Empty()
- : await ComputeHitsAsync(board!.Rules, text, ct);
-
- await MoveToColumnAsync(card, toCol, hits, ActionMove, ct);
- if (toCol != CardIds.Inbox && text.Length > 0)
- {
- await _mlClient.PushAsync(text, toCol, PushWeightUser, ct);
- }
-
- return new CardResultDto(null, await _store.GetCardAsync(cardId, ct));
- }
-
- ///
- /// Перенос карточки в корзину (trash_lead L194–201): col=trash, is_new=FALSE, matchHits пусто.
- ///
- ///
- /// Карточка уже в корзине — no-op (как в _move L165–166). Журнал action=trash пишется при реальном
- /// переносе; обучающий сигнал «спам» 1.0 — только если карточка была НЕ в trash/archive (L198–201,
- /// Ruling 4). Исключение archive: перенос архива в корзину не «переучивает» на спам.
- ///
- /// Id карточки (c_...).
- /// Токен отмены.
- /// Карточка после переноса (при no-op — как была) либо null — карточки нет (404).
- public Task TrashCardAsync(string cardId, CancellationToken ct)
- {
- return TrashCardAsync(cardId, teach: true, ct);
- }
-
- ///
- /// Перенос карточки в корзину с управлением обучением ML (trash_lead L194–201).
- ///
- ///
- /// = false — «тихое» перемещение без сигнала «спам»: используется ручной
- /// переклассификацией (leads.py reclassify_lead L311/L316), где обучение кладётся ЯВНО одним сигналом
- /// с весом гипотезы ИИ (0.4), а не весом действия пользователя (1.0). Журнал action=trash пишется всегда.
- ///
- /// Id карточки (c_...).
- /// True — писать сигнал «спам» (действие пользователя); false — не писать.
- /// Токен отмены.
- /// Карточка после переноса (при no-op — как была) либо null — карточки нет (404).
- public async Task TrashCardAsync(
- string cardId,
- bool teach,
- CancellationToken ct)
- {
- CardDto? card = await _store.GetCardAsync(cardId, ct);
- if (card is null)
- {
- return null;
- }
-
- if (card.Col == CardIds.Trash)
- {
- return card;
- }
-
- string text = LearningText(card);
- await MoveToColumnAsync(card, CardIds.Trash, Array.Empty(), ActionTrash, ct);
- if (teach && card.Col != CardIds.Archive && text.Length > 0)
- {
- await _mlClient.PushAsync(text, MlLearningLabels.Spam, PushWeightUser, ct);
- }
-
- return await _store.GetCardAsync(cardId, ct);
- }
-
- ///
- /// Возврат карточки из архива/корзины на канбан (restore_lead L204–222).
- ///
- ///
- /// Куда возвращаем: prev_col, если это «Неразобранное» или существующая доска, иначе inbox (L209).
- /// При возврате is_new=TRUE, prev_col='inbox', archived_at=NULL (Ruling 10), matchHits пересчитаны для
- /// доски (Ruling 2), журнал action=restore. Возврат ИЗ корзины снимает метку спам:
- /// PushAsync(text, "spam", −1.0) (L218–221, Ruling 4); из архива сигнал не шлётся.
- ///
- /// Id карточки (c_...).
- /// Токен отмены.
- /// Колонка возврата (inbox/доска) либо null — карточки нет (404).
- public async Task RestoreCardAsync(string cardId, CancellationToken ct)
- {
- CardDto? card = await _store.GetCardAsync(cardId, ct);
- if (card is null)
- {
- return null;
- }
-
- string back = await ResolveReturnColAsync(card.PrevCol, ct);
- string text = LearningText(card);
- IReadOnlyList hits = back == CardIds.Inbox
- ? Array.Empty()
- : await ComputeHitsForBoardAsync(back, text, ct);
-
- await _store.UpdateColumnAsync(new CardColumnUpdateDto(
- CardId: cardId,
- Col: back,
- IsNew: true,
- PrevCol: CardIds.Inbox,
- ArchivedAt: null,
- MatchHits: hits), ct);
- await LogMoveAsync(cardId, ActionRestore, card.Col, back, ct);
-
- if (card.Col == CardIds.Trash && text.Length > 0)
- {
- await _mlClient.PushAsync(text, MlLearningLabels.Spam, PushWeightUnlearn, ct);
- }
-
- return back;
- }
-
- ///
- /// Полное удаление карточки (delete_forever L225–234): Cards + комментарии (FK cascade),
- /// журнал CardMoves/MlOutbox не трогаются (Ruling 10).
- ///
- /// Id карточки (c_...).
- /// Токен отмены.
- /// False — карточки нет (404 «Карточка не найдена»); True — удалена.
- public async Task DeleteForeverAsync(string cardId, CancellationToken ct)
- {
- if (await _store.GetCardAsync(cardId, ct) is null)
- {
- return false;
- }
-
- await _store.DeleteForeverAsync(cardId, ct);
- return true;
- }
-
- ///
- /// Полная ручная очистка служебной колонки trash/archive (clear_col L237–247).
- ///
- /// Другая колонка (inbox/доска/…) → 400 (как ValueError L239–240).
- /// Очищаемая колонка: trash | archive.
- /// Токен отмены.
- /// Результат: Error (400) либо Cleared — сколько карточек удалено навсегда.
- public async Task ClearColAsync(string col, CancellationToken ct)
- {
- if (col != CardIds.Trash && col != CardIds.Archive)
- {
- return new ClearColResultDto(ClearColInvalidDetail, 0);
- }
-
- int cleared = await _store.ClearColAsync(col, ct);
- return new ClearColResultDto(null, cleared);
- }
-
- // ── Комментарии (add_comment L259–265) ──────────────────────────────────
-
- ///
- /// Добавляет комментарий к карточке: строка LeadComments (id cm_) + журнал action=comment.
- ///
- ///
- /// Текст Trim'ится (пустой после Trim → 400 , как dashboard_routes L240–241);
- /// автор — «Вы»; ответ — полный список комментариев (свежий — «только что», маппинг адаптера). Карточки
- /// нет → Comments=null, Error=null (404 «Карточка не найдена» — сервис читает карточку до записи, порт
- /// LeadComments ссылается FK, Task 4).
- ///
- /// Id карточки (c_...).
- /// Текст комментария (непустой после Trim).
- /// Токен отмены.
- /// Результат: Error (400) | Comments=null (404) | Comments — список после добавления.
- public async Task AddCommentAsync(
- string cardId,
- string text,
- CancellationToken ct)
- {
- string trimmed = (text ?? string.Empty).Trim();
- if (trimmed.Length == 0)
- {
- return new AddCommentResultDto(EmptyCommentDetail, null);
- }
-
- if (await _store.GetCardAsync(cardId, ct) is null)
- {
- return new AddCommentResultDto(null, null);
- }
-
- await _store.AddCommentAsync(PrefixId.New(KanbanIdPrefixes.Comment), cardId, CommentAuthor, trimmed, ct);
- await LogMoveAsync(cardId, ActionComment, null, null, ct);
- IReadOnlyList comments = await _store.ListCommentsAsync(cardId, ct);
- return new AddCommentResultDto(null, comments);
- }
-
- // ── Пометить прочитанным (mark_seen L250–256) ───────────────────────────
-
- ///
- /// Снимает флаг «новое»: с одной карточки (cardId), колонки (col) или всех (оба null/пустые).
- ///
- /// Семантика 1:1 с mark_seen L250–256 (проверка на truthiness: пустая строка = параметр не задан).
- /// Эндпоинты этапа: mark-col-seen {col}, mark-all-seen (Ruling 11); /leads/{id}/seen фронтом не вызывается.
- /// Id карточки либо null/пусто.
- /// Колонка либо null/пусто (используется, когда cardId не задан).
- /// Токен отмены.
- public Task MarkSeenAsync(
- string? cardId,
- string? col,
- CancellationToken ct)
- {
- return _store.UpdateSeenAsync(
- string.IsNullOrEmpty(cardId) ? null : cardId,
- string.IsNullOrEmpty(col) ? null : col,
- ct);
- }
-
- // ── Счётчики (counts L268–279) ──────────────────────────────────────────
-
- ///
- /// Счётчики колонок (count+new по Cards) + статистика обучения/решений ML (learning/ml/ai).
- ///
- ///
- /// Форма CardCountsDto: Columns — только колонки с карточками; New — сумма «новых» по колонкам
- /// (counts L270–274). learning/ml/ai — из IMlClient.StatusAsync (L275–278, план Task 7 L321): learning =
- /// count(CardMoves), ml/ai — KV-счётчики решений пайплайна (на этапе 3 — 0, Ruling 4). Плоскую wire-форму
- /// «{new, <col>:{…}, learning, ml, ai}» собирает эндпоинт Task 8.
- ///
- /// Токен отмены.
- /// Счётчики: колонки + learning/ml/ai (поля New/Learning/Ml/Ai и словарь Columns).
- public async Task CountsAsync(CancellationToken ct)
- {
- IReadOnlyDictionary columns = await _store.CountCardsByColAsync(ct);
- MlStatusResponseDto mlStatus = await _mlClient.StatusAsync(ct);
- return new CardCountsDto
- {
- New = columns.Values.Sum(column => column.New),
- Columns = columns,
- Learning = mlStatus.Stats.Learning,
- Ml = mlStatus.Stats.Ml,
- Ai = mlStatus.Stats.Ai,
- };
- }
-
- // ── Поиск (search L509–551, LIKE-вариант Ruling 6) ──────────────────────
-
- ///
- /// Поиск карточек: FTS по Cards.SearchTsv + LIKE-дополнение (search L509–551, Ruling 6/Task 12).
- ///
- ///
- /// q после Trim короче 2 символов → пусто, порт не вызывается (поведение этапа 3, L511–512). Сам поиск
- /// выполняет адаптер — : SearchTsv @@ plainto_tsquery('russian')
- /// (морфология) ∪ LIKE по lower(title/summary/contact/source_msg), контейнеры-стадии «Выбранных»
- /// исключены, порядок
- /// ts_rank DESC, ReceivedAt DESC, результат ограничен = 12. messages: [] — на
- /// совесть эндпоинта (Task 8). Запрос нормализуется trim+lowercase (как отсев-поиск Ruling 6).
- ///
- /// Поисковый запрос (trim + lowercase внутри).
- /// Токен отмены.
- /// Найденные карточки (≤12); пусто — запрос короче 2 символов или нет совпадений.
- public async Task> SearchCardsAsync(string? query, CancellationToken ct)
- {
- string lowered = (query ?? string.Empty).Trim().ToLowerInvariant();
- if (lowered.Length < MinSearchQueryLength)
- {
- return Array.Empty();
- }
-
- return await _store.SearchCardsAsync(lowered, SearchLimit, ct);
- }
-
-}
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
+using Deal.Modules.Cards.Application.Abstractions;
+using Deal.Modules.Cards.Application.Dtos;
+using Deal.Modules.Cards.Application.Models;
+using Deal.Modules.Kanban.Application.ColumnRules;
+using Deal.Modules.Kanban.Application.Models;
+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.Kanban.Application.Abstractions;
+using Deal.Modules.Kanban.Application.Extensions;
+using Deal.Modules.Kanban.Application.Registrars;
+
+namespace Deal.Modules.Kanban.Application.Services;
+
+///
+/// Публичные операции карточек — partial-часть (C32: выделено из общего
+/// файла по темам, поведение не менялось): чтение списка/карточки, переносы/корзина/возврат/удаление/
+/// очистка колонки, комментарии, mark-seen, счётчики и поиск (leads.py L151–279, L509–551).
+///
+public sealed partial class CardsService
+{
+ // ── Чтение (list_leads/get_lead L151–160) ───────────────────────────────
+
+ ///
+ /// Карточки колонки или всех колонок дашборда, received_at DESC (list_leads L151–156).
+ ///
+ /// Колонка-фильтр (inbox/archive/trash/доска); null — все колонки дашборда.
+ /// Токен отмены.
+ /// Полные карточки (маппинг/комментарии/time — адаптер); пусто — карточек нет.
+ public Task> ListCardsAsync(string? col, CancellationToken ct)
+ {
+ return _store.ListCardsAsync(new CardsQuery(col), ct);
+ }
+
+ ///
+ /// Одна карточка по id (get_lead L159–160; GET /api/cards/{id}).
+ ///
+ /// Id карточки (c_...).
+ /// Токен отмены.
+ /// Карточка или null — строки нет (эндпоинт отвечает 404 «Карточка не найдена»).
+ public Task GetCardAsync(string cardId, CancellationToken ct)
+ {
+ return _store.GetCardAsync(cardId, ct);
+ }
+
+ // ── Переносы / архив / корзина (L163–247) ───────────────────────────────
+
+ ///
+ /// Перенос карточки на доску или в «Неразобранное» (move_lead L177–191 + _move L163–174).
+ ///
+ ///
+ /// Цель валидируется до чтения карточки: не inbox и не существующая доска → 400
+ /// . Исходная колонка archive/trash для MoveLeadAsync недоступна
+ /// → 400 : вывод из них — только restore_lead (иначе перенос минует
+ /// снятие метки «спам» возврата из корзины, Ruling 4). Перенос «в ту же колонку» — no-op (карточка
+ /// возвращается без изменений; журнал и обучение не пишутся). При реальном переносе: колонка меняется
+ /// (is_new=FALSE, prev_col = прежняя колонка), matchHits пересчитываются для доски через
+ /// (Ruling 2; для inbox — пусто), пишется строка журнала action=move,
+ /// а при to≠inbox — обучающий сигнал PushAsync(text, id доски, 1.0) (Ruling 4; текст = source_msg или title;
+ /// пустой текст не учим — L188–191).
+ ///
+ /// Id карточки (c_...).
+ /// Цель: inbox либо id доски (b_...).
+ /// Токен отмены.
+ /// Результат: Error (400) | Card=null (карточки нет, 404) | Card — карточка после переноса.
+ public async Task MoveDashboardCardAsync(
+ string cardId,
+ string toCol,
+ CancellationToken ct)
+ {
+ ContainerDto? board = null;
+ if (toCol != CardIds.Inbox)
+ {
+ board = await _store.GetContainerAsync(toCol, ct);
+ if (board is null)
+ {
+ return new CardResultDto(MoveTargetInvalidDetail, null);
+ }
+ }
+
+ CardDto? card = await _store.GetCardAsync(cardId, ct);
+ if (card is null)
+ {
+ return new CardResultDto(null, null);
+ }
+
+ // Из archive/trash карточку выводит только restore (L204–222): прямой move в доску
+ // прошёл бы мимо снятия у ML веса «спама» возврата из корзины (Ruling 4, L218–221).
+ if (card.Col == CardIds.Archive || card.Col == CardIds.Trash)
+ {
+ return new CardResultDto(MoveSourceRestrictedDetail, null);
+ }
+
+ if (card.Col == toCol)
+ {
+ return new CardResultDto(null, card);
+ }
+
+ string text = LearningText(card);
+ IReadOnlyList hits = toCol == CardIds.Inbox
+ ? Array.Empty()
+ : await ComputeHitsAsync(board!.Rules, text, ct);
+
+ await MoveToColumnAsync(card, toCol, hits, ActionMove, ct);
+ if (toCol != CardIds.Inbox && text.Length > 0)
+ {
+ await _mlClient.PushAsync(text, toCol, PushWeightUser, ct);
+ }
+
+ return new CardResultDto(null, await _store.GetCardAsync(cardId, ct));
+ }
+
+ ///
+ /// Перенос карточки в корзину (trash_lead L194–201): col=trash, is_new=FALSE, matchHits пусто.
+ ///
+ ///
+ /// Карточка уже в корзине — no-op (как в _move L165–166). Журнал action=trash пишется при реальном
+ /// переносе; обучающий сигнал «спам» 1.0 — только если карточка была НЕ в trash/archive (L198–201,
+ /// Ruling 4). Исключение archive: перенос архива в корзину не «переучивает» на спам.
+ ///
+ /// Id карточки (c_...).
+ /// Токен отмены.
+ /// Карточка после переноса (при no-op — как была) либо null — карточки нет (404).
+ public Task TrashCardAsync(string cardId, CancellationToken ct)
+ {
+ return TrashCardAsync(cardId, teach: true, ct);
+ }
+
+ ///
+ /// Перенос карточки в корзину с управлением обучением ML (trash_lead L194–201).
+ ///
+ ///
+ /// = false — «тихое» перемещение без сигнала «спам»: используется ручной
+ /// переклассификацией (leads.py reclassify_lead L311/L316), где обучение кладётся ЯВНО одним сигналом
+ /// с весом гипотезы ИИ (0.4), а не весом действия пользователя (1.0). Журнал action=trash пишется всегда.
+ ///
+ /// Id карточки (c_...).
+ /// True — писать сигнал «спам» (действие пользователя); false — не писать.
+ /// Токен отмены.
+ /// Карточка после переноса (при no-op — как была) либо null — карточки нет (404).
+ public async Task TrashCardAsync(
+ string cardId,
+ bool teach,
+ CancellationToken ct)
+ {
+ CardDto? card = await _store.GetCardAsync(cardId, ct);
+ if (card is null)
+ {
+ return null;
+ }
+
+ if (card.Col == CardIds.Trash)
+ {
+ return card;
+ }
+
+ string text = LearningText(card);
+ await MoveToColumnAsync(card, CardIds.Trash, Array.Empty(), ActionTrash, ct);
+ if (teach && card.Col != CardIds.Archive && text.Length > 0)
+ {
+ await _mlClient.PushAsync(text, MlLearningLabels.Spam, PushWeightUser, ct);
+ }
+
+ return await _store.GetCardAsync(cardId, ct);
+ }
+
+ ///
+ /// Возврат карточки из архива/корзины на канбан (restore_lead L204–222).
+ ///
+ ///
+ /// Куда возвращаем: prev_col, если это «Неразобранное» или существующая доска, иначе inbox (L209).
+ /// При возврате is_new=TRUE, prev_col='inbox', archived_at=NULL (Ruling 10), matchHits пересчитаны для
+ /// доски (Ruling 2), журнал action=restore. Возврат ИЗ корзины снимает метку спам:
+ /// PushAsync(text, "spam", −1.0) (L218–221, Ruling 4); из архива сигнал не шлётся.
+ ///
+ /// Id карточки (c_...).
+ /// Токен отмены.
+ /// Колонка возврата (inbox/доска) либо null — карточки нет (404).
+ public async Task RestoreCardAsync(string cardId, CancellationToken ct)
+ {
+ CardDto? card = await _store.GetCardAsync(cardId, ct);
+ if (card is null)
+ {
+ return null;
+ }
+
+ string back = await ResolveReturnColAsync(card.PrevCol, ct);
+ string text = LearningText(card);
+ IReadOnlyList hits = back == CardIds.Inbox
+ ? Array.Empty()
+ : await ComputeHitsForBoardAsync(back, text, ct);
+
+ await _store.UpdateColumnAsync(new CardColumnUpdateDto(
+ CardId: cardId,
+ Col: back,
+ IsNew: true,
+ PrevCol: CardIds.Inbox,
+ ArchivedAt: null,
+ MatchHits: hits), ct);
+ await LogMoveAsync(cardId, ActionRestore, card.Col, back, ct);
+
+ if (card.Col == CardIds.Trash && text.Length > 0)
+ {
+ await _mlClient.PushAsync(text, MlLearningLabels.Spam, PushWeightUnlearn, ct);
+ }
+
+ return back;
+ }
+
+ ///
+ /// Полное удаление карточки (delete_forever L225–234): Cards + комментарии (FK cascade),
+ /// журнал CardMoves/MlOutbox не трогаются (Ruling 10).
+ ///
+ /// Id карточки (c_...).
+ /// Токен отмены.
+ /// False — карточки нет (404 «Карточка не найдена»); True — удалена.
+ public async Task DeleteForeverAsync(string cardId, CancellationToken ct)
+ {
+ if (await _store.GetCardAsync(cardId, ct) is null)
+ {
+ return false;
+ }
+
+ await _store.DeleteForeverAsync(cardId, ct);
+ return true;
+ }
+
+ ///
+ /// Полная ручная очистка служебной колонки trash/archive (clear_col L237–247).
+ ///
+ /// Другая колонка (inbox/доска/…) → 400 (как ValueError L239–240).
+ /// Очищаемая колонка: trash | archive.
+ /// Токен отмены.
+ /// Результат: Error (400) либо Cleared — сколько карточек удалено навсегда.
+ public async Task ClearColAsync(string col, CancellationToken ct)
+ {
+ if (col != CardIds.Trash && col != CardIds.Archive)
+ {
+ return new ClearColResultDto(ClearColInvalidDetail, 0);
+ }
+
+ int cleared = await _store.ClearColAsync(col, ct);
+ return new ClearColResultDto(null, cleared);
+ }
+
+ // ── Комментарии (add_comment L259–265) ──────────────────────────────────
+
+ ///
+ /// Добавляет комментарий к карточке: строка LeadComments (id cm_) + журнал action=comment.
+ ///
+ ///
+ /// Текст Trim'ится (пустой после Trim → 400 , как dashboard_routes L240–241);
+ /// автор — «Вы»; ответ — полный список комментариев (свежий — «только что», маппинг адаптера). Карточки
+ /// нет → Comments=null, Error=null (404 «Карточка не найдена» — сервис читает карточку до записи, порт
+ /// LeadComments ссылается FK, Task 4).
+ ///
+ /// Id карточки (c_...).
+ /// Текст комментария (непустой после Trim).
+ /// Токен отмены.
+ /// Результат: Error (400) | Comments=null (404) | Comments — список после добавления.
+ public async Task AddCommentAsync(
+ string cardId,
+ string text,
+ CancellationToken ct)
+ {
+ string trimmed = (text ?? string.Empty).Trim();
+ if (trimmed.Length == 0)
+ {
+ return new AddCommentResultDto(EmptyCommentDetail, null);
+ }
+
+ if (await _store.GetCardAsync(cardId, ct) is null)
+ {
+ return new AddCommentResultDto(null, null);
+ }
+
+ await _store.AddCommentAsync(PrefixId.New(KanbanIdPrefixes.Comment), cardId, CommentAuthor, trimmed, ct);
+ await LogMoveAsync(cardId, ActionComment, null, null, ct);
+ IReadOnlyList comments = await _store.ListCommentsAsync(cardId, ct);
+ return new AddCommentResultDto(null, comments);
+ }
+
+ // ── Пометить прочитанным (mark_seen L250–256) ───────────────────────────
+
+ ///
+ /// Снимает флаг «новое»: с одной карточки (cardId), колонки (col) или всех (оба null/пустые).
+ ///
+ /// Семантика 1:1 с mark_seen L250–256 (проверка на truthiness: пустая строка = параметр не задан).
+ /// Эндпоинты этапа: mark-col-seen {col}, mark-all-seen (Ruling 11); /leads/{id}/seen фронтом не вызывается.
+ /// Id карточки либо null/пусто.
+ /// Колонка либо null/пусто (используется, когда cardId не задан).
+ /// Токен отмены.
+ public Task MarkSeenAsync(
+ string? cardId,
+ string? col,
+ CancellationToken ct)
+ {
+ return _store.UpdateSeenAsync(
+ string.IsNullOrEmpty(cardId) ? null : cardId,
+ string.IsNullOrEmpty(col) ? null : col,
+ ct);
+ }
+
+ // ── Счётчики (counts L268–279) ──────────────────────────────────────────
+
+ ///
+ /// Счётчики колонок (count+new по Cards) + статистика обучения/решений ML (learning/ml/ai).
+ ///
+ ///
+ /// Форма CardCountsDto: Columns — только колонки с карточками; New — сумма «новых» по колонкам
+ /// (counts L270–274). learning/ml/ai — из IMlClient.StatusAsync (L275–278, план Task 7 L321): learning =
+ /// count(CardMoves), ml/ai — KV-счётчики решений пайплайна (на этапе 3 — 0, Ruling 4). Плоскую wire-форму
+ /// «{new, <col>:{…}, learning, ml, ai}» собирает эндпоинт Task 8.
+ ///
+ /// Токен отмены.
+ /// Счётчики: колонки + learning/ml/ai (поля New/Learning/Ml/Ai и словарь Columns).
+ public async Task CountsAsync(CancellationToken ct)
+ {
+ IReadOnlyDictionary columns = await _store.CountCardsByColAsync(ct);
+ MlStatusResponseDto mlStatus = await _mlClient.StatusAsync(ct);
+ return new CardCountsDto
+ {
+ New = columns.Values.Sum(column => column.New),
+ Columns = columns,
+ Learning = mlStatus.Stats.Learning,
+ Ml = mlStatus.Stats.Ml,
+ Ai = mlStatus.Stats.Ai,
+ };
+ }
+
+ // ── Поиск (search L509–551, LIKE-вариант Ruling 6) ──────────────────────
+
+ ///
+ /// Поиск карточек: FTS по Cards.SearchTsv + LIKE-дополнение (search L509–551, Ruling 6/Task 12).
+ ///
+ ///
+ /// q после Trim короче 2 символов → пусто, порт не вызывается (поведение этапа 3, L511–512). Сам поиск
+ /// выполняет адаптер — : SearchTsv @@ plainto_tsquery('russian')
+ /// (морфология) ∪ LIKE по lower(title/summary/contact/source_msg), контейнеры-стадии «Выбранных»
+ /// исключены, порядок
+ /// ts_rank DESC, ReceivedAt DESC, результат ограничен = 12. messages: [] — на
+ /// совесть эндпоинта (Task 8). Запрос нормализуется trim+lowercase (как отсев-поиск Ruling 6).
+ ///
+ /// Поисковый запрос (trim + lowercase внутри).
+ /// Токен отмены.
+ /// Найденные карточки (≤12); пусто — запрос короче 2 символов или нет совпадений.
+ public async Task> SearchCardsAsync(string? query, CancellationToken ct)
+ {
+ string lowered = (query ?? string.Empty).Trim().ToLowerInvariant();
+ if (lowered.Length < MinSearchQueryLength)
+ {
+ return Array.Empty();
+ }
+
+ return await _store.SearchCardsAsync(lowered, SearchLimit, ct);
+ }
+
+}
diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.cs
index cf96e17..c287415 100644
--- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.ColumnRules;
using Deal.Modules.Kanban.Application.Models;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/AiCardLearning.cs b/src/core/Deal.Modules.Pipeline/Application/Services/AiCardLearning.cs
index 64716b2..8adde08 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/AiCardLearning.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/AiCardLearning.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Cards.Application.Abstractions;
using Deal.Modules.Cards.Application.Dtos;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/CardComposer.cs b/src/core/Deal.Modules.Pipeline/Application/Services/CardComposer.cs
index 7b87084..7615d72 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/CardComposer.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/CardComposer.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Cards.Application.Abstractions;
using Deal.Modules.Cards.Application.Dtos;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/CardReclassifier.cs b/src/core/Deal.Modules.Pipeline/Application/Services/CardReclassifier.cs
index eba79e9..d828408 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/CardReclassifier.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/CardReclassifier.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Cards.Application.Abstractions;
using Deal.Modules.Cards.Application.Dtos;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs
index f3c4fc0..179467c 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Cards.Application.Abstractions;
using Deal.Modules.Cards.Application.Dtos;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs
index f6adb37..7f91892 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs
@@ -1,4 +1,5 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
using Deal.Modules.Kanban.Application.Models;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Checks.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Checks.cs
index 4c928dd..5f89b3b 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Checks.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Checks.cs
@@ -1,6 +1,6 @@
using System.Globalization;
using System.Text.Json;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Decisions.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Decisions.cs
index 0a20228..331eaac 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Decisions.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Decisions.cs
@@ -1,6 +1,6 @@
using System.Globalization;
using System.Text.Json;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Learning.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Learning.cs
index 28561c7..68b7f4d 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Learning.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Learning.cs
@@ -1,6 +1,6 @@
using System.Globalization;
using System.Text.Json;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Pump.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Pump.cs
index 95e8099..00cfb45 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Pump.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Pump.cs
@@ -1,6 +1,6 @@
using System.Globalization;
using System.Text.Json;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Rejections.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Rejections.cs
index abab740..5f50973 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Rejections.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Rejections.cs
@@ -1,6 +1,6 @@
using System.Globalization;
using System.Text.Json;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Settings.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Settings.cs
index 069f25b..dcc1034 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Settings.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Settings.cs
@@ -1,6 +1,6 @@
using System.Globalization;
using System.Text.Json;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.State.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.State.cs
index b2ef58a..b7e9526 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.State.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.State.cs
@@ -1,6 +1,6 @@
using System.Globalization;
using System.Text.Json;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.cs
index d921c00..b132c87 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.cs
@@ -1,6 +1,6 @@
using System.Globalization;
using System.Text.Json;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
diff --git a/src/core/Deal.Modules.Telegram/Application/DialogsService.cs b/src/core/Deal.Modules.Telegram/Application/DialogsService.cs
index 1d412da..8201fef 100644
--- a/src/core/Deal.Modules.Telegram/Application/DialogsService.cs
+++ b/src/core/Deal.Modules.Telegram/Application/DialogsService.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Settings.Application.Abstractions;
using Deal.Modules.Settings.Application.Models;
diff --git a/src/core/Deal.Modules.Telegram/Application/TelegramModuleRegistrar.cs b/src/core/Deal.Modules.Telegram/Application/TelegramModuleRegistrar.cs
index 6470b1a..82ee8b8 100644
--- a/src/core/Deal.Modules.Telegram/Application/TelegramModuleRegistrar.cs
+++ b/src/core/Deal.Modules.Telegram/Application/TelegramModuleRegistrar.cs
@@ -1,4 +1,5 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Modules.Settings.Application.Abstractions;
using Deal.Modules.Settings.Application.Models;
using Deal.Modules.Settings.Application.Registrars;
diff --git a/src/core/Deal.Modules.Tenants/Application/Abstractions/ITenantProvisioner.cs b/src/core/Deal.Modules.Tenants/Application/Abstractions/ITenantProvisioner.cs
index 135fe98..a363848 100644
--- a/src/core/Deal.Modules.Tenants/Application/Abstractions/ITenantProvisioner.cs
+++ b/src/core/Deal.Modules.Tenants/Application/Abstractions/ITenantProvisioner.cs
@@ -1,4 +1,5 @@
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
diff --git a/src/core/Deal.Modules.Tenants/Application/Services/TenantService.cs b/src/core/Deal.Modules.Tenants/Application/Services/TenantService.cs
index 92c3de9..5d4a4dd 100644
--- a/src/core/Deal.Modules.Tenants/Application/Services/TenantService.cs
+++ b/src/core/Deal.Modules.Tenants/Application/Services/TenantService.cs
@@ -1,5 +1,6 @@
using Deal.Modules.Tenants.Application.Models;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Registrars;
diff --git a/src/core/Deal.SharedKernel/Tenants/ITenantContext.cs b/src/core/Deal.SharedKernel/Tenants/Abstractions/ITenantContext.cs
similarity index 92%
rename from src/core/Deal.SharedKernel/Tenants/ITenantContext.cs
rename to src/core/Deal.SharedKernel/Tenants/Abstractions/ITenantContext.cs
index 9d91c78..e228890 100644
--- a/src/core/Deal.SharedKernel/Tenants/ITenantContext.cs
+++ b/src/core/Deal.SharedKernel/Tenants/Abstractions/ITenantContext.cs
@@ -1,4 +1,6 @@
-namespace Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Models;
+
+namespace Deal.SharedKernel.Tenants.Abstractions;
///
/// Контекст текущего тенанта запроса.
diff --git a/src/core/Deal.SharedKernel/Tenants/TenantId.cs b/src/core/Deal.SharedKernel/Tenants/Models/TenantId.cs
similarity index 96%
rename from src/core/Deal.SharedKernel/Tenants/TenantId.cs
rename to src/core/Deal.SharedKernel/Tenants/Models/TenantId.cs
index d918582..03f5328 100644
--- a/src/core/Deal.SharedKernel/Tenants/TenantId.cs
+++ b/src/core/Deal.SharedKernel/Tenants/Models/TenantId.cs
@@ -1,4 +1,6 @@
-namespace Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+
+namespace Deal.SharedKernel.Tenants.Models;
///
/// Идентификатор тенанта. Инвариант: ровно 32 hex-символа (Guid в формате "N" без дефисов) — значение
diff --git a/src/core/tests/Deal.Tests.Unit/BudgetedAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/BudgetedAiClassifierTests.cs
index c26571e..54f6a6d 100644
--- a/src/core/tests/Deal.Tests.Unit/BudgetedAiClassifierTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/BudgetedAiClassifierTests.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Data;
using Deal.Infrastructure.Integrations.Abstractions;
@@ -17,7 +17,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.Logging.Abstractions;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/BudgetedAiToolsTests.cs b/src/core/tests/Deal.Tests.Unit/BudgetedAiToolsTests.cs
index eb04b4e..bb60003 100644
--- a/src/core/tests/Deal.Tests.Unit/BudgetedAiToolsTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/BudgetedAiToolsTests.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Data;
using Deal.Infrastructure.Integrations.Abstractions;
@@ -12,7 +12,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.Logging.Abstractions;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/DialogsServiceTests.cs b/src/core/tests/Deal.Tests.Unit/DialogsServiceTests.cs
index c88508e..94b3f11 100644
--- a/src/core/tests/Deal.Tests.Unit/DialogsServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/DialogsServiceTests.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Settings.Application.Abstractions;
using Deal.Modules.Settings.Application.Models;
diff --git a/src/core/tests/Deal.Tests.Unit/DiscoveryWorkerSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/DiscoveryWorkerSchedulerTests.cs
index 053e0c6..cd164e3 100644
--- a/src/core/tests/Deal.Tests.Unit/DiscoveryWorkerSchedulerTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/DiscoveryWorkerSchedulerTests.cs
@@ -1,5 +1,5 @@
using Deal.Api.Hosting;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Data;
using Deal.Modules.Discovery.Application.Abstractions;
@@ -17,7 +17,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
diff --git a/src/core/tests/Deal.Tests.Unit/FailingTenantProvisioner.cs b/src/core/tests/Deal.Tests.Unit/FailingTenantProvisioner.cs
index ae8045c..0163841 100644
--- a/src/core/tests/Deal.Tests.Unit/FailingTenantProvisioner.cs
+++ b/src/core/tests/Deal.Tests.Unit/FailingTenantProvisioner.cs
@@ -3,7 +3,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/FakeAiClassifier.cs b/src/core/tests/Deal.Tests.Unit/FakeAiClassifier.cs
index 15ea1ce..600a8d8 100644
--- a/src/core/tests/Deal.Tests.Unit/FakeAiClassifier.cs
+++ b/src/core/tests/Deal.Tests.Unit/FakeAiClassifier.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/FakeAiTools.cs b/src/core/tests/Deal.Tests.Unit/FakeAiTools.cs
index 737b4a1..43b4e2d 100644
--- a/src/core/tests/Deal.Tests.Unit/FakeAiTools.cs
+++ b/src/core/tests/Deal.Tests.Unit/FakeAiTools.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/FakeDiscoveryGateway.cs b/src/core/tests/Deal.Tests.Unit/FakeDiscoveryGateway.cs
index 921c87e..2810ae9 100644
--- a/src/core/tests/Deal.Tests.Unit/FakeDiscoveryGateway.cs
+++ b/src/core/tests/Deal.Tests.Unit/FakeDiscoveryGateway.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/FakeFileStorage.cs b/src/core/tests/Deal.Tests.Unit/FakeFileStorage.cs
index 749668f..e515dd5 100644
--- a/src/core/tests/Deal.Tests.Unit/FakeFileStorage.cs
+++ b/src/core/tests/Deal.Tests.Unit/FakeFileStorage.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/FakeMlClient.cs b/src/core/tests/Deal.Tests.Unit/FakeMlClient.cs
index 10a3202..5836d4f 100644
--- a/src/core/tests/Deal.Tests.Unit/FakeMlClient.cs
+++ b/src/core/tests/Deal.Tests.Unit/FakeMlClient.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/FakeTelegramGateway.cs b/src/core/tests/Deal.Tests.Unit/FakeTelegramGateway.cs
index dcb60b4..204e621 100644
--- a/src/core/tests/Deal.Tests.Unit/FakeTelegramGateway.cs
+++ b/src/core/tests/Deal.Tests.Unit/FakeTelegramGateway.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Grpc.Core;
diff --git a/src/core/tests/Deal.Tests.Unit/FakeTenantProvisioner.cs b/src/core/tests/Deal.Tests.Unit/FakeTenantProvisioner.cs
index f235d79..718d103 100644
--- a/src/core/tests/Deal.Tests.Unit/FakeTenantProvisioner.cs
+++ b/src/core/tests/Deal.Tests.Unit/FakeTenantProvisioner.cs
@@ -3,7 +3,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/GrpcAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/GrpcAiClassifierTests.cs
index 66a1cb8..9220900 100644
--- a/src/core/tests/Deal.Tests.Unit/GrpcAiClassifierTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/GrpcAiClassifierTests.cs
@@ -1,6 +1,6 @@
using System.Text.Json;
using System.Text.Json.Nodes;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Grpc.Ai;
using Deal.Infrastructure.Data;
@@ -24,7 +24,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.Logging.Abstractions;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/GrpcAiToolsTests.cs b/src/core/tests/Deal.Tests.Unit/GrpcAiToolsTests.cs
index 709d7e5..5dd1849 100644
--- a/src/core/tests/Deal.Tests.Unit/GrpcAiToolsTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/GrpcAiToolsTests.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Grpc.Ai;
using Deal.Infrastructure.Data;
@@ -17,7 +17,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.Logging.Abstractions;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/GrpcMlClientTests.cs b/src/core/tests/Deal.Tests.Unit/GrpcMlClientTests.cs
index 2589572..a0bea74 100644
--- a/src/core/tests/Deal.Tests.Unit/GrpcMlClientTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/GrpcMlClientTests.cs
@@ -17,7 +17,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.Logging.Abstractions;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/GrpcTelegramClientTests.cs b/src/core/tests/Deal.Tests.Unit/GrpcTelegramClientTests.cs
index 1b35c7b..0a5bd2f 100644
--- a/src/core/tests/Deal.Tests.Unit/GrpcTelegramClientTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/GrpcTelegramClientTests.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Grpc.Telegram;
using Deal.Infrastructure.Data;
@@ -8,7 +8,8 @@ using Deal.Infrastructure.Integrations.Extensions;
using Deal.Infrastructure.Integrations.Models;
using Deal.Infrastructure.Integrations.Options;
using Deal.Infrastructure.Integrations.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Grpc.Core;
using Microsoft.Extensions.Logging.Abstractions;
diff --git a/src/core/tests/Deal.Tests.Unit/IntegrationsDiTests.cs b/src/core/tests/Deal.Tests.Unit/IntegrationsDiTests.cs
index 8e7896c..3aea81c 100644
--- a/src/core/tests/Deal.Tests.Unit/IntegrationsDiTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/IntegrationsDiTests.cs
@@ -1,4 +1,5 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure;
using Deal.Infrastructure.Data;
using Deal.Infrastructure.Integrations.Abstractions;
@@ -26,7 +27,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.DependencyInjection;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/LocalAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/LocalAiClassifierTests.cs
index aa07f3f..cf94954 100644
--- a/src/core/tests/Deal.Tests.Unit/LocalAiClassifierTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/LocalAiClassifierTests.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
diff --git a/src/core/tests/Deal.Tests.Unit/LocalAiToolsTests.cs b/src/core/tests/Deal.Tests.Unit/LocalAiToolsTests.cs
index 9996a57..ac4cb90 100644
--- a/src/core/tests/Deal.Tests.Unit/LocalAiToolsTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/LocalAiToolsTests.cs
@@ -1,4 +1,5 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
using Deal.Infrastructure.Integrations.Extensions;
diff --git a/src/core/tests/Deal.Tests.Unit/LocalColumnSuggesterTests.cs b/src/core/tests/Deal.Tests.Unit/LocalColumnSuggesterTests.cs
index 20d5179..5d2087e 100644
--- a/src/core/tests/Deal.Tests.Unit/LocalColumnSuggesterTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/LocalColumnSuggesterTests.cs
@@ -1,5 +1,5 @@
using System.Globalization;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
diff --git a/src/core/tests/Deal.Tests.Unit/LoginAttemptEndpointHttpTests.cs b/src/core/tests/Deal.Tests.Unit/LoginAttemptEndpointHttpTests.cs
index 8791ce5..9d1dcaf 100644
--- a/src/core/tests/Deal.Tests.Unit/LoginAttemptEndpointHttpTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/LoginAttemptEndpointHttpTests.cs
@@ -2,7 +2,9 @@ using System.Net;
using System.Text;
using System.Text.Json;
using Deal.Api.Configuration;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Tenants.Application.Models;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/LoginAttemptGuardTests.cs b/src/core/tests/Deal.Tests.Unit/LoginAttemptGuardTests.cs
index 54a0cd9..76e4ec6 100644
--- a/src/core/tests/Deal.Tests.Unit/LoginAttemptGuardTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/LoginAttemptGuardTests.cs
@@ -1,5 +1,7 @@
using Deal.Api.Configuration;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/MlOutboxFlushSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/MlOutboxFlushSchedulerTests.cs
index e56d2bc..4a63f9e 100644
--- a/src/core/tests/Deal.Tests.Unit/MlOutboxFlushSchedulerTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/MlOutboxFlushSchedulerTests.cs
@@ -1,5 +1,6 @@
using Deal.Api.Hosting;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Data;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
@@ -21,7 +22,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
diff --git a/src/core/tests/Deal.Tests.Unit/OperatorAuthEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/OperatorAuthEndpointsHttpTests.cs
index f00f5c1..4a1db49 100644
--- a/src/core/tests/Deal.Tests.Unit/OperatorAuthEndpointsHttpTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/OperatorAuthEndpointsHttpTests.cs
@@ -1,7 +1,9 @@
using System.Net;
using System.Text;
using System.Text.Json;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
diff --git a/src/core/tests/Deal.Tests.Unit/OperatorAuthHttpHost.cs b/src/core/tests/Deal.Tests.Unit/OperatorAuthHttpHost.cs
index 88983eb..8ced108 100644
--- a/src/core/tests/Deal.Tests.Unit/OperatorAuthHttpHost.cs
+++ b/src/core/tests/Deal.Tests.Unit/OperatorAuthHttpHost.cs
@@ -1,7 +1,9 @@
using System.Net;
using Deal.Api.Configuration;
using Deal.Api.Endpoints;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Api.Middleware;
using Deal.Api.Telegram;
using Deal.Infrastructure.Data;
@@ -23,7 +25,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
diff --git a/src/core/tests/Deal.Tests.Unit/PipelineWorkerGrpcAiTests.cs b/src/core/tests/Deal.Tests.Unit/PipelineWorkerGrpcAiTests.cs
index 4030d20..92720fb 100644
--- a/src/core/tests/Deal.Tests.Unit/PipelineWorkerGrpcAiTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/PipelineWorkerGrpcAiTests.cs
@@ -1,4 +1,4 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Grpc.Ai;
using Deal.Infrastructure.Data;
@@ -27,7 +27,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.Logging.Abstractions;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/PipelineWorkerSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/PipelineWorkerSchedulerTests.cs
index c18596f..c91a3ac 100644
--- a/src/core/tests/Deal.Tests.Unit/PipelineWorkerSchedulerTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/PipelineWorkerSchedulerTests.cs
@@ -3,7 +3,7 @@ using Deal.Api;
using Deal.Api.Dtos;
using Deal.Api.Services;
using Deal.Api.Events;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Data;
using Deal.Modules.Kanban.Application.Abstractions;
@@ -25,7 +25,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
diff --git a/src/core/tests/Deal.Tests.Unit/RateLimitHttpTests.cs b/src/core/tests/Deal.Tests.Unit/RateLimitHttpTests.cs
index 6e49ecf..e662f4e 100644
--- a/src/core/tests/Deal.Tests.Unit/RateLimitHttpTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/RateLimitHttpTests.cs
@@ -1,7 +1,9 @@
using System.Net;
using System.Text.Json;
using Deal.Api.Configuration;
-using Deal.Api.Http;
+using Deal.Api.Extensions;
+using Deal.Api.Models;
+using Deal.Api.Services;
using Deal.Api.Middleware;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
diff --git a/src/core/tests/Deal.Tests.Unit/RuntimeDepthsCollectorTests.cs b/src/core/tests/Deal.Tests.Unit/RuntimeDepthsCollectorTests.cs
index 8d572f6..9def148 100644
--- a/src/core/tests/Deal.Tests.Unit/RuntimeDepthsCollectorTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/RuntimeDepthsCollectorTests.cs
@@ -1,5 +1,6 @@
using Deal.Api.Observability;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Data;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
@@ -15,7 +16,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
diff --git a/src/core/tests/Deal.Tests.Unit/StorageTickSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/StorageTickSchedulerTests.cs
index 0e1b434..15076f1 100644
--- a/src/core/tests/Deal.Tests.Unit/StorageTickSchedulerTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/StorageTickSchedulerTests.cs
@@ -1,7 +1,8 @@
using System.Text.Json;
using Deal.Api.Events;
using Deal.Api.Hosting;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Data;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
@@ -21,7 +22,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
diff --git a/src/core/tests/Deal.Tests.Unit/TelegramGatewayPortTests.cs b/src/core/tests/Deal.Tests.Unit/TelegramGatewayPortTests.cs
index 2ee89c9..1908971 100644
--- a/src/core/tests/Deal.Tests.Unit/TelegramGatewayPortTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/TelegramGatewayPortTests.cs
@@ -1,4 +1,5 @@
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/TelegramIngressTestHost.cs b/src/core/tests/Deal.Tests.Unit/TelegramIngressTestHost.cs
index 9ad836a..a89f8d6 100644
--- a/src/core/tests/Deal.Tests.Unit/TelegramIngressTestHost.cs
+++ b/src/core/tests/Deal.Tests.Unit/TelegramIngressTestHost.cs
@@ -5,7 +5,8 @@ using Deal.Api.Services;
using Deal.Api.Configuration;
using Deal.Api.Events;
using Deal.Api.Telegram;
-using Deal.Contracts.Integrations;
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Contracts.Integrations.Models;
using Deal.Grpc.Telegram;
using Deal.Infrastructure.Data;
using Deal.Modules.Pipeline.Application.Abstractions;
@@ -22,7 +23,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.AspNetCore.Builder;
diff --git a/src/core/tests/Deal.Tests.Unit/TenantIdTests.cs b/src/core/tests/Deal.Tests.Unit/TenantIdTests.cs
index eb1d991..9ebca24 100644
--- a/src/core/tests/Deal.Tests.Unit/TenantIdTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/TenantIdTests.cs
@@ -1,4 +1,5 @@
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
namespace Deal.Tests.Unit;
diff --git a/src/core/tests/Deal.Tests.Unit/TokenUsageRecorderTests.cs b/src/core/tests/Deal.Tests.Unit/TokenUsageRecorderTests.cs
index 92ca7f4..0edee9b 100644
--- a/src/core/tests/Deal.Tests.Unit/TokenUsageRecorderTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/TokenUsageRecorderTests.cs
@@ -16,7 +16,8 @@ using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
-using Deal.SharedKernel.Tenants;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Deal.SharedKernel.Tenants.Models;
namespace Deal.Tests.Unit;
diff --git a/src/ml-service/Deal.Ml.Tests/LearningData.cs b/src/ml-service/Deal.Ml.Tests/LearningData.cs
index 91a6c56..6eab0aa 100644
--- a/src/ml-service/Deal.Ml.Tests/LearningData.cs
+++ b/src/ml-service/Deal.Ml.Tests/LearningData.cs
@@ -1,4 +1,5 @@
using Deal.Ml.Model;
+using Deal.Ml.Extensions;
namespace Deal.Ml.Tests;
diff --git a/src/ml-service/Deal.Ml.Tests/MlTestHost.cs b/src/ml-service/Deal.Ml.Tests/MlTestHost.cs
index 0b6a27f..dfbd1ef 100644
--- a/src/ml-service/Deal.Ml.Tests/MlTestHost.cs
+++ b/src/ml-service/Deal.Ml.Tests/MlTestHost.cs
@@ -1,6 +1,7 @@
using Deal.Grpc.Hosting;
using Deal.Ml;
using Deal.Ml.Model;
+using Deal.Ml.Extensions;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.AspNetCore.Builder;
diff --git a/src/ml-service/Deal.Ml.Tests/MlTokenizerTests.cs b/src/ml-service/Deal.Ml.Tests/MlTokenizerTests.cs
index aa089b3..01a5422 100644
--- a/src/ml-service/Deal.Ml.Tests/MlTokenizerTests.cs
+++ b/src/ml-service/Deal.Ml.Tests/MlTokenizerTests.cs
@@ -1,4 +1,5 @@
using Deal.Ml.Model;
+using Deal.Ml.Extensions;
namespace Deal.Ml.Tests;
diff --git a/src/ml-service/Deal.Ml.Tests/ModelPersistenceTests.cs b/src/ml-service/Deal.Ml.Tests/ModelPersistenceTests.cs
index f56d586..8175786 100644
--- a/src/ml-service/Deal.Ml.Tests/ModelPersistenceTests.cs
+++ b/src/ml-service/Deal.Ml.Tests/ModelPersistenceTests.cs
@@ -1,4 +1,5 @@
using Deal.Ml.Model;
+using Deal.Ml.Extensions;
using Microsoft.Data.Sqlite;
namespace Deal.Ml.Tests;
diff --git a/src/ml-service/Deal.Ml.Tests/ModelPoolTests.cs b/src/ml-service/Deal.Ml.Tests/ModelPoolTests.cs
index 4d74fab..e224906 100644
--- a/src/ml-service/Deal.Ml.Tests/ModelPoolTests.cs
+++ b/src/ml-service/Deal.Ml.Tests/ModelPoolTests.cs
@@ -1,4 +1,5 @@
using Deal.Ml.Model;
+using Deal.Ml.Extensions;
namespace Deal.Ml.Tests;
diff --git a/src/ml-service/Deal.Ml.Tests/TenantModelLearningTests.cs b/src/ml-service/Deal.Ml.Tests/TenantModelLearningTests.cs
index fafa0eb..e410705 100644
--- a/src/ml-service/Deal.Ml.Tests/TenantModelLearningTests.cs
+++ b/src/ml-service/Deal.Ml.Tests/TenantModelLearningTests.cs
@@ -1,4 +1,5 @@
using Deal.Ml.Model;
+using Deal.Ml.Extensions;
namespace Deal.Ml.Tests;
diff --git a/src/ml-service/Deal.Ml/Model/LabelExtensions.cs b/src/ml-service/Deal.Ml/Extensions/LabelExtensions.cs
similarity index 86%
rename from src/ml-service/Deal.Ml/Model/LabelExtensions.cs
rename to src/ml-service/Deal.Ml/Extensions/LabelExtensions.cs
index 04a0383..ccc9775 100644
--- a/src/ml-service/Deal.Ml/Model/LabelExtensions.cs
+++ b/src/ml-service/Deal.Ml/Extensions/LabelExtensions.cs
@@ -1,4 +1,7 @@
-namespace Deal.Ml.Model;
+using Deal.Ml.Model;
+using Deal.Ml.Extensions;
+
+namespace Deal.Ml.Extensions;
///
/// Расширения для меток классов ML-модели.
diff --git a/src/ml-service/Deal.Ml/MlServiceHost.cs b/src/ml-service/Deal.Ml/MlServiceHost.cs
index 6f71732..08adb10 100644
--- a/src/ml-service/Deal.Ml/MlServiceHost.cs
+++ b/src/ml-service/Deal.Ml/MlServiceHost.cs
@@ -1,5 +1,6 @@
using Deal.Grpc.Hosting;
using Deal.Ml.Model;
+using Deal.Ml.Extensions;
namespace Deal.Ml;
diff --git a/src/ml-service/Deal.Ml/MlServiceImpl.cs b/src/ml-service/Deal.Ml/MlServiceImpl.cs
index b1b10ae..79cf39d 100644
--- a/src/ml-service/Deal.Ml/MlServiceImpl.cs
+++ b/src/ml-service/Deal.Ml/MlServiceImpl.cs
@@ -1,5 +1,6 @@
using Deal.Grpc.Ml;
using Deal.Ml.Model;
+using Deal.Ml.Extensions;
using Grpc.Core;
namespace Deal.Ml;
diff --git a/src/ml-service/Deal.Ml/Model/EvalEntry.cs b/src/ml-service/Deal.Ml/Model/EvalEntry.cs
index 7333044..587c85e 100644
--- a/src/ml-service/Deal.Ml/Model/EvalEntry.cs
+++ b/src/ml-service/Deal.Ml/Model/EvalEntry.cs
@@ -1,3 +1,5 @@
+using Deal.Ml.Extensions;
+
namespace Deal.Ml.Model;
///
diff --git a/src/ml-service/Deal.Ml/Model/LearnItem.cs b/src/ml-service/Deal.Ml/Model/LearnItem.cs
index 20dfbd1..d4995b2 100644
--- a/src/ml-service/Deal.Ml/Model/LearnItem.cs
+++ b/src/ml-service/Deal.Ml/Model/LearnItem.cs
@@ -1,3 +1,5 @@
+using Deal.Ml.Extensions;
+
namespace Deal.Ml.Model;
///
diff --git a/src/ml-service/Deal.Ml/Model/MlEvalInfo.cs b/src/ml-service/Deal.Ml/Model/MlEvalInfo.cs
index 48546b0..baffcad 100644
--- a/src/ml-service/Deal.Ml/Model/MlEvalInfo.cs
+++ b/src/ml-service/Deal.Ml/Model/MlEvalInfo.cs
@@ -1,3 +1,5 @@
+using Deal.Ml.Extensions;
+
namespace Deal.Ml.Model;
///
diff --git a/src/ml-service/Deal.Ml/Model/MlOptions.cs b/src/ml-service/Deal.Ml/Model/MlOptions.cs
index 1661486..3166611 100644
--- a/src/ml-service/Deal.Ml/Model/MlOptions.cs
+++ b/src/ml-service/Deal.Ml/Model/MlOptions.cs
@@ -1,3 +1,5 @@
+using Deal.Ml.Extensions;
+
namespace Deal.Ml.Model;
///
diff --git a/src/ml-service/Deal.Ml/Model/MlPredictResult.cs b/src/ml-service/Deal.Ml/Model/MlPredictResult.cs
index 7bda992..4f216e1 100644
--- a/src/ml-service/Deal.Ml/Model/MlPredictResult.cs
+++ b/src/ml-service/Deal.Ml/Model/MlPredictResult.cs
@@ -1,3 +1,5 @@
+using Deal.Ml.Extensions;
+
namespace Deal.Ml.Model;
///
diff --git a/src/ml-service/Deal.Ml/Model/MlStatusResult.cs b/src/ml-service/Deal.Ml/Model/MlStatusResult.cs
index 410386e..62388fa 100644
--- a/src/ml-service/Deal.Ml/Model/MlStatusResult.cs
+++ b/src/ml-service/Deal.Ml/Model/MlStatusResult.cs
@@ -1,3 +1,5 @@
+using Deal.Ml.Extensions;
+
namespace Deal.Ml.Model;
///
diff --git a/src/ml-service/Deal.Ml/Model/MlTypeDecision.cs b/src/ml-service/Deal.Ml/Model/MlTypeDecision.cs
index d3844c3..c49580b 100644
--- a/src/ml-service/Deal.Ml/Model/MlTypeDecision.cs
+++ b/src/ml-service/Deal.Ml/Model/MlTypeDecision.cs
@@ -1,3 +1,5 @@
+using Deal.Ml.Extensions;
+
namespace Deal.Ml.Model;
///
diff --git a/src/ml-service/Deal.Ml/Model/ModelConstants.cs b/src/ml-service/Deal.Ml/Model/ModelConstants.cs
index c601191..eb0938e 100644
--- a/src/ml-service/Deal.Ml/Model/ModelConstants.cs
+++ b/src/ml-service/Deal.Ml/Model/ModelConstants.cs
@@ -1,3 +1,5 @@
+using Deal.Ml.Extensions;
+
namespace Deal.Ml.Model;
///
diff --git a/src/ml-service/Deal.Ml/Model/ModelState.cs b/src/ml-service/Deal.Ml/Model/ModelState.cs
index 264b0bf..302a3f8 100644
--- a/src/ml-service/Deal.Ml/Model/ModelState.cs
+++ b/src/ml-service/Deal.Ml/Model/ModelState.cs
@@ -1,3 +1,5 @@
+using Deal.Ml.Extensions;
+
namespace Deal.Ml.Model;
///
diff --git a/src/ml-service/Deal.Ml/Model/OnlineNaiveBayes.cs b/src/ml-service/Deal.Ml/Model/OnlineNaiveBayes.cs
index b0b67a0..924c408 100644
--- a/src/ml-service/Deal.Ml/Model/OnlineNaiveBayes.cs
+++ b/src/ml-service/Deal.Ml/Model/OnlineNaiveBayes.cs
@@ -1,3 +1,5 @@
+using Deal.Ml.Extensions;
+
namespace Deal.Ml.Model;
///
diff --git a/src/ml-service/Deal.Ml/Storage/MlDb.cs b/src/ml-service/Deal.Ml/Storage/MlDb.cs
index c50490e..8bd4cfa 100644
--- a/src/ml-service/Deal.Ml/Storage/MlDb.cs
+++ b/src/ml-service/Deal.Ml/Storage/MlDb.cs
@@ -1,4 +1,5 @@
using Deal.Ml.Model;
+using Deal.Ml.Extensions;
using Microsoft.Data.Sqlite;
namespace Deal.Ml.Storage;
diff --git a/src/telegram-service/Deal.Telegram.Tests/BackfillServiceTests.cs b/src/telegram-service/Deal.Telegram.Tests/BackfillServiceTests.cs
index b94db0e..b5aaf07 100644
--- a/src/telegram-service/Deal.Telegram.Tests/BackfillServiceTests.cs
+++ b/src/telegram-service/Deal.Telegram.Tests/BackfillServiceTests.cs
@@ -1,6 +1,7 @@
using Deal.Telegram.Dialogs;
using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
using Grpc.Core;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
diff --git a/src/telegram-service/Deal.Telegram.Tests/DialogRpcTests.cs b/src/telegram-service/Deal.Telegram.Tests/DialogRpcTests.cs
index bb93913..e35b069 100644
--- a/src/telegram-service/Deal.Telegram.Tests/DialogRpcTests.cs
+++ b/src/telegram-service/Deal.Telegram.Tests/DialogRpcTests.cs
@@ -1,6 +1,7 @@
using Deal.Grpc.Telegram;
using Deal.Telegram.Dialogs;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.AspNetCore.Builder;
diff --git a/src/telegram-service/Deal.Telegram.Tests/DiscoveryOpsTests.cs b/src/telegram-service/Deal.Telegram.Tests/DiscoveryOpsTests.cs
index 5e92989..84179f5 100644
--- a/src/telegram-service/Deal.Telegram.Tests/DiscoveryOpsTests.cs
+++ b/src/telegram-service/Deal.Telegram.Tests/DiscoveryOpsTests.cs
@@ -1,6 +1,7 @@
using Deal.Telegram.Discovery;
using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
using Grpc.Core;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
diff --git a/src/telegram-service/Deal.Telegram.Tests/DiscoveryProtoMapperTests.cs b/src/telegram-service/Deal.Telegram.Tests/DiscoveryProtoMapperTests.cs
index 347f801..2d90db5 100644
--- a/src/telegram-service/Deal.Telegram.Tests/DiscoveryProtoMapperTests.cs
+++ b/src/telegram-service/Deal.Telegram.Tests/DiscoveryProtoMapperTests.cs
@@ -2,6 +2,7 @@ using Deal.Grpc.Telegram;
using Deal.Telegram.Discovery;
using Deal.Telegram.Dialogs;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
using Xunit;
namespace Deal.Telegram.Tests;
diff --git a/src/telegram-service/Deal.Telegram.Tests/DiscoveryRpcTests.cs b/src/telegram-service/Deal.Telegram.Tests/DiscoveryRpcTests.cs
index a4bdd68..6ce6eff 100644
--- a/src/telegram-service/Deal.Telegram.Tests/DiscoveryRpcTests.cs
+++ b/src/telegram-service/Deal.Telegram.Tests/DiscoveryRpcTests.cs
@@ -1,6 +1,7 @@
using Deal.Grpc.Telegram;
using Deal.Telegram.Dialogs;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.AspNetCore.Builder;
diff --git a/src/telegram-service/Deal.Telegram.Tests/FakeSessionClient.cs b/src/telegram-service/Deal.Telegram.Tests/FakeSessionClient.cs
index 282a373..bb0e59e 100644
--- a/src/telegram-service/Deal.Telegram.Tests/FakeSessionClient.cs
+++ b/src/telegram-service/Deal.Telegram.Tests/FakeSessionClient.cs
@@ -1,5 +1,6 @@
using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
namespace Deal.Telegram.Tests;
diff --git a/src/telegram-service/Deal.Telegram.Tests/RealtimeListenerTests.cs b/src/telegram-service/Deal.Telegram.Tests/RealtimeListenerTests.cs
index 9fe6915..0960f4d 100644
--- a/src/telegram-service/Deal.Telegram.Tests/RealtimeListenerTests.cs
+++ b/src/telegram-service/Deal.Telegram.Tests/RealtimeListenerTests.cs
@@ -2,6 +2,7 @@ using Deal.Grpc.Telegram;
using Deal.Telegram.Dialogs;
using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
using Grpc.Core;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
diff --git a/src/telegram-service/Deal.Telegram.Tests/RealtimeSweepTests.cs b/src/telegram-service/Deal.Telegram.Tests/RealtimeSweepTests.cs
index 25bc56f..065bb5b 100644
--- a/src/telegram-service/Deal.Telegram.Tests/RealtimeSweepTests.cs
+++ b/src/telegram-service/Deal.Telegram.Tests/RealtimeSweepTests.cs
@@ -1,6 +1,7 @@
using Deal.Telegram.Dialogs;
using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
using Grpc.Core;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
diff --git a/src/telegram-service/Deal.Telegram.Tests/TelegramSessionRpcTests.cs b/src/telegram-service/Deal.Telegram.Tests/TelegramSessionRpcTests.cs
index 0a002c2..d546163 100644
--- a/src/telegram-service/Deal.Telegram.Tests/TelegramSessionRpcTests.cs
+++ b/src/telegram-service/Deal.Telegram.Tests/TelegramSessionRpcTests.cs
@@ -1,5 +1,6 @@
using Deal.Grpc.Telegram;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.Extensions.DependencyInjection;
diff --git a/src/telegram-service/Deal.Telegram.Tests/TlMessageMapperTests.cs b/src/telegram-service/Deal.Telegram.Tests/TlMessageMapperTests.cs
index 0da9617..41788ad 100644
--- a/src/telegram-service/Deal.Telegram.Tests/TlMessageMapperTests.cs
+++ b/src/telegram-service/Deal.Telegram.Tests/TlMessageMapperTests.cs
@@ -1,4 +1,5 @@
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
using TL;
using Xunit;
diff --git a/src/telegram-service/Deal.Telegram/Dialogs/BackfillService.cs b/src/telegram-service/Deal.Telegram/Dialogs/BackfillService.cs
index a33fc2f..f7c7b93 100644
--- a/src/telegram-service/Deal.Telegram/Dialogs/BackfillService.cs
+++ b/src/telegram-service/Deal.Telegram/Dialogs/BackfillService.cs
@@ -3,6 +3,7 @@ using Deal.Grpc.Telegram;
using Deal.Telegram.Core;
using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
namespace Deal.Telegram.Dialogs;
diff --git a/src/telegram-service/Deal.Telegram/Dialogs/DialogProtoMapper.cs b/src/telegram-service/Deal.Telegram/Dialogs/DialogProtoMapper.cs
index 014e39a..1c408e7 100644
--- a/src/telegram-service/Deal.Telegram/Dialogs/DialogProtoMapper.cs
+++ b/src/telegram-service/Deal.Telegram/Dialogs/DialogProtoMapper.cs
@@ -1,6 +1,7 @@
using System.Globalization;
using Deal.Grpc.Telegram;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
namespace Deal.Telegram.Dialogs;
diff --git a/src/telegram-service/Deal.Telegram/Dialogs/RealtimeListener.cs b/src/telegram-service/Deal.Telegram/Dialogs/RealtimeListener.cs
index d54aad7..7fb2221 100644
--- a/src/telegram-service/Deal.Telegram/Dialogs/RealtimeListener.cs
+++ b/src/telegram-service/Deal.Telegram/Dialogs/RealtimeListener.cs
@@ -2,6 +2,7 @@ using Deal.Grpc.Telegram;
using Deal.Telegram.Core;
using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
namespace Deal.Telegram.Dialogs;
diff --git a/src/telegram-service/Deal.Telegram/Dialogs/RealtimeSweep.cs b/src/telegram-service/Deal.Telegram/Dialogs/RealtimeSweep.cs
index 883a14a..aae1679 100644
--- a/src/telegram-service/Deal.Telegram/Dialogs/RealtimeSweep.cs
+++ b/src/telegram-service/Deal.Telegram/Dialogs/RealtimeSweep.cs
@@ -2,6 +2,7 @@ using Deal.Grpc.Telegram;
using Deal.Telegram.Core;
using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
namespace Deal.Telegram.Dialogs;
diff --git a/src/telegram-service/Deal.Telegram/Discovery/DiscoveryOps.cs b/src/telegram-service/Deal.Telegram/Discovery/DiscoveryOps.cs
index a482b71..9483216 100644
--- a/src/telegram-service/Deal.Telegram/Discovery/DiscoveryOps.cs
+++ b/src/telegram-service/Deal.Telegram/Discovery/DiscoveryOps.cs
@@ -1,6 +1,7 @@
using Deal.Telegram.Dialogs;
using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
using Grpc.Core;
namespace Deal.Telegram.Discovery;
diff --git a/src/telegram-service/Deal.Telegram/Discovery/DiscoveryProtoMapper.cs b/src/telegram-service/Deal.Telegram/Discovery/DiscoveryProtoMapper.cs
index 231229a..af6d6d7 100644
--- a/src/telegram-service/Deal.Telegram/Discovery/DiscoveryProtoMapper.cs
+++ b/src/telegram-service/Deal.Telegram/Discovery/DiscoveryProtoMapper.cs
@@ -1,6 +1,7 @@
using Deal.Grpc.Telegram;
using Deal.Telegram.Dialogs;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
namespace Deal.Telegram.Discovery;
diff --git a/src/telegram-service/Deal.Telegram/Telegram/ExceptionExtensions.cs b/src/telegram-service/Deal.Telegram/Extensions/ExceptionExtensions.cs
similarity index 87%
rename from src/telegram-service/Deal.Telegram/Telegram/ExceptionExtensions.cs
rename to src/telegram-service/Deal.Telegram/Extensions/ExceptionExtensions.cs
index 3f7fba9..8a89b44 100644
--- a/src/telegram-service/Deal.Telegram/Telegram/ExceptionExtensions.cs
+++ b/src/telegram-service/Deal.Telegram/Extensions/ExceptionExtensions.cs
@@ -1,8 +1,10 @@
using System.IO;
using System.Net.Http;
using Grpc.Core;
+using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
-namespace Deal.Telegram.Telegram;
+namespace Deal.Telegram.Extensions;
///
/// Расширения для классификации сбоев telegram-service.
diff --git a/src/telegram-service/Deal.Telegram/Sessions/SessionFarm.cs b/src/telegram-service/Deal.Telegram/Sessions/SessionFarm.cs
index c2586e3..a17aaa4 100644
--- a/src/telegram-service/Deal.Telegram/Sessions/SessionFarm.cs
+++ b/src/telegram-service/Deal.Telegram/Sessions/SessionFarm.cs
@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using Grpc.Core;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
namespace Deal.Telegram.Sessions;
diff --git a/src/telegram-service/Deal.Telegram/Sessions/TenantSession.cs b/src/telegram-service/Deal.Telegram/Sessions/TenantSession.cs
index 5fc5337..455d934 100644
--- a/src/telegram-service/Deal.Telegram/Sessions/TenantSession.cs
+++ b/src/telegram-service/Deal.Telegram/Sessions/TenantSession.cs
@@ -1,5 +1,6 @@
using Grpc.Core;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
namespace Deal.Telegram.Sessions;
diff --git a/src/telegram-service/Deal.Telegram/Telegram/ClientFactory.cs b/src/telegram-service/Deal.Telegram/Telegram/ClientFactory.cs
index dcf278c..cf7311f 100644
--- a/src/telegram-service/Deal.Telegram/Telegram/ClientFactory.cs
+++ b/src/telegram-service/Deal.Telegram/Telegram/ClientFactory.cs
@@ -1,3 +1,5 @@
+using Deal.Telegram.Extensions;
+
namespace Deal.Telegram.Telegram;
///
diff --git a/src/telegram-service/Deal.Telegram/Telegram/DialogKinds.cs b/src/telegram-service/Deal.Telegram/Telegram/DialogKinds.cs
index 8e6cec3..ef05bf3 100644
--- a/src/telegram-service/Deal.Telegram/Telegram/DialogKinds.cs
+++ b/src/telegram-service/Deal.Telegram/Telegram/DialogKinds.cs
@@ -1,3 +1,5 @@
+using Deal.Telegram.Extensions;
+
namespace Deal.Telegram.Telegram;
///
diff --git a/src/telegram-service/Deal.Telegram/Telegram/DiscoveryMessage.cs b/src/telegram-service/Deal.Telegram/Telegram/DiscoveryMessage.cs
index 848a413..ae07109 100644
--- a/src/telegram-service/Deal.Telegram/Telegram/DiscoveryMessage.cs
+++ b/src/telegram-service/Deal.Telegram/Telegram/DiscoveryMessage.cs
@@ -1,3 +1,5 @@
+using Deal.Telegram.Extensions;
+
namespace Deal.Telegram.Telegram;
///
diff --git a/src/telegram-service/Deal.Telegram/Telegram/DiscoveryReadResult.cs b/src/telegram-service/Deal.Telegram/Telegram/DiscoveryReadResult.cs
index 4f024be..ff15945 100644
--- a/src/telegram-service/Deal.Telegram/Telegram/DiscoveryReadResult.cs
+++ b/src/telegram-service/Deal.Telegram/Telegram/DiscoveryReadResult.cs
@@ -1,3 +1,5 @@
+using Deal.Telegram.Extensions;
+
namespace Deal.Telegram.Telegram;
///
diff --git a/src/telegram-service/Deal.Telegram/Telegram/ISessionClient.cs b/src/telegram-service/Deal.Telegram/Telegram/ISessionClient.cs
index f7be887..6af5cde 100644
--- a/src/telegram-service/Deal.Telegram/Telegram/ISessionClient.cs
+++ b/src/telegram-service/Deal.Telegram/Telegram/ISessionClient.cs
@@ -1,3 +1,5 @@
+using Deal.Telegram.Extensions;
+
namespace Deal.Telegram.Telegram;
///
diff --git a/src/telegram-service/Deal.Telegram/Telegram/ITelegramClientFactory.cs b/src/telegram-service/Deal.Telegram/Telegram/ITelegramClientFactory.cs
index 68d9d37..7c94c8f 100644
--- a/src/telegram-service/Deal.Telegram/Telegram/ITelegramClientFactory.cs
+++ b/src/telegram-service/Deal.Telegram/Telegram/ITelegramClientFactory.cs
@@ -1,3 +1,5 @@
+using Deal.Telegram.Extensions;
+
namespace Deal.Telegram.Telegram;
///
diff --git a/src/telegram-service/Deal.Telegram/Telegram/TelegramDialog.cs b/src/telegram-service/Deal.Telegram/Telegram/TelegramDialog.cs
index 410bcb5..0635179 100644
--- a/src/telegram-service/Deal.Telegram/Telegram/TelegramDialog.cs
+++ b/src/telegram-service/Deal.Telegram/Telegram/TelegramDialog.cs
@@ -1,3 +1,5 @@
+using Deal.Telegram.Extensions;
+
namespace Deal.Telegram.Telegram;
///
diff --git a/src/telegram-service/Deal.Telegram/Telegram/TelegramMessage.cs b/src/telegram-service/Deal.Telegram/Telegram/TelegramMessage.cs
index 2c56da3..c76170e 100644
--- a/src/telegram-service/Deal.Telegram/Telegram/TelegramMessage.cs
+++ b/src/telegram-service/Deal.Telegram/Telegram/TelegramMessage.cs
@@ -1,3 +1,5 @@
+using Deal.Telegram.Extensions;
+
namespace Deal.Telegram.Telegram;
///
diff --git a/src/telegram-service/Deal.Telegram/Telegram/TelegramSourceInfo.cs b/src/telegram-service/Deal.Telegram/Telegram/TelegramSourceInfo.cs
index 9d8df17..c36d481 100644
--- a/src/telegram-service/Deal.Telegram/Telegram/TelegramSourceInfo.cs
+++ b/src/telegram-service/Deal.Telegram/Telegram/TelegramSourceInfo.cs
@@ -1,3 +1,5 @@
+using Deal.Telegram.Extensions;
+
namespace Deal.Telegram.Telegram;
///
diff --git a/src/telegram-service/Deal.Telegram/TelegramServiceHost.cs b/src/telegram-service/Deal.Telegram/TelegramServiceHost.cs
index 6eb0647..fef7e5f 100644
--- a/src/telegram-service/Deal.Telegram/TelegramServiceHost.cs
+++ b/src/telegram-service/Deal.Telegram/TelegramServiceHost.cs
@@ -5,6 +5,7 @@ using Deal.Telegram.Discovery;
using Deal.Telegram.Hosting;
using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
namespace Deal.Telegram;
diff --git a/src/telegram-service/Deal.Telegram/TelegramServiceImpl.cs b/src/telegram-service/Deal.Telegram/TelegramServiceImpl.cs
index e91f33d..8f31cdf 100644
--- a/src/telegram-service/Deal.Telegram/TelegramServiceImpl.cs
+++ b/src/telegram-service/Deal.Telegram/TelegramServiceImpl.cs
@@ -5,6 +5,7 @@ using Deal.Telegram.Dialogs;
using Deal.Telegram.Discovery;
using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
+using Deal.Telegram.Extensions;
using Grpc.Core;
namespace Deal.Telegram;