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);
}
}
}