diff --git a/README.md b/README.md
index b4153ef..70283c2 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
# CMS Microservice
-Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
-
\ No newline at end of file
+Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
+
\ No newline at end of file
diff --git a/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj b/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj
index b364983..5b33f39 100644
--- a/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj
+++ b/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayResolver.cs b/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayResolver.cs
new file mode 100644
index 0000000..2c16d14
--- /dev/null
+++ b/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayResolver.cs
@@ -0,0 +1,9 @@
+namespace CMSMicroservice.Application.Common.Interfaces;
+
+///
+/// Resolve payment gateway by provider key (zarinpal, 20card, …).
+///
+public interface IPaymentGatewayResolver
+{
+ IPaymentGatewayService Get(string? provider);
+}
diff --git a/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayService.cs b/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayService.cs
index 704749b..e411212 100644
--- a/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayService.cs
+++ b/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayService.cs
@@ -98,6 +98,16 @@ public class PaymentRequest
/// URL بازگشت بعد از پرداخت
///
public string CallbackUrl { get; set; } = string.Empty;
+
+ ///
+ /// نام پرداختکننده (برای درگاههایی مثل ۲۰کارت)
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// شماره فاکتور / سفارش
+ ///
+ public string Invoice { get; set; } = string.Empty;
}
///
diff --git a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommandHandler.cs b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommandHandler.cs
index 42c61ec..576cd6a 100644
--- a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommandHandler.cs
+++ b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommandHandler.cs
@@ -8,6 +8,7 @@ using CMSMicroservice.Domain.Entities.Payment;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
@@ -16,20 +17,23 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
: IRequestHandler
{
private readonly IApplicationDbContext _context;
- private readonly IPaymentGatewayService _paymentGateway;
+ private readonly IPaymentGatewayResolver _paymentGatewayResolver;
+ private readonly IConfiguration _configuration;
private readonly ISender _sender;
private readonly IUserPaymentLock _paymentLock;
private readonly ILogger _logger;
public VerifyUserPackagePurchasePaymentCommandHandler(
IApplicationDbContext context,
- IPaymentGatewayService paymentGateway,
+ IPaymentGatewayResolver paymentGatewayResolver,
+ IConfiguration configuration,
ISender sender,
IUserPaymentLock paymentLock,
ILogger logger)
{
_context = context;
- _paymentGateway = paymentGateway;
+ _paymentGatewayResolver = paymentGatewayResolver;
+ _configuration = configuration;
_sender = sender;
_paymentLock = paymentLock;
_logger = logger;
@@ -105,7 +109,8 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
}
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
- var verifyResult = await _paymentGateway.VerifyPaymentAsync(
+ var gateway = _paymentGatewayResolver.Get(paymentTx?.GatewayProvider);
+ var verifyResult = await gateway.VerifyPaymentAsync(
request.Authority, request.Status, (decimal)amountInToman, ct);
if (!verifyResult.IsSuccess)
@@ -193,17 +198,30 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
await _context.SaveChangesAsync(ct);
}
- var discountMultiplier = purchase.Package?.DiscountMultiplier
+ var packagePrice = purchase.Package?.Price
?? throw new InvalidOperationException("پکیج سفارش یافت نشد");
- var discountAmount = (long)(purchase.Amount * (double)discountMultiplier);
- wallet.Balance += purchase.Amount;
+ var discountMultiplier = purchase.Package.DiscountMultiplier;
+ var discountAmount = (long)(packagePrice * (double)discountMultiplier);
+
+ var isTwentyCard = string.Equals(
+ paymentTx?.GatewayProvider, "20card", StringComparison.OrdinalIgnoreCase);
+ long balanceCredit = purchase.Amount;
+ if (isTwentyCard)
+ {
+ balanceCredit = _configuration.GetValue(
+ $"TwentyCard:Packages:{purchase.PackageId}:WalletCreditToman") ?? 0;
+ if (balanceCredit <= 0)
+ throw new InvalidOperationException("WalletCredit ۲۰کارت برای این پکیج پیکربندی نشده است");
+ }
+
+ wallet.Balance += balanceCredit;
wallet.DiscountBalance += discountAmount;
_context.UserWalletHistories.Add(new UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
- ChangeValue = purchase.Amount,
+ ChangeValue = balanceCredit,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
diff --git a/src/CMSMicroservice.Infrastructure/ConfigureServices.cs b/src/CMSMicroservice.Infrastructure/ConfigureServices.cs
index 8890d70..0f854fb 100644
--- a/src/CMSMicroservice.Infrastructure/ConfigureServices.cs
+++ b/src/CMSMicroservice.Infrastructure/ConfigureServices.cs
@@ -77,27 +77,33 @@ public static class ConfigureServices
});
}
- // Payment Gateway Service - Multi-Provider Architecture
- // پشتیبانی از درگاههای مختلف: ZarinPal, Daya, Mock
+ // Payment Gateway — default provider + multi-gateway resolver (ZarinPal + TwentyCard coexist)
var paymentProvider = configuration.GetValue("PaymentProvider", "Mock")?.ToLowerInvariant();
-
+
+ services.AddHttpClient()
+ .SetHandlerLifetime(TimeSpan.FromMinutes(5));
+ services.AddHttpClient()
+ .SetHandlerLifetime(TimeSpan.FromMinutes(5));
+
switch (paymentProvider)
{
case "zarinpal":
- services.AddHttpClient()
- .SetHandlerLifetime(TimeSpan.FromMinutes(5));
+ services.AddScoped(sp => sp.GetRequiredService());
break;
-
+
case "daya":
- services.AddHttpClient()
+ services.AddHttpClient()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
+ services.AddScoped(sp => sp.GetRequiredService());
break;
-
+
case "mock":
default:
services.AddScoped();
break;
}
+
+ services.AddScoped();
services.AddScoped(p => p.GetRequiredService());
diff --git a/src/CMSMicroservice.Infrastructure/Services/Payment/PaymentGatewayResolver.cs b/src/CMSMicroservice.Infrastructure/Services/Payment/PaymentGatewayResolver.cs
new file mode 100644
index 0000000..3aed4a3
--- /dev/null
+++ b/src/CMSMicroservice.Infrastructure/Services/Payment/PaymentGatewayResolver.cs
@@ -0,0 +1,35 @@
+using CMSMicroservice.Application.Common.Interfaces;
+using CMSMicroservice.Infrastructure.Services.Payment;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CMSMicroservice.Infrastructure.Services.Payment;
+
+public class PaymentGatewayResolver : IPaymentGatewayResolver
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IConfiguration _configuration;
+
+ public PaymentGatewayResolver(IServiceProvider serviceProvider, IConfiguration configuration)
+ {
+ _serviceProvider = serviceProvider;
+ _configuration = configuration;
+ }
+
+ public IPaymentGatewayService Get(string? provider)
+ {
+ var key = Normalize(provider)
+ ?? Normalize(_configuration["PaymentProvider"])
+ ?? "zarinpal";
+
+ return key switch
+ {
+ "20card" or "twentycard" => _serviceProvider.GetRequiredService(),
+ "zarinpal" => _serviceProvider.GetRequiredService(),
+ _ => _serviceProvider.GetRequiredService()
+ };
+ }
+
+ private static string? Normalize(string? value)
+ => string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
+}
diff --git a/src/CMSMicroservice.Infrastructure/Services/Payment/TwentyCardPaymentService.cs b/src/CMSMicroservice.Infrastructure/Services/Payment/TwentyCardPaymentService.cs
new file mode 100644
index 0000000..01396a3
--- /dev/null
+++ b/src/CMSMicroservice.Infrastructure/Services/Payment/TwentyCardPaymentService.cs
@@ -0,0 +1,264 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using CMSMicroservice.Application.Common.Interfaces;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace CMSMicroservice.Infrastructure.Services.Payment;
+
+///
+/// درگاه پرداخت بیستکارت (20Card IPG Merchant API).
+/// مبلغ ورودی به تومان است (مثل بقیهٔ سیستم).
+///
+public class TwentyCardPaymentService : IPaymentGatewayService
+{
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+ private readonly string _terminalId;
+ private readonly string _terminalUsername;
+ private readonly string _terminalPassword;
+
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ };
+
+ public TwentyCardPaymentService(
+ HttpClient httpClient,
+ IConfiguration configuration,
+ ILogger logger)
+ {
+ _httpClient = httpClient;
+ _logger = logger;
+
+ var baseUrl = configuration["TwentyCard:BaseUrl"]
+ ?? throw new InvalidOperationException("TwentyCard:BaseUrl is not configured.");
+ _terminalId = configuration["TwentyCard:TerminalId"]
+ ?? throw new InvalidOperationException("TwentyCard:TerminalId is not configured.");
+ _terminalUsername = configuration["TwentyCard:TerminalUsername"]
+ ?? throw new InvalidOperationException("TwentyCard:TerminalUsername is not configured.");
+ _terminalPassword = configuration["TwentyCard:TerminalPassword"]
+ ?? throw new InvalidOperationException("TwentyCard:TerminalPassword is not configured.");
+
+ _httpClient.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
+ _logger.LogInformation("TwentyCard payment service initialized. BaseUrl={BaseUrl}", baseUrl);
+ }
+
+ public async Task InitiatePaymentAsync(
+ PaymentRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var amount = (long)request.Amount;
+ var payerName = string.IsNullOrWhiteSpace(request.Name)
+ ? (string.IsNullOrWhiteSpace(request.Mobile) ? "کاربر" : request.Mobile)
+ : request.Name.Trim();
+
+ var body = new Dictionary
+ {
+ ["terminal_id"] = _terminalId,
+ ["terminal_username"] = _terminalUsername,
+ ["terminal_password"] = _terminalPassword,
+ ["amount"] = amount,
+ ["name"] = payerName,
+ ["mobile"] = request.Mobile ?? string.Empty,
+ ["invoice"] = string.IsNullOrWhiteSpace(request.Invoice)
+ ? request.UserId.ToString()
+ : request.Invoice,
+ ["callback"] = request.CallbackUrl,
+ ["description"] = request.Description
+ };
+
+ _logger.LogInformation(
+ "TwentyCard payment request: Amount={Amount} Toman, User={UserId}, Invoice={Invoice}",
+ amount, request.UserId, body["invoice"]);
+
+ using var content = new StringContent(
+ JsonSerializer.Serialize(body, JsonOptions), Encoding.UTF8, "application/json");
+
+ var response = await _httpClient.PostAsync("api/pgw/merchant/request", content, cancellationToken);
+ var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogError(
+ "TwentyCard HTTP error on request: StatusCode={StatusCode}, Body={Body}",
+ (int)response.StatusCode, responseBody);
+
+ return new PaymentInitiateResult
+ {
+ IsSuccess = false,
+ ErrorMessage = $"خطای ارتباط با درگاه ۲۰کارت (HTTP {(int)response.StatusCode})"
+ };
+ }
+
+ var result = JsonSerializer.Deserialize(responseBody, JsonOptions);
+ var authority = result?.Data?.Authority;
+ var redirectUrl = result?.Data?.RedirectUrl;
+
+ if (string.IsNullOrWhiteSpace(authority) || string.IsNullOrWhiteSpace(redirectUrl))
+ {
+ _logger.LogError("TwentyCard request failed. Body={Body}", responseBody);
+ return new PaymentInitiateResult
+ {
+ IsSuccess = false,
+ ErrorMessage = result?.Message ?? "ایجاد تراکنش ۲۰کارت ناموفق بود"
+ };
+ }
+
+ return new PaymentInitiateResult
+ {
+ IsSuccess = true,
+ RefId = authority,
+ GatewayUrl = redirectUrl
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "TwentyCard InitiatePayment failed");
+ return new PaymentInitiateResult
+ {
+ IsSuccess = false,
+ ErrorMessage = "خطا در ارتباط با درگاه ۲۰کارت"
+ };
+ }
+ }
+
+ public Task VerifyPaymentAsync(
+ string refId,
+ string verificationToken,
+ CancellationToken cancellationToken = default)
+ => throw new NotImplementedException("TwentyCard requires VerifyPaymentAsync with amount.");
+
+ public async Task VerifyPaymentAsync(
+ string refId,
+ string verificationToken,
+ decimal amountInToman,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ if (!string.Equals(verificationToken, "OK", StringComparison.OrdinalIgnoreCase))
+ {
+ return new PaymentVerificationResult
+ {
+ IsSuccess = false,
+ RefId = refId,
+ Message = "پرداخت توسط کاربر لغو شد یا ناموفق بود",
+ VerificationCode = 0
+ };
+ }
+
+ var body = new Dictionary
+ {
+ ["terminal_id"] = _terminalId,
+ ["terminal_username"] = _terminalUsername,
+ ["terminal_password"] = _terminalPassword,
+ ["authority"] = refId,
+ ["amount"] = (long)amountInToman
+ };
+
+ using var content = new StringContent(
+ JsonSerializer.Serialize(body, JsonOptions), Encoding.UTF8, "application/json");
+
+ var response = await _httpClient.PostAsync("api/pgw/merchant/verify", content, cancellationToken);
+ var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "TwentyCard verify response: Authority={Authority}, StatusCode={StatusCode}, Body={Body}",
+ refId, (int)response.StatusCode, responseBody);
+
+ var result = JsonSerializer.Deserialize(responseBody, JsonOptions);
+
+ var paid = result?.Status?.Paid == true;
+ var verified = result?.Status?.Verified == true;
+ var noError = result?.Error != true;
+
+ if (response.IsSuccessStatusCode && noError && paid && verified)
+ {
+ return new PaymentVerificationResult
+ {
+ IsSuccess = true,
+ RefId = refId,
+ TrackingCode = refId,
+ Message = result?.Message ?? "تراکنش با موفقیت تایید شد",
+ VerificationCode = result?.Code ?? 200
+ };
+ }
+
+ // Code 103 = already verified — treat as success for idempotency
+ if (result?.Code == 103)
+ {
+ return new PaymentVerificationResult
+ {
+ IsSuccess = true,
+ RefId = refId,
+ TrackingCode = refId,
+ Message = result.Message ?? "تراکنش قبلاً تایید شده است",
+ VerificationCode = 103
+ };
+ }
+
+ return new PaymentVerificationResult
+ {
+ IsSuccess = false,
+ RefId = refId,
+ Message = result?.Message ?? "تایید تراکنش ۲۰کارت ناموفق بود",
+ VerificationCode = result?.Code ?? (int)response.StatusCode
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "TwentyCard VerifyPayment failed. Authority={Authority}", refId);
+ return new PaymentVerificationResult
+ {
+ IsSuccess = false,
+ RefId = refId,
+ Message = "خطا در تایید تراکنش ۲۰کارت"
+ };
+ }
+ }
+
+ public Task ProcessPayoutAsync(
+ PayoutRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(new PayoutResult
+ {
+ IsSuccess = false,
+ Message = "واریز از طریق درگاه ۲۰کارت پشتیبانی نمیشود"
+ });
+ }
+
+ private sealed class TwentyCardRequestResponse
+ {
+ public string? Message { get; set; }
+ public TwentyCardRequestData? Data { get; set; }
+ }
+
+ private sealed class TwentyCardRequestData
+ {
+ public string? Authority { get; set; }
+ public string? RedirectUrl { get; set; }
+ }
+
+ private sealed class TwentyCardVerifyResponse
+ {
+ public bool Error { get; set; }
+ public int Code { get; set; }
+ public string? Message { get; set; }
+ public TwentyCardVerifyStatus? Status { get; set; }
+ }
+
+ private sealed class TwentyCardVerifyStatus
+ {
+ public bool Paid { get; set; }
+ public bool Verified { get; set; }
+ }
+}
diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj
index 79ca2d5..b3fd16c 100644
--- a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj
+++ b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj
@@ -3,7 +3,7 @@
net9.0
enable
enable
- 0.0.211
+ 0.0.212
None
False
False
diff --git a/src/CMSMicroservice.Protobuf/Protos/package.proto b/src/CMSMicroservice.Protobuf/Protos/package.proto
index 3b88874..3acbd26 100644
--- a/src/CMSMicroservice.Protobuf/Protos/package.proto
+++ b/src/CMSMicroservice.Protobuf/Protos/package.proto
@@ -249,6 +249,8 @@ message CustomerPurchasePackageRequest
int64 package_id = 1;
PurchaseMethodEnum purchase_method = 2;
string callback_url = 3;
+ // "zarinpal" (default) | "20card"
+ string gateway_provider = 4;
}
message CustomerPurchasePackageResponse
diff --git a/src/CMSMicroservice.WebApi/Services/PackageService.cs b/src/CMSMicroservice.WebApi/Services/PackageService.cs
index 89b333f..2c2410d 100644
--- a/src/CMSMicroservice.WebApi/Services/PackageService.cs
+++ b/src/CMSMicroservice.WebApi/Services/PackageService.cs
@@ -32,7 +32,7 @@ public class PackageService : PackageContract.PackageContractBase
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
- private readonly IPaymentGatewayService _paymentGateway;
+ private readonly IPaymentGatewayResolver _paymentGatewayResolver;
private readonly IConfiguration _configuration;
private readonly IUserPaymentLock _paymentLock;
@@ -41,7 +41,7 @@ public class PackageService : PackageContract.PackageContractBase
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService,
- IPaymentGatewayService paymentGateway,
+ IPaymentGatewayResolver paymentGatewayResolver,
IConfiguration configuration,
IUserPaymentLock paymentLock)
{
@@ -49,7 +49,7 @@ public class PackageService : PackageContract.PackageContractBase
_sender = sender;
_context = context;
_currentUserService = currentUserService;
- _paymentGateway = paymentGateway;
+ _paymentGatewayResolver = paymentGatewayResolver;
_configuration = configuration;
_paymentLock = paymentLock;
}
@@ -171,19 +171,38 @@ public class PackageService : PackageContract.PackageContractBase
if (package == null)
throw new RpcException(new Status(StatusCode.NotFound, "پکیج مورد نظر یافت نشد"));
-
- // Create transaction
+
+ var gatewayProvider = NormalizeGatewayProvider(request.GatewayProvider);
+ var isTwentyCard = gatewayProvider == "20card";
+
+ long gatewayAmount = package.Price;
+ if (isTwentyCard)
+ {
+ var packageKey = $"TwentyCard:Packages:{package.Id}";
+ gatewayAmount = _configuration.GetValue($"{packageKey}:GatewayAmountToman") ?? 0;
+ var walletCredit = _configuration.GetValue($"{packageKey}:WalletCreditToman") ?? 0;
+ if (gatewayAmount <= 0 || walletCredit <= 0)
+ {
+ throw new RpcException(new Status(
+ StatusCode.FailedPrecondition,
+ "پرداخت ۲۰کارت برای این پکیج پیکربندی نشده است"));
+ }
+ }
+
+ // Create transaction — 20card: Amount = gateway charge; zarinpal: Package.Price
var transaction = new CMSMicroservice.Domain.Entities.Transaction
{
- Amount = package.Price,
- Description = $"خرید پکیج {package.Title}",
+ Amount = gatewayAmount,
+ Description = isTwentyCard
+ ? $"خرید پکیج {package.Title} (۲۰کارت)"
+ : $"خرید پکیج {package.Title}",
PaymentStatus = Domain.Enums.PaymentStatus.Pending,
Type = Domain.Enums.TransactionType.Buy
};
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(cancellationToken);
- // Create purchase record
+ // Create purchase record — UPP.Amount always Package.Price (catalog)
var purchaseMethod = request.PurchaseMethod == PurchaseMethodEnum.PurchaseMethodGateway
? Domain.Enums.PackagePurchaseMethod.DirectPurchase
: Domain.Enums.PackagePurchaseMethod.DayaLoan;
@@ -204,18 +223,23 @@ public class PackageService : PackageContract.PackageContractBase
var user = await _context.Users
.AsNoTracking()
.Where(u => u.Id == userId)
- .Select(u => new { u.Mobile })
+ .Select(u => new { u.Mobile, u.FirstName, u.LastName })
.FirstOrDefaultAsync(cancellationToken);
// Callback URL از config — نه از ورودی کاربر (امنیت)
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
var callbackUrl = $"{frontOfficeBaseUrl}/profile/payment-callback?orderId={purchase.Id}";
+
+ var paymentGateway = _paymentGatewayResolver.Get(gatewayProvider);
+ var payerName = $"{user?.FirstName} {user?.LastName}".Trim();
- var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
+ var paymentResult = await paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
- Amount = package.Price,
+ Amount = gatewayAmount,
UserId = userId,
Mobile = user?.Mobile ?? string.Empty,
+ Name = payerName,
+ Invoice = purchase.Id.ToString(),
Description = $"خرید پکیج {package.Title}",
CallbackUrl = callbackUrl
}, cancellationToken);
@@ -235,12 +259,14 @@ public class PackageService : PackageContract.PackageContractBase
// Save RefId
transaction.RefId = paymentResult.RefId;
- // ثبت PaymentTransaction
+ // ثبت PaymentTransaction — Amount = gateway amount
var paymentTx = new PaymentTransaction
{
- GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
- MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
- Amount = package.Price,
+ GatewayProvider = gatewayProvider,
+ MerchantId = isTwentyCard
+ ? (_configuration["TwentyCard:TerminalId"] ?? "")
+ : (_configuration["ZarinPal:MerchantId"] ?? ""),
+ Amount = gatewayAmount,
CallbackUrl = callbackUrl,
Description = $"خرید پکیج {package.Title}",
Mobile = user?.Mobile,
@@ -264,6 +290,14 @@ public class PackageService : PackageContract.PackageContractBase
Authority = paymentResult.RefId ?? string.Empty
};
}
+
+ private static string NormalizeGatewayProvider(string? provider)
+ {
+ if (string.IsNullOrWhiteSpace(provider))
+ return "zarinpal";
+ var key = provider.Trim().ToLowerInvariant();
+ return key is "20card" or "twentycard" ? "20card" : "zarinpal";
+ }
public override async Task CustomerVerifyPackagePurchase(
CustomerVerifyPackagePurchaseRequest request,
diff --git a/src/CMSMicroservice.WebApi/appsettings.Staging.json b/src/CMSMicroservice.WebApi/appsettings.Staging.json
index ba9c060..209616a 100644
--- a/src/CMSMicroservice.WebApi/appsettings.Staging.json
+++ b/src/CMSMicroservice.WebApi/appsettings.Staging.json
@@ -4,6 +4,23 @@
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
"UseSandbox": true
},
+ "TwentyCard": {
+ "BaseUrl": "https://sandbox.20card.ir/ipg",
+ "TerminalId": "",
+ "TerminalUsername": "",
+ "TerminalPassword": "",
+ "CreditPortalUrl": "https://20card.ir",
+ "Packages": {
+ "4": {
+ "GatewayAmountToman": 9700000,
+ "WalletCreditToman": 13000000
+ },
+ "1": {
+ "GatewayAmountToman": 97000000,
+ "WalletCreditToman": 130000000
+ }
+ }
+ },
"CmsBaseUrl": "https://cms.se.kbs1.ir",
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
"FMS": {
diff --git a/src/CMSMicroservice.WebApi/appsettings.json b/src/CMSMicroservice.WebApi/appsettings.json
index 006a80b..8be1e8b 100644
--- a/src/CMSMicroservice.WebApi/appsettings.json
+++ b/src/CMSMicroservice.WebApi/appsettings.json
@@ -4,6 +4,23 @@
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
"UseSandbox": true
},
+ "TwentyCard": {
+ "BaseUrl": "https://sandbox.20card.ir/ipg",
+ "TerminalId": "",
+ "TerminalUsername": "",
+ "TerminalPassword": "",
+ "CreditPortalUrl": "https://20card.ir",
+ "Packages": {
+ "4": {
+ "GatewayAmountToman": 9700000,
+ "WalletCreditToman": 13000000
+ },
+ "1": {
+ "GatewayAmountToman": 97000000,
+ "WalletCreditToman": 130000000
+ }
+ }
+ },
"CmsBaseUrl": "https://localhost:32846",
"FrontOfficeBaseUrl": "http://localhost:5268",
"FMS": {