refactor: remove PYMS microservice, use ZarinPal directly in CMS
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m26s
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:
@@ -75,7 +75,7 @@ public static class ConfigureServices
|
||||
}
|
||||
|
||||
// Payment Gateway Service - Multi-Provider Architecture
|
||||
// پشتیبانی از درگاههای مختلف: ZarinPal, Daya, PYMS, Mock
|
||||
// پشتیبانی از درگاههای مختلف: ZarinPal, Daya, Mock
|
||||
var paymentProvider = configuration.GetValue<string>("PaymentProvider", "Mock")?.ToLowerInvariant();
|
||||
|
||||
switch (paymentProvider)
|
||||
@@ -90,11 +90,6 @@ public static class ConfigureServices
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||
break;
|
||||
|
||||
case "pyms":
|
||||
// PYMS (Payment Microservice) — ارتباط gRPC با سرویس پرداخت مستقل
|
||||
services.AddSingleton<IPaymentGatewayService, PYMSPaymentService>();
|
||||
break;
|
||||
|
||||
case "mock":
|
||||
default:
|
||||
services.AddScoped<IPaymentGatewayService, MockPaymentGatewayService>();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user