From 0b7212ac1c3db154ae6e4e493887ed2db8acd3ac Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sun, 20 Sep 2026 22:17:55 +0330 Subject: [PATCH] feat(payment): implement multi-gateway support and enhance payment processing - Added support for multiple payment gateways, including ZarinPal and TwentyCard, through a new IPaymentGatewayResolver. - Updated PaymentRequest DTO to include payer name and invoice number for better transaction tracking. - Enhanced PackageService to dynamically determine payment amounts based on the selected gateway. - Updated appsettings to include configuration for TwentyCard payment options. - Bumped Protobuf project version to reflect the addition of new features and changes. --- README.md | 4 +- .../CMSMicroservice.Application.csproj | 1 + .../Interfaces/IPaymentGatewayResolver.cs | 9 + .../Interfaces/IPaymentGatewayService.cs | 10 + ...serPackagePurchasePaymentCommandHandler.cs | 34 ++- .../ConfigureServices.cs | 22 +- .../Payment/PaymentGatewayResolver.cs | 35 +++ .../Payment/TwentyCardPaymentService.cs | 264 ++++++++++++++++++ .../CMSMicroservice.Protobuf.csproj | 2 +- .../Protos/package.proto | 2 + .../Services/PackageService.cs | 64 ++++- .../appsettings.Staging.json | 17 ++ src/CMSMicroservice.WebApi/appsettings.json | 17 ++ 13 files changed, 447 insertions(+), 34 deletions(-) create mode 100644 src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayResolver.cs create mode 100644 src/CMSMicroservice.Infrastructure/Services/Payment/PaymentGatewayResolver.cs create mode 100644 src/CMSMicroservice.Infrastructure/Services/Payment/TwentyCardPaymentService.cs 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": {