2026-05-27 21:33:48 +03:30
|
|
|
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<PlatformIntegrationsDto> GetIntegrationsAsync(CancellationToken ct = default);
|
|
|
|
|
Task SaveIntegrationsAsync(UpdatePlatformIntegrationsRequest request, CancellationToken ct = default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class PlatformIntegrationService : IPlatformIntegrationService
|
|
|
|
|
{
|
|
|
|
|
public const string KeyActiveGateway = "payment.activeGateway";
|
2026-05-29 02:38:06 +03:30
|
|
|
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";
|
2026-05-27 21:33:48 +03:30
|
|
|
|
|
|
|
|
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<PlatformIntegrationsDto> 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)),
|
2026-05-29 02:55:02 +03:30
|
|
|
map.GetValueOrDefault(KeyKavenegarOtpTemplate) ?? "meeziotp",
|
2026-05-29 02:38:06 +03:30
|
|
|
map.GetValueOrDefault(KeyKavenegarSender) ?? string.Empty,
|
2026-05-27 21:33:48 +03:30
|
|
|
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);
|
2026-05-29 02:38:06 +03:30
|
|
|
if (!string.IsNullOrWhiteSpace(request.Kavenegar.SenderNumber))
|
|
|
|
|
await UpsertAsync(KeyKavenegarSender, request.Kavenegar.SenderNumber.Trim(), "integrations", "شماره فرستنده کاوهنگار", ct);
|
2026-05-27 21:33:48 +03:30
|
|
|
|
|
|
|
|
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<string, string> 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<string, string> 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);
|
|
|
|
|
}
|