refactor: remove PYMS microservice, use ZarinPal directly in CMS
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m26s

- Remove PYMSPaymentService.cs and PYMS proto files
- Remove 'pyms' case from DI ConfigureServices
- Remove PYMS config from appsettings
- Switch PaymentProvider to 'zarinpal' (direct integration)
- ZarinPalPaymentService handles sandbox/production, verify, errors
- PYMS deployment/service/ingress removed from K8s
This commit is contained in:
masoodafar-web
2026-02-15 23:25:20 +03:30
parent 2502cbbda2
commit 207f53ef80
7 changed files with 3 additions and 621 deletions
@@ -1,284 +0,0 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.PYMS;
using CMSMicroservice.Protobuf.Protos.PYMS.Transaction;
using Grpc.Net.Client;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Net.Http;
namespace CMSMicroservice.Infrastructure.Services.Payment;
/// <summary>
/// پیاده‌سازی درگاه پرداخت از طریق PYMS (Payment Microservice)
/// CMS به جای اتصال مستقیم به ZarinPal، از PYMS استفاده می‌کند.
/// PYMS تراکنش‌ها را ذخیره و با ZarinPal ارتباط برقرار می‌کند.
/// </summary>
public class PYMSPaymentService : IPaymentGatewayService, IDisposable
{
private readonly ILogger<PYMSPaymentService> _logger;
private readonly GrpcChannel _channel;
private readonly TransactionContract.TransactionContractClient _client;
private readonly string _merchantId;
private readonly bool _useSandbox;
public PYMSPaymentService(
IConfiguration configuration,
ILogger<PYMSPaymentService> logger)
{
_logger = logger;
var pymsAddress = configuration["PYMS:Address"]
?? throw new InvalidOperationException("PYMS:Address is not configured.");
_merchantId = configuration["ZarinPal:MerchantId"]
?? throw new InvalidOperationException("ZarinPal:MerchantId is not configured.");
_useSandbox = configuration.GetValue<bool>("ZarinPal:UseSandbox", true);
// ایجاد کانال gRPC به PYMS
_channel = GrpcChannel.ForAddress(pymsAddress, new GrpcChannelOptions
{
HttpHandler = new SocketsHttpHandler
{
EnableMultipleHttp2Connections = true,
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
KeepAlivePingDelay = TimeSpan.FromSeconds(60),
KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
}
});
_client = new TransactionContract.TransactionContractClient(_channel);
_logger.LogInformation(
"PYMS Payment Service initialized. Address={Address}, Mode={Mode}",
pymsAddress, _useSandbox ? "🧪 Sandbox" : "🏦 Production");
}
/// <summary>
/// مرحله ۱: ارسال درخواست پرداخت به PYMS
/// PYMS تراکنش را ایجاد و URL درگاه را برمی‌گرداند
/// </summary>
public async Task<PaymentInitiateResult> InitiatePaymentAsync(
PaymentRequest request,
CancellationToken cancellationToken = default)
{
try
{
// CMS مبالغ را به تومان نگه‌داری می‌کند
// PYMS مبلغ را به ریال می‌خواهد — تبدیل تومان به ریال
var amountInRials = (long)(request.Amount * 10);
var grpcRequest = new PaymentRequestRequest
{
MerchantId = _merchantId,
Amount = amountInRials,
CallbackUrl = request.CallbackUrl ?? string.Empty,
Description = request.Description ?? string.Empty,
OrderId = request.UserId.ToString(),
// نوع تراکنش: Sandbox برای تست، Real برای Production
Type = _useSandbox ? TransactionTypeEnum.Sandbox : TransactionTypeEnum.Real,
Currency = CurrencyEnum.Irt, // تومان
};
if (!string.IsNullOrWhiteSpace(request.Mobile))
grpcRequest.Mobile = request.Mobile;
_logger.LogInformation(
"PYMS payment request: Amount={AmountToman} Toman ({AmountRial} Rial), User={UserId}, Sandbox={Sandbox}",
request.Amount, amountInRials, request.UserId, _useSandbox);
var response = await _client.PaymentRequestAsync(grpcRequest, cancellationToken: cancellationToken);
if (!string.IsNullOrEmpty(response.PaymentGWUrl))
{
_logger.LogInformation(
"PYMS payment initiated successfully: GatewayUrl={Url}",
response.PaymentGWUrl);
// از URL درگاه، Authority را استخراج می‌کنیم (آخرین بخش URL)
var authority = ExtractAuthorityFromUrl(response.PaymentGWUrl);
return new PaymentInitiateResult
{
IsSuccess = true,
RefId = authority,
GatewayUrl = response.PaymentGWUrl
};
}
_logger.LogError("PYMS payment request failed: Empty gateway URL returned");
return new PaymentInitiateResult
{
IsSuccess = false,
ErrorMessage = "خطا در دریافت آدرس درگاه از PYMS"
};
}
catch (Grpc.Core.RpcException ex)
{
_logger.LogError(ex, "PYMS gRPC error in InitiatePayment: Status={Status}, Detail={Detail}",
ex.StatusCode, ex.Status.Detail);
return new PaymentInitiateResult
{
IsSuccess = false,
ErrorMessage = $"خطا در ارتباط با سرویس پرداخت: {ex.Status.Detail}"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "PYMS InitiatePayment exception");
return new PaymentInitiateResult
{
IsSuccess = false,
ErrorMessage = $"خطا در ارتباط با سرویس پرداخت: {ex.Message}"
};
}
}
/// <summary>
/// تأیید پرداخت بدون مبلغ — PYMS خودش مبلغ را از تراکنش ذخیره‌شده می‌خواند
/// </summary>
public async Task<PaymentVerificationResult> VerifyPaymentAsync(
string refId,
string verificationToken,
CancellationToken cancellationToken = default)
{
return await VerifyPaymentInternalAsync(refId, verificationToken, cancellationToken);
}
/// <summary>
/// تأیید پرداخت با مبلغ — PYMS خودش verify را انجام می‌دهد
/// refId = Authority, verificationToken = Status (OK/NOK)
/// </summary>
public async Task<PaymentVerificationResult> VerifyPaymentAsync(
string refId,
string verificationToken,
decimal amountInToman,
CancellationToken cancellationToken = default)
{
return await VerifyPaymentInternalAsync(refId, verificationToken, cancellationToken);
}
private async Task<PaymentVerificationResult> VerifyPaymentInternalAsync(
string refId,
string verificationToken,
CancellationToken cancellationToken)
{
try
{
// اگر کاربر لغو کرده
if (!string.Equals(verificationToken, "OK", StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("Payment cancelled by user: Authority={Authority}", refId);
return new PaymentVerificationResult
{
IsSuccess = false,
RefId = refId,
Message = "پرداخت توسط کاربر لغو شد"
};
}
var grpcRequest = new PaymentVerificationRequest
{
Authority = refId,
Status = verificationToken
};
_logger.LogInformation("PYMS verify request: Authority={Authority}, Status={Status}",
refId, verificationToken);
var response = await _client.PaymentVerificationAsync(grpcRequest, cancellationToken: cancellationToken);
if (response.PaymentStatus)
{
_logger.LogInformation(
"PYMS payment verified: Id={Id}, RefId={RefId}, OrderId={OrderId}, StatusCode={StatusCode}",
response.Id, response.RefId, response.OrderId, response.VerificationStatusCode);
return new PaymentVerificationResult
{
IsSuccess = true,
RefId = refId,
TrackingCode = response.RefId,
Amount = 0, // مبلغ از DB خوانده می‌شود
Message = response.Message ?? "تراکنش موفق"
};
}
_logger.LogError(
"PYMS verify failed: Authority={Authority}, StatusCode={StatusCode}, Message={Message}",
refId, response.VerificationStatusCode, response.Message);
return new PaymentVerificationResult
{
IsSuccess = false,
RefId = refId,
Message = response.Message ?? "تأیید پرداخت ناموفق"
};
}
catch (Grpc.Core.RpcException ex)
{
_logger.LogError(ex, "PYMS gRPC error in VerifyPayment: Status={Status}, Detail={Detail}",
ex.StatusCode, ex.Status.Detail);
return new PaymentVerificationResult
{
IsSuccess = false,
RefId = refId,
Message = $"خطا در تأیید تراکنش: {ex.Status.Detail}"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "PYMS VerifyPayment exception: Authority={Authority}", refId);
return new PaymentVerificationResult
{
IsSuccess = false,
RefId = refId,
Message = $"خطا در تأیید تراکنش: {ex.Message}"
};
}
}
/// <summary>
/// PYMS فعلاً قابلیت Payout ندارد
/// </summary>
public Task<PayoutResult> ProcessPayoutAsync(
PayoutRequest request,
CancellationToken cancellationToken = default)
{
_logger.LogWarning("PYMS does not support direct payout yet.");
return Task.FromResult(new PayoutResult
{
IsSuccess = false,
Message = "سرویس پرداخت (PYMS) فعلاً از قابلیت واریز مستقیم پشتیبانی نمی‌کند",
ProcessedAt = DateTime.UtcNow
});
}
/// <summary>
/// استخراج Authority از URL درگاه
/// مثال: https://sandbox.zarinpal.com/pg/StartPay/A00000000000000000000000000123456789 → A00000000000000000000000000123456789
/// </summary>
private static string ExtractAuthorityFromUrl(string gatewayUrl)
{
if (string.IsNullOrEmpty(gatewayUrl))
return string.Empty;
// Authority معمولاً آخرین بخش URL است
var uri = new Uri(gatewayUrl);
var segments = uri.Segments;
if (segments.Length > 0)
{
return segments[^1].TrimEnd('/');
}
return gatewayUrl;
}
public void Dispose()
{
_channel?.Dispose();
}
}