Разбить Infrastructure и корень Deal.Api по назначению

Integrations -> Abstractions/Exceptions/Extensions/Models/Options/
Services (включая Storage); Persistence-конфигурации -> Configurations;
корень Deal.Api (оркестратор/планировщики/DTO) -> Services/Dtos.
namespace приведён к путям, using добавлены/дедуплицированы, FQN
обновлены.
This commit is contained in:
Rustam Khalimov
2026-09-11 13:20:10 +03:00
parent cd0b3b606b
commit 410194b0cb
289 changed files with 855 additions and 101 deletions
@@ -0,0 +1,98 @@
using Deal.Grpc.Telegram;
using Grpc.Core;
using Grpc.Net.Client;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
using Deal.Infrastructure.Integrations.Extensions;
using Deal.Infrastructure.Integrations.Options;
using Deal.Infrastructure.Integrations.Services;
namespace Deal.Infrastructure.Integrations.Models;
/// <summary>
/// Транспорт gRPC-клиента telegram-service: общий канал + обязательные metadata (Ruling 1, план Task 14).
/// </summary>
/// <remarks>
/// Singleton (канал живёт долго и переиспользуется всеми вызовами): endpoint из
/// <see cref="TelegramServiceOptions"/>, service-token — из env <c>DEAL_SERVICE_TOKEN</c> (Ruling 13: секреты
/// только в env). Dev-транспорт без TLS (Ruling 2); mTLS (Ruling 6, Task 13): при включённом флаге канал
/// подписывает запрос клиентским сертификатом и проверяет CA сервера (сертификаты передаются
/// <see cref="MtlsCertificates"/>). Пустой endpoint либо пустой токен при создании — ошибка конфигурации (fail-closed:
/// без токена сервис отвергнет каждый вызов UNAUTHENTICATED, Ruling 1). Автоповторы Grpc.Net.Client отключены
/// (MaxRetryAttempts=0): стратегию повторов реализует вызывающий (фоновые циклы Api пробуют в следующем тике).
/// </remarks>
public sealed class TelegramGrpcConnection : IDisposable
{
/// <summary>
/// Env-ключ ожидаемого service-token (зеркало ServiceTokenInterceptor сервисов, Ruling 1).
/// </summary>
public const string ServiceTokenEnvKey = "DEAL_SERVICE_TOKEN";
/// <summary>
/// Ключ gRPC-metadata с tenant-id (зеркало TelegramServiceImpl, Ruling 1).
/// </summary>
public const string TenantIdMetadataKey = "tenant-id";
/// <summary>
/// Ключ gRPC-metadata с service-token (зеркало TelegramServiceImpl, Ruling 1).
/// </summary>
public const string ServiceTokenMetadataKey = "service-token";
private readonly GrpcChannel _channel;
private readonly string _serviceToken;
/// <summary>
/// Создаёт транспорт telegram-service по конфигурации и env-токену (валидация fail-closed).
/// </summary>
/// <param name="options">Конфигурация секции <c>Services:Telegram</c> (endpoint).</param>
/// <param name="mtlsCertificates">Сертификаты mTLS (Ruling 6): null — plaintext-канал (dev, флаг выключен).</param>
/// <exception cref="InvalidOperationException">Пустой endpoint или пустой DEAL_SERVICE_TOKEN.</exception>
public TelegramGrpcConnection(TelegramServiceOptions options, MtlsCertificates? mtlsCertificates = null)
{
ArgumentNullException.ThrowIfNull(options);
if (string.IsNullOrWhiteSpace(options.Endpoint))
{
throw new InvalidOperationException(
$"TelegramGrpcConnection: не задан endpoint telegram-service (секция \"{TelegramServiceOptions.SectionName}:Endpoint\").");
}
_serviceToken = Environment.GetEnvironmentVariable(ServiceTokenEnvKey) ?? string.Empty;
if (_serviceToken.Length == 0)
{
throw new InvalidOperationException(
$"TelegramGrpcConnection: не задан env {ServiceTokenEnvKey} — telegram-service отвергнет вызовы (fail-closed, Ruling 1).");
}
GrpcChannelOptions channelOptions = new() { MaxRetryAttempts = 0 };
if (mtlsCertificates is not null)
{
channelOptions.HttpHandler = mtlsCertificates.CreateClientHttpHandler();
}
_channel = GrpcChannel.ForAddress(options.Endpoint, channelOptions);
}
/// <summary>
/// Создаёт клиент RPC TelegramService поверх общего канала (клиент — лёгкий, на каждый вызов).
/// </summary>
/// <returns>Клиент сервиса Telegram (команды ядра наружу).</returns>
public TelegramService.TelegramServiceClient CreateClient() => new(_channel);
/// <summary>
/// Собирает обязательные metadata вызова: tenant-id + service-token (Ruling 1).
/// </summary>
/// <param name="tenantId">Id тенанта (строка, формат N — как в сессиях telegram-service).</param>
/// <returns>Metadata для CallOptions вызова.</returns>
public Metadata CreateMetadata(string tenantId)
{
var metadata = new Metadata
{
{ TenantIdMetadataKey, tenantId },
{ ServiceTokenMetadataKey, _serviceToken },
};
return metadata;
}
/// <inheritdoc />
public void Dispose() => _channel.Dispose();
}