using foodmarket.Application.Common.Fiscal; using foodmarket.Domain.Sales; using foodmarket.Infrastructure.Persistence; using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace foodmarket.Infrastructure.Fiscal; /// Касса24 (https://kassa24.kz) — облачная касса от Kaspi-группы. /// Тесно интегрирована с QR-эквайрингом Kaspi Pay (платёж и фискализация /// проходят одним flow). /// /// Skeleton, TODO: публичная документация Касса24 на момент /// scaffolding'а недоступна (NDA-only после подписания договора). Поэтому /// этот файл — каркас с тем же контрактом, что Webkassa, но реальный POST /// заменён на throw new FiscalNotConfiguredException: пока user /// не получит ApiKey + спецификацию endpoints, провайдер просит /// переключиться на Mock или Webkassa. /// /// Когда документация появится, нужно реализовать: /// /// Аутентификацию (предположительно — HMAC-SHA256 подпись запроса /// ApiSecret'ом, как у Kaspi merchant API). /// POST /v1/check (рабочее название — уточнить в доке). /// Маппинг RetailSale.Lines → их формат позиций. /// Парсинг ответа: fiscalNumber, qrCode, ticketUrl, transactionId. /// public class Kassa24Provider : IFiscalProvider { private const string DefaultBaseUrl = "https://api.kassa24.kz/"; private readonly HttpClient _http; private readonly IServiceScopeFactory _scopes; private readonly IDataProtectionProvider _dpProvider; private readonly ILogger _log; public Kassa24Provider( HttpClient http, IServiceScopeFactory scopes, IDataProtectionProvider dpProvider, ILogger log) { _http = http; _scopes = scopes; _dpProvider = dpProvider; _log = log; } public FiscalProviderKind Kind => FiscalProviderKind.Kassa24; public async Task RegisterAsync(RetailSale sale, CancellationToken ct) { // Проверка кредов — общий контракт со всеми провайдерами (даёт // диагностику в UI до того, как мы дойдём до реального POST). var (_, _) = await LoadCredentialsAsync(sale.OrganizationId, ct); // TODO(sprint11+): когда у нас будет ApiKey и доступ к Касса24-доке, // заменить throw на реальный запрос. Пока провайдер выбран в UI // только для демонстрации — фактическая фискализация не происходит. await Task.Yield(); _log.LogWarning( "Касса24-провайдер выбран, но интеграция ещё не реализована. " + "Чек {SaleNumber} не зарегистрирован у оператора.", sale.Number); throw new FiscalNotConfiguredException( "Касса24: интеграция ещё не реализована (нужны спецификации API от оператора). " + "Используйте Mock для разработки или переключитесь на Webkassa."); } private async Task<(string ApiKey, string ApiSecret)> LoadCredentialsAsync( Guid organizationId, CancellationToken ct) { using var scope = _scopes.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var org = await db.Organizations.IgnoreQueryFilters().AsNoTracking() .FirstOrDefaultAsync(o => o.Id == organizationId, ct); if (org is null) throw new FiscalProviderException($"Касса24: организация {organizationId} не найдена."); if (string.IsNullOrEmpty(org.FiscalApiKeyEncrypted) || string.IsNullOrEmpty(org.FiscalApiSecretEncrypted)) { throw new FiscalNotConfiguredException( "Касса24: ApiKey/ApiSecret не заданы. Откройте «Настройки организации → ОФД» и заполните их."); } var protector = _dpProvider.CreateProtector("foodmarket.fiscal"); try { return (protector.Unprotect(org.FiscalApiKeyEncrypted), protector.Unprotect(org.FiscalApiSecretEncrypted)); } catch (Exception ex) { throw new FiscalNotConfiguredException( "Не удалось расшифровать креды Касса24. Введите ApiKey/ApiSecret заново. Detail: " + ex.Message); } } }