using Meezi.Admin.API.Models; using Meezi.Core.Platform; using Meezi.Infrastructure.Services.Platform; using Meezi.Core.Interfaces; using Meezi.Core.Entities; using Meezi.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace Meezi.Admin.API.Services; public interface IPlatformIntegrationService { Task GetIntegrationsAsync(CancellationToken ct = default); Task SaveIntegrationsAsync(UpdatePlatformIntegrationsRequest request, CancellationToken ct = default); } public class PlatformIntegrationService : IPlatformIntegrationService { public const string KeyActiveGateway = "payment.activeGateway"; public const string KeyKavenegarApi = "integrations.kavenegar.apiKey"; public const string KeyKavenegarOtpTemplate = "integrations.kavenegar.otpTemplate"; public const string KeyKavenegarEnabled = "integrations.kavenegar.enabled"; public const string KeyKavenegarSender = "integrations.kavenegar.senderNumber"; private static readonly (string Id, string NameFa, string Prefix)[] Gateways = [ ("zarinpal", "زرین‌پال", "payment.zarinpal"), ("tara", "تارا", "payment.tara"), ("snapppay", "اسنپ‌پی", "payment.snapppay"), ("nextpay", "نکست‌پی", "payment.nextpay"), ("vandar", "وندار", "payment.vandar") ]; private readonly AppDbContext _db; private readonly IPlatformCatalogService _catalog; private readonly IPlatformRuntimeConfig _runtime; public PlatformIntegrationService( AppDbContext db, IPlatformCatalogService catalog, IPlatformRuntimeConfig runtime) { _db = db; _catalog = catalog; _runtime = runtime; } public async Task GetIntegrationsAsync(CancellationToken ct = default) { var settings = await _db.PlatformSettings.AsNoTracking().ToListAsync(ct); var map = settings.ToDictionary(s => s.Key, s => s.Value, StringComparer.OrdinalIgnoreCase); var active = map.GetValueOrDefault(KeyActiveGateway) ?? "zarinpal"; var gateways = Gateways.Select(g => MapGateway(g.Id, g.NameFa, g.Prefix, active, map)).ToList(); var kavenegar = new KavenegarConfigDto( map.GetValueOrDefault(KeyKavenegarEnabled) is "true", MaskSecret(map.GetValueOrDefault(KeyKavenegarApi)), map.GetValueOrDefault(KeyKavenegarOtpTemplate) ?? "meeziotp", map.GetValueOrDefault(KeyKavenegarSender) ?? string.Empty, HasSecret(map, KeyKavenegarApi)); var ai = new AiIntegrationsConfigDto( new OpenAiIntegrationConfigDto( map.GetValueOrDefault(PlatformIntegrationKeys.OpenAiEnabled) is not "false", MaskSecret(map.GetValueOrDefault(PlatformIntegrationKeys.OpenAiApiKey)), map.GetValueOrDefault(PlatformIntegrationKeys.OpenAiModel) ?? "gpt-4o-mini", map.GetValueOrDefault(PlatformIntegrationKeys.OpenAiCoffeeAdvisorEnabled) is not "false", HasSecret(map, PlatformIntegrationKeys.OpenAiApiKey)), new MeshyIntegrationConfigDto( map.GetValueOrDefault(PlatformIntegrationKeys.MeshyEnabled) is not "false", MaskSecret(map.GetValueOrDefault(PlatformIntegrationKeys.MeshyApiKey)), map.GetValueOrDefault(PlatformIntegrationKeys.MeshyMenu3dEnabled) is not "false", HasSecret(map, PlatformIntegrationKeys.MeshyApiKey))); return new PlatformIntegrationsDto(active, gateways, kavenegar, ai); } public async Task SaveIntegrationsAsync(UpdatePlatformIntegrationsRequest request, CancellationToken ct = default) { var active = request.ActivePaymentGateway.Trim().ToLowerInvariant(); if (!Gateways.Any(g => g.Id == active)) active = "zarinpal"; await UpsertAsync(KeyActiveGateway, active, "payment", "درگاه پیش‌فرض اشتراک", ct); foreach (var gw in request.PaymentGateways) { var meta = Gateways.FirstOrDefault(g => g.Id == gw.Id); if (string.IsNullOrEmpty(meta.Id)) continue; await UpsertAsync($"{meta.Prefix}.enabled", gw.IsEnabled ? "true" : "false", "payment", $"فعال {meta.NameFa}", ct); await UpsertAsync($"{meta.Prefix}.sandbox", gw.Sandbox ? "true" : "false", "payment", $"حالت تست {meta.NameFa}", ct); if (gw.Id == "zarinpal") { if (!string.IsNullOrWhiteSpace(gw.MerchantId)) await UpsertAsync($"{meta.Prefix}.merchantId", gw.MerchantId.Trim(), "payment", "مرچنت زرین‌پال", ct); } else if (gw.Id is "nextpay" or "vandar") { if (!string.IsNullOrWhiteSpace(gw.ApiKey) && !IsMaskedPlaceholder(gw.ApiKey)) await UpsertAsync($"{meta.Prefix}.apiKey", gw.ApiKey.Trim(), "payment", $"توکن {meta.NameFa}", ct); } if (gw.Credentials is not null) await SaveCredentialsAsync(meta.Prefix, gw.Id, gw.Credentials, ct); } await UpsertAsync(KeyKavenegarEnabled, request.Kavenegar.IsEnabled ? "true" : "false", "integrations", "فعال کاوه‌نگار", ct); await UpsertAsync(KeyKavenegarOtpTemplate, request.Kavenegar.OtpTemplate.Trim(), "integrations", "قالب OTP", ct); if (!string.IsNullOrWhiteSpace(request.Kavenegar.ApiKey) && !IsMaskedPlaceholder(request.Kavenegar.ApiKey)) await UpsertAsync(KeyKavenegarApi, request.Kavenegar.ApiKey.Trim(), "integrations", "API Key کاوه‌نگار", ct); if (!string.IsNullOrWhiteSpace(request.Kavenegar.SenderNumber)) await UpsertAsync(KeyKavenegarSender, request.Kavenegar.SenderNumber.Trim(), "integrations", "شماره فرستنده کاوه‌نگار", ct); await UpsertAsync(PlatformIntegrationKeys.OpenAiEnabled, request.Ai.OpenAi.IsEnabled ? "true" : "false", "integrations", "فعال OpenAI", ct); await UpsertAsync(PlatformIntegrationKeys.OpenAiModel, string.IsNullOrWhiteSpace(request.Ai.OpenAi.Model) ? "gpt-4o-mini" : request.Ai.OpenAi.Model.Trim(), "integrations", "مدل OpenAI", ct); await UpsertAsync(PlatformIntegrationKeys.OpenAiCoffeeAdvisorEnabled, request.Ai.OpenAi.CoffeeAdvisorEnabled ? "true" : "false", "integrations", "مشاور قهوه OpenAI", ct); if (!string.IsNullOrWhiteSpace(request.Ai.OpenAi.ApiKey) && !IsMaskedPlaceholder(request.Ai.OpenAi.ApiKey)) await UpsertAsync(PlatformIntegrationKeys.OpenAiApiKey, request.Ai.OpenAi.ApiKey.Trim(), "integrations", "API Key OpenAI", ct); await UpsertAsync(PlatformIntegrationKeys.MeshyEnabled, request.Ai.Meshy.IsEnabled ? "true" : "false", "integrations", "فعال Meshy", ct); await UpsertAsync(PlatformIntegrationKeys.MeshyMenu3dEnabled, request.Ai.Meshy.Menu3dEnabled ? "true" : "false", "integrations", "ساخت ۳D منو با Meshy", ct); if (!string.IsNullOrWhiteSpace(request.Ai.Meshy.ApiKey) && !IsMaskedPlaceholder(request.Ai.Meshy.ApiKey)) await UpsertAsync(PlatformIntegrationKeys.MeshyApiKey, request.Ai.Meshy.ApiKey.Trim(), "integrations", "API Key Meshy", ct); await _db.SaveChangesAsync(ct); _catalog.InvalidateCache(); _runtime.InvalidateCache(); } private async Task SaveCredentialsAsync( string prefix, string gatewayId, UpdatePaymentGatewayCredentialsRequest creds, CancellationToken ct) { if (!string.IsNullOrWhiteSpace(creds.BaseUrl)) await UpsertAsync($"{prefix}.baseUrl", creds.BaseUrl.Trim(), "payment", "آدرس API", ct); if (gatewayId == "tara") { if (!string.IsNullOrWhiteSpace(creds.Username)) await UpsertAsync($"{prefix}.username", creds.Username.Trim(), "payment", "نام کاربری تارا", ct); if (!string.IsNullOrWhiteSpace(creds.Password) && !IsMaskedPlaceholder(creds.Password)) await UpsertAsync($"{prefix}.password", creds.Password.Trim(), "payment", "رمز تارا", ct); if (!string.IsNullOrWhiteSpace(creds.BranchCode)) await UpsertAsync($"{prefix}.branchCode", creds.BranchCode.Trim(), "payment", "کد شعبه تارا", ct); if (!string.IsNullOrWhiteSpace(creds.TerminalCode)) await UpsertAsync($"{prefix}.terminalCode", creds.TerminalCode.Trim(), "payment", "ترمینال تارا", ct); } else if (gatewayId == "snapppay") { if (!string.IsNullOrWhiteSpace(creds.ClientId)) await UpsertAsync($"{prefix}.clientId", creds.ClientId.Trim(), "payment", "Client ID اسنپ‌پی", ct); if (!string.IsNullOrWhiteSpace(creds.ClientSecret) && !IsMaskedPlaceholder(creds.ClientSecret)) await UpsertAsync($"{prefix}.clientSecret", creds.ClientSecret.Trim(), "payment", "Client Secret اسنپ‌پی", ct); if (!string.IsNullOrWhiteSpace(creds.Username)) await UpsertAsync($"{prefix}.username", creds.Username.Trim(), "payment", "نام کاربری اسنپ‌پی", ct); if (!string.IsNullOrWhiteSpace(creds.Password) && !IsMaskedPlaceholder(creds.Password)) await UpsertAsync($"{prefix}.password", creds.Password.Trim(), "payment", "رمز اسنپ‌پی", ct); } } private async Task UpsertAsync(string key, string value, string category, string descFa, CancellationToken ct) { var row = await _db.PlatformSettings.FirstOrDefaultAsync(s => s.Key == key, ct); if (row is null) { _db.PlatformSettings.Add(new PlatformSetting { Key = key, Value = value, Category = category, DescriptionFa = descFa }); } else { row.Value = value; row.UpdatedAt = DateTime.UtcNow; } } private static PaymentGatewayConfigDto MapGateway( string id, string nameFa, string prefix, string activeGateway, Dictionary map) { var enabled = map.GetValueOrDefault($"{prefix}.enabled") is "true"; var sandbox = map.GetValueOrDefault($"{prefix}.sandbox") is not "false"; string? merchantId = null; string? apiKey = null; var hasSecret = false; GatewayCredentialsDto? credentials = null; if (id == "zarinpal") { merchantId = map.GetValueOrDefault($"{prefix}.merchantId"); hasSecret = HasSecret(map, $"{prefix}.merchantId"); } else if (id is "nextpay" or "vandar") { apiKey = MaskSecret(map.GetValueOrDefault($"{prefix}.apiKey")); hasSecret = HasSecret(map, $"{prefix}.apiKey"); } else if (id == "tara") { credentials = new GatewayCredentialsDto( map.GetValueOrDefault($"{prefix}.username"), MaskSecret(map.GetValueOrDefault($"{prefix}.password")), map.GetValueOrDefault($"{prefix}.branchCode"), map.GetValueOrDefault($"{prefix}.terminalCode"), null, null, map.GetValueOrDefault($"{prefix}.baseUrl"), HasSecret(map, $"{prefix}.password"), false); hasSecret = credentials.HasStoredPassword; } else if (id == "snapppay") { credentials = new GatewayCredentialsDto( map.GetValueOrDefault($"{prefix}.username"), MaskSecret(map.GetValueOrDefault($"{prefix}.password")), null, null, map.GetValueOrDefault($"{prefix}.clientId"), MaskSecret(map.GetValueOrDefault($"{prefix}.clientSecret")), map.GetValueOrDefault($"{prefix}.baseUrl"), HasSecret(map, $"{prefix}.password"), HasSecret(map, $"{prefix}.clientSecret")); hasSecret = credentials.HasStoredPassword || credentials.HasStoredClientSecret; } return new PaymentGatewayConfigDto( id, nameFa, enabled, activeGateway == id, merchantId, apiKey, sandbox, hasSecret, credentials); } private static bool HasSecret(Dictionary map, string key) => !string.IsNullOrWhiteSpace(map.GetValueOrDefault(key)); private static string? MaskSecret(string? value) => string.IsNullOrWhiteSpace(value) ? null : "••••••••"; private static bool IsMaskedPlaceholder(string? value) => string.IsNullOrWhiteSpace(value) || value.Contains("••••", StringComparison.Ordinal); }