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
@@ -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();
}
}
@@ -73,9 +73,6 @@
<Protobuf Include="Protos\sitepage.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<!-- Image Resolver Service -->
<Protobuf Include="Protos\imageresolver.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<!-- PYMS (Payment Microservice) - gRPC Client only -->
<Protobuf Include="Protos\pyms\pyms_public_messages.proto" ProtoRoot="Protos\" GrpcServices="Client" />
<Protobuf Include="Protos\pyms\pyms_transaction.proto" ProtoRoot="Protos\" GrpcServices="Client" />
</ItemGroup>
<Target Name="PushToFoursatNuget" AfterTargets="Pack" Condition="'$(CI)' != 'true'">
@@ -1,41 +0,0 @@
syntax = "proto3";
package pyms_messages;
option csharp_namespace = "CMSMicroservice.Protobuf.Protos.PYMS";
service PYMSPublicMessageContract{}
message PaginationState
{
int32 page_number = 1;
int32 page_size = 2;
}
message MetaData
{
int64 current_page = 1;
int64 total_page = 2;
int64 page_size = 3;
int64 total_count = 4;
bool has_previous = 5;
bool has_next = 6;
}
message DecimalValue
{
int64 units = 1;
sfixed32 nanos = 2;
}
enum TransactionTypeEnum
{
Real = 0;
Sandbox = 1;
}
enum CurrencyEnum
{
IRR = 0;
IRT = 1;
}
@@ -1,279 +0,0 @@
syntax = "proto3";
package pyms_transaction;
import "pyms/pyms_public_messages.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
option csharp_namespace = "CMSMicroservice.Protobuf.Protos.PYMS.Transaction";
service TransactionContract
{
rpc CreateNewTransaction(CreateNewTransactionRequest) returns (CreateNewTransactionResponse){
option (google.api.http) = {
post: "/CreateNewTransaction"
body: "*"
};
};
rpc UpdateTransaction(UpdateTransactionRequest) returns (google.protobuf.Empty){
option (google.api.http) = {
put: "/UpdateTransaction"
body: "*"
};
};
rpc DeleteTransaction(DeleteTransactionRequest) returns (google.protobuf.Empty){
option (google.api.http) = {
delete: "/DeleteTransaction"
body: "*"
};
};
rpc GetTransaction(GetTransactionRequest) returns (GetTransactionResponse){
option (google.api.http) = {
get: "/GetTransaction"
};
};
rpc GetAllTransactionByFilter(GetAllTransactionByFilterRequest) returns (GetAllTransactionByFilterResponse){
option (google.api.http) = {
get: "/GetAllTransactionByFilter"
};
};
rpc PaymentRequest(PaymentRequestRequest) returns (PaymentRequestResponse){
option (google.api.http) = {
post: "/PaymentRequest"
body: "*"
};
};
rpc PaymentVerification(PaymentVerificationRequest) returns (PaymentVerificationResponse){
option (google.api.http) = {
post: "/PaymentVerification"
body: "*"
};
};
}
message CreateNewTransactionRequest
{
string merchant_id = 1;
int64 amount = 2;
string callback_url = 3;
string description = 4;
google.protobuf.StringValue mobile = 5;
google.protobuf.StringValue email = 6;
google.protobuf.Int32Value request_status_code = 7;
google.protobuf.StringValue request_status_message = 8;
google.protobuf.StringValue authority = 9;
google.protobuf.StringValue fee_type = 10;
google.protobuf.Int64Value fee = 11;
oneof Currency_item
{
pyms_messages.CurrencyEnum currency = 12;
}
bool payment_status = 13;
google.protobuf.Int32Value verification_status_code = 14;
google.protobuf.StringValue verification_status_message = 15;
google.protobuf.StringValue card_hash = 16;
google.protobuf.StringValue card_pan = 17;
google.protobuf.StringValue ref_id = 18;
google.protobuf.StringValue order_id = 19;
oneof Type_item
{
pyms_messages.TransactionTypeEnum type = 20;
}
}
message CreateNewTransactionResponse
{
int64 id = 1;
}
message UpdateTransactionRequest
{
int64 id = 1;
string merchant_id = 2;
int64 amount = 3;
string callback_url = 4;
string description = 5;
google.protobuf.StringValue mobile = 6;
google.protobuf.StringValue email = 7;
google.protobuf.Int32Value request_status_code = 8;
google.protobuf.StringValue request_status_message = 9;
google.protobuf.StringValue authority = 10;
google.protobuf.StringValue fee_type = 11;
google.protobuf.Int64Value fee = 12;
oneof Currency_item
{
pyms_messages.CurrencyEnum currency = 13;
}
bool payment_status = 14;
google.protobuf.Int32Value verification_status_code = 15;
google.protobuf.StringValue verification_status_message = 16;
google.protobuf.StringValue card_hash = 17;
google.protobuf.StringValue card_pan = 18;
google.protobuf.StringValue ref_id = 19;
google.protobuf.StringValue order_id = 20;
oneof Type_item
{
pyms_messages.TransactionTypeEnum type = 21;
}
}
message DeleteTransactionRequest
{
int64 id = 1;
}
message GetTransactionRequest
{
google.protobuf.Int64Value id = 1;
google.protobuf.StringValue authority = 2;
}
message GetTransactionResponse
{
int64 id = 1;
string merchant_id = 2;
int64 amount = 3;
string callback_url = 4;
string description = 5;
google.protobuf.StringValue mobile = 6;
google.protobuf.StringValue email = 7;
google.protobuf.Int32Value request_status_code = 8;
google.protobuf.StringValue request_status_message = 9;
google.protobuf.StringValue authority = 10;
google.protobuf.StringValue fee_type = 11;
google.protobuf.Int64Value fee = 12;
oneof Currency_item
{
pyms_messages.CurrencyEnum currency = 13;
}
bool payment_status = 14;
google.protobuf.Int32Value verification_status_code = 15;
google.protobuf.StringValue verification_status_message = 16;
google.protobuf.StringValue card_hash = 17;
google.protobuf.StringValue card_pan = 18;
google.protobuf.StringValue ref_id = 19;
google.protobuf.StringValue order_id = 20;
oneof Type_item
{
pyms_messages.TransactionTypeEnum type = 21;
}
}
message GetAllTransactionByFilterRequest
{
pyms_messages.PaginationState pagination_state = 1;
google.protobuf.StringValue sort_by = 2;
GetAllTransactionByFilterFilter filter = 3;
}
message GetAllTransactionByFilterFilter
{
google.protobuf.Int64Value id = 1;
google.protobuf.StringValue merchant_id = 2;
google.protobuf.Int64Value amount = 3;
google.protobuf.StringValue callback_url = 4;
google.protobuf.StringValue description = 5;
google.protobuf.StringValue mobile = 6;
google.protobuf.StringValue email = 7;
google.protobuf.Int32Value request_status_code = 8;
google.protobuf.StringValue request_status_message = 9;
google.protobuf.StringValue authority = 10;
google.protobuf.StringValue fee_type = 11;
google.protobuf.Int64Value fee = 12;
oneof Currency_item
{
pyms_messages.CurrencyEnum currency = 13;
}
google.protobuf.BoolValue payment_status = 14;
google.protobuf.Int32Value verification_status_code = 15;
google.protobuf.StringValue verification_status_message = 16;
google.protobuf.StringValue card_hash = 17;
google.protobuf.StringValue card_pan = 18;
google.protobuf.StringValue ref_id = 19;
google.protobuf.StringValue order_id = 20;
oneof Type_item
{
pyms_messages.TransactionTypeEnum type = 21;
}
}
message GetAllTransactionByFilterResponse
{
pyms_messages.MetaData meta_data = 1;
repeated GetAllTransactionByFilterResponseModel models = 2;
}
message GetAllTransactionByFilterResponseModel
{
int64 id = 1;
string merchant_id = 2;
int64 amount = 3;
string callback_url = 4;
string description = 5;
google.protobuf.StringValue mobile = 6;
google.protobuf.StringValue email = 7;
google.protobuf.Int32Value request_status_code = 8;
google.protobuf.StringValue request_status_message = 9;
google.protobuf.StringValue authority = 10;
google.protobuf.StringValue fee_type = 11;
google.protobuf.Int64Value fee = 12;
oneof Currency_item
{
pyms_messages.CurrencyEnum currency = 13;
}
bool payment_status = 14;
google.protobuf.Int32Value verification_status_code = 15;
google.protobuf.StringValue verification_status_message = 16;
google.protobuf.StringValue card_hash = 17;
google.protobuf.StringValue card_pan = 18;
google.protobuf.StringValue ref_id = 19;
google.protobuf.StringValue order_id = 20;
oneof Type_item
{
pyms_messages.TransactionTypeEnum type = 21;
}
}
message PaymentRequestRequest
{
google.protobuf.StringValue merchant_id = 1;
int64 amount = 2;
string callback_url = 3;
google.protobuf.StringValue description = 4;
google.protobuf.StringValue mobile = 5;
google.protobuf.StringValue email = 6;
oneof Currency_item
{
pyms_messages.CurrencyEnum currency = 7;
}
oneof Type_item
{
pyms_messages.TransactionTypeEnum type = 8;
}
google.protobuf.StringValue order_id = 9;
}
message PaymentRequestResponse
{
string payment_g_w_url = 1;
}
message PaymentVerificationRequest
{
string authority = 1;
string status = 2;
}
message PaymentVerificationResponse
{
int64 id = 1;
bool payment_status = 2;
string message = 3;
google.protobuf.StringValue ref_id = 4;
google.protobuf.StringValue order_id = 5;
google.protobuf.Int32Value verification_status_code = 6;
}
@@ -1,8 +1,5 @@
{
"PaymentProvider": "pyms",
"PYMS": {
"Address": "https://pyms.se.kbs1.ir"
},
"PaymentProvider": "zarinpal",
"ZarinPal": {
"MerchantId": "00000000-0000-0000-0000-000000000000",
"UseSandbox": true
+1 -4
View File
@@ -1,8 +1,5 @@
{
"PaymentProvider": "pyms",
"PYMS": {
"Address": "http://pyms-svc.default.svc.cluster.local:80"
},
"PaymentProvider": "zarinpal",
"ZarinPal": {
"MerchantId": "6b098fc8-f490-47a1-aac3-1de1a1b84404",
"UseSandbox": true