feat(payment): enhance TwentyCard payment integration and update configuration
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled

- Added support for optional Email and NationalId fields in PaymentRequest for TwentyCard transactions.
- Updated TwentyCardPaymentService to handle multipart/form-data requests and improved error handling.
- Modified appsettings and configuration files for both production and staging environments to include updated BaseUrl and credentials for TwentyCard.
- Updated README to reflect the new documentation location.
This commit is contained in:
masoodafar-web
2026-09-21 21:31:17 +03:30
parent 2b958bd214
commit 29426022aa
8 changed files with 91 additions and 29 deletions
+17
View File
@@ -24,6 +24,23 @@ stringData:
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8", "MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
"UseSandbox": false "UseSandbox": false
}, },
"TwentyCard": {
"BaseUrl": "https://gateway.20card.ir/ipg",
"TerminalId": "165674",
"TerminalUsername": "ea5y1biehi",
"TerminalPassword": "SaLamAT@1404!",
"CreditPortalUrl": "https://userpanel.20card.ir/",
"Packages": {
"4": {
"GatewayAmountToman": 9700000,
"WalletCreditToman": 13000000
},
"1": {
"GatewayAmountToman": 97000000,
"WalletCreditToman": 130000000
}
}
},
"CmsBaseUrl": "https://cms.kbs2.ir", "CmsBaseUrl": "https://cms.kbs2.ir",
"FrontOfficeBaseUrl": "https://kbs2.ir", "FrontOfficeBaseUrl": "https://kbs2.ir",
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=", "JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
+17
View File
@@ -24,6 +24,23 @@ stringData:
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8", "MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
"UseSandbox": true "UseSandbox": true
}, },
"TwentyCard": {
"BaseUrl": "https://gateway.20card.ir/ipg",
"TerminalId": "165674",
"TerminalUsername": "ea5y1biehi",
"TerminalPassword": "SaLamAT@1404!",
"CreditPortalUrl": "https://userpanel.20card.ir/",
"Packages": {
"4": {
"GatewayAmountToman": 9700000,
"WalletCreditToman": 13000000
},
"1": {
"GatewayAmountToman": 97000000,
"WalletCreditToman": 130000000
}
}
},
"CmsBaseUrl": "https://cms.se.kbs1.ir", "CmsBaseUrl": "https://cms.se.kbs1.ir",
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir", "FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
"FMS": { "FMS": {
@@ -108,6 +108,16 @@ public class PaymentRequest
/// شماره فاکتور / سفارش /// شماره فاکتور / سفارش
/// </summary> /// </summary>
public string Invoice { get; set; } = string.Empty; public string Invoice { get; set; } = string.Empty;
/// <summary>
/// ایمیل (اختیاری — برای ۲۰کارت)
/// </summary>
public string? Email { get; set; }
/// <summary>
/// کد ملی (اختیاری — برای ۲۰کارت)
/// </summary>
public string? NationalId { get; set; }
} }
/// <summary> /// <summary>
@@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Application.Common.Interfaces;
@@ -11,7 +10,7 @@ namespace CMSMicroservice.Infrastructure.Services.Payment;
/// <summary> /// <summary>
/// درگاه پرداخت بیست‌کارت (20Card IPG Merchant API). /// درگاه پرداخت بیست‌کارت (20Card IPG Merchant API).
/// مبلغ ورودی به تومان است (مثل بقیهٔ سیستم). /// مبلغ ورودی به تومان است. درخواست‌ها به‌صورت multipart/form-data ارسال می‌شوند.
/// </summary> /// </summary>
public class TwentyCardPaymentService : IPaymentGatewayService public class TwentyCardPaymentService : IPaymentGatewayService
{ {
@@ -55,12 +54,15 @@ public class TwentyCardPaymentService : IPaymentGatewayService
{ {
try try
{ {
var amount = (long)request.Amount; var amount = ((long)request.Amount).ToString();
var payerName = string.IsNullOrWhiteSpace(request.Name) var payerName = string.IsNullOrWhiteSpace(request.Name)
? (string.IsNullOrWhiteSpace(request.Mobile) ? "کاربر" : request.Mobile) ? (string.IsNullOrWhiteSpace(request.Mobile) ? "کاربر" : request.Mobile)
: request.Name.Trim(); : request.Name.Trim();
var invoice = string.IsNullOrWhiteSpace(request.Invoice)
? request.UserId.ToString()
: request.Invoice;
var body = new Dictionary<string, object?> using var content = BuildForm(new Dictionary<string, string>
{ {
["terminal_id"] = _terminalId, ["terminal_id"] = _terminalId,
["terminal_username"] = _terminalUsername, ["terminal_username"] = _terminalUsername,
@@ -68,19 +70,19 @@ public class TwentyCardPaymentService : IPaymentGatewayService
["amount"] = amount, ["amount"] = amount,
["name"] = payerName, ["name"] = payerName,
["mobile"] = request.Mobile ?? string.Empty, ["mobile"] = request.Mobile ?? string.Empty,
["invoice"] = string.IsNullOrWhiteSpace(request.Invoice) ["invoice"] = invoice,
? request.UserId.ToString()
: request.Invoice,
["callback"] = request.CallbackUrl, ["callback"] = request.CallbackUrl,
["description"] = request.Description ["description"] = request.Description ?? string.Empty
}; });
if (!string.IsNullOrWhiteSpace(request.Email))
content.Add(new StringContent(request.Email), "email");
if (!string.IsNullOrWhiteSpace(request.NationalId))
content.Add(new StringContent(request.NationalId), "national_id");
_logger.LogInformation( _logger.LogInformation(
"TwentyCard payment request: Amount={Amount} Toman, User={UserId}, Invoice={Invoice}", "TwentyCard payment request: Amount={Amount} Toman, User={UserId}, Invoice={Invoice}",
amount, request.UserId, body["invoice"]); amount, request.UserId, 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 response = await _httpClient.PostAsync("api/pgw/merchant/request", content, cancellationToken);
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken); var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
@@ -108,7 +110,9 @@ public class TwentyCardPaymentService : IPaymentGatewayService
return new PaymentInitiateResult return new PaymentInitiateResult
{ {
IsSuccess = false, IsSuccess = false,
ErrorMessage = result?.Message ?? "ایجاد تراکنش ۲۰کارت ناموفق بود" ErrorMessage = result?.Message
?? result?.Error?.Message
?? "ایجاد تراکنش ۲۰کارت ناموفق بود"
}; };
} }
@@ -155,17 +159,14 @@ public class TwentyCardPaymentService : IPaymentGatewayService
}; };
} }
var body = new Dictionary<string, object?> using var content = BuildForm(new Dictionary<string, string>
{ {
["terminal_id"] = _terminalId, ["terminal_id"] = _terminalId,
["terminal_username"] = _terminalUsername, ["terminal_username"] = _terminalUsername,
["terminal_password"] = _terminalPassword, ["terminal_password"] = _terminalPassword,
["authority"] = refId, ["authority"] = refId,
["amount"] = (long)amountInToman ["amount"] = ((long)amountInToman).ToString()
}; });
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 response = await _httpClient.PostAsync("api/pgw/merchant/verify", content, cancellationToken);
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken); var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
@@ -236,10 +237,25 @@ public class TwentyCardPaymentService : IPaymentGatewayService
}); });
} }
private static MultipartFormDataContent BuildForm(Dictionary<string, string> fields)
{
var form = new MultipartFormDataContent();
foreach (var (key, value) in fields)
form.Add(new StringContent(value ?? string.Empty), key);
return form;
}
private sealed class TwentyCardRequestResponse private sealed class TwentyCardRequestResponse
{ {
public string? Message { get; set; } public string? Message { get; set; }
public TwentyCardRequestData? Data { get; set; } public TwentyCardRequestData? Data { get; set; }
public TwentyCardErrorBody? Error { get; set; }
}
private sealed class TwentyCardErrorBody
{
public string? Code { get; set; }
public string? Message { get; set; }
} }
private sealed class TwentyCardRequestData private sealed class TwentyCardRequestData
@@ -223,7 +223,7 @@ public class PackageService : PackageContract.PackageContractBase
var user = await _context.Users var user = await _context.Users
.AsNoTracking() .AsNoTracking()
.Where(u => u.Id == userId) .Where(u => u.Id == userId)
.Select(u => new { u.Mobile, u.FirstName, u.LastName }) .Select(u => new { u.Mobile, u.FirstName, u.LastName, u.Email, u.NationalCode })
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
// Callback URL از config — نه از ورودی کاربر (امنیت) // Callback URL از config — نه از ورودی کاربر (امنیت)
@@ -241,7 +241,9 @@ public class PackageService : PackageContract.PackageContractBase
Name = payerName, Name = payerName,
Invoice = purchase.Id.ToString(), Invoice = purchase.Id.ToString(),
Description = $"خرید پکیج {package.Title}", Description = $"خرید پکیج {package.Title}",
CallbackUrl = callbackUrl CallbackUrl = callbackUrl,
Email = user?.Email,
NationalId = user?.NationalCode
}, cancellationToken); }, cancellationToken);
if (!paymentResult.IsSuccess) if (!paymentResult.IsSuccess)
@@ -5,11 +5,11 @@
"UseSandbox": true "UseSandbox": true
}, },
"TwentyCard": { "TwentyCard": {
"BaseUrl": "https://gateway.20card.ir", "BaseUrl": "https://gateway.20card.ir/ipg",
"TerminalId": "165674", "TerminalId": "165674",
"TerminalUsername": "ea5y1biehi", "TerminalUsername": "ea5y1biehi",
"TerminalPassword": "ea5y1biehi", "TerminalPassword": "SaLamAT@1404!",
"CreditPortalUrl": "https://20card.ir", "CreditPortalUrl": "https://userpanel.20card.ir/",
"Packages": { "Packages": {
"4": { "4": {
"GatewayAmountToman": 9700000, "GatewayAmountToman": 9700000,
+3 -3
View File
@@ -5,11 +5,11 @@
"UseSandbox": true "UseSandbox": true
}, },
"TwentyCard": { "TwentyCard": {
"BaseUrl": "https://gateway.20card.ir", "BaseUrl": "https://gateway.20card.ir/ipg",
"TerminalId": "165674", "TerminalId": "165674",
"TerminalUsername": "ea5y1biehi", "TerminalUsername": "ea5y1biehi",
"TerminalPassword": "ea5y1biehi", "TerminalPassword": "SaLamAT@1404!",
"CreditPortalUrl": "https://20card.ir", "CreditPortalUrl": "https://userpanel.20card.ir/",
"Packages": { "Packages": {
"4": { "4": {
"GatewayAmountToman": 9700000, "GatewayAmountToman": 9700000,