Files
CMS/src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs
T
masoodafar-web a39a36e66d
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m6s
feat: add PaymentTransaction table for gateway-level tracking
- New PaymentTransaction entity (Domain/Entities/Payment/) with all gateway fields:
  GatewayProvider, MerchantId, Authority, CardPan, CardHash, RefId, VerificationStatusCode, etc.
- New PaymentTransactionConfiguration with indexes on Authority, GatewayProvider, UserId, TransactionId, RefId
- Added DbSet<PaymentTransaction> to IApplicationDbContext and ApplicationDbContext
- Extended PaymentVerificationResult DTO with CardPan, CardHash, VerificationCode
- Updated ZarinPalPaymentService.VerifyPayment to return CardPan/CardHash/VerificationCode
- Updated all 5 payment consumers to create/update PaymentTransaction:
  * PlaceOrderCommandHandler — creates PaymentTransaction after InitiatePayment
  * PaymentCallbackController — updates PaymentTransaction after VerifyPayment
  * ChargeDiscountWalletCommandHandler — creates PaymentTransaction + fixed callback URL
  * VerifyDiscountWalletChargeCommandHandler — updates PaymentTransaction after verify
  * TransactionsService.CustomerPaymentRequest/Verification — create/update PaymentTransaction
  * PackageService.CustomerPurchasePackage/Verify — create/update PaymentTransaction
- Transaction table untouched — PaymentTransaction is a separate table
- Pattern inspired by PYMS: create row before gateway → update after verify
- EF migration: AddPaymentTransactionTable
2026-02-15 23:53:28 +03:30

147 lines
6.1 KiB
C#

using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Controllers;
/// <summary>
/// Callback endpoint for payment gateways (ZarinPal, etc.)
/// درگاه پرداخت بعد از پرداخت (یا لغو) کاربر را به اینجا redirect می‌کند
/// </summary>
[ApiController]
[AllowAnonymous] // کاربر از درگاه بانک برمی‌گردد — JWT ندارد
[ApiExplorerSettings(GroupName = "cms")]
public class PaymentCallbackController : ControllerBase
{
private readonly ISender _sender;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IApplicationDbContext _context;
private readonly IConfiguration _configuration;
private readonly ILogger<PaymentCallbackController> _logger;
public PaymentCallbackController(
ISender sender,
IPaymentGatewayService paymentGateway,
IApplicationDbContext context,
IConfiguration configuration,
ILogger<PaymentCallbackController> logger)
{
_sender = sender;
_paymentGateway = paymentGateway;
_context = context;
_configuration = configuration;
_logger = logger;
}
/// <summary>
/// Callback برای پرداخت سفارش فروشگاه تخفیفی
/// زرین‌پال کاربر را با Authority و Status به این endpoint برمی‌گرداند
/// </summary>
[HttpGet("/api/payment/discount-order/callback")]
public async Task<IActionResult> DiscountOrderCallback(
[FromQuery] long orderId,
[FromQuery(Name = "Authority")] string? authority,
[FromQuery(Name = "Status")] string? status,
CancellationToken cancellationToken)
{
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
_logger.LogInformation(
"Payment callback received: OrderId={OrderId}, Authority={Authority}, Status={Status}",
orderId, authority, status);
try
{
// پیدا کردن سفارش و تراکنش
var order = await _context.DiscountOrders
.Include(o => o.OrderDetails)
.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
if (order == null)
{
_logger.LogError("Payment callback: Order #{OrderId} not found", orderId);
return Redirect($"{frontOfficeBaseUrl}/discount-store/orders?error=order-not-found");
}
var transaction = order.TransactionId.HasValue
? await _context.Transactions.FirstOrDefaultAsync(
t => t.Id == order.TransactionId.Value, cancellationToken)
: null;
// تأیید پرداخت از درگاه
bool paymentSuccess = false;
string? refId = null;
if (string.Equals(status, "OK", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(authority))
{
// Verify با مبلغ از دیتابیس (تومان)
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
authority,
status!,
order.GatewayAmountPaid, // مبلغ به تومان
cancellationToken);
paymentSuccess = verifyResult.IsSuccess;
refId = verifyResult.TrackingCode ?? verifyResult.RefId;
// آپدیت PaymentTransaction با نتیجه verify
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == authority, cancellationToken);
if (paymentTx != null)
{
paymentTx.PaymentStatus = verifyResult.IsSuccess;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
paymentTx.CardPan = verifyResult.CardPan;
paymentTx.CardHash = verifyResult.CardHash;
paymentTx.RefId = verifyResult.TrackingCode;
await _context.SaveChangesAsync(cancellationToken);
}
_logger.LogInformation(
"Payment verification for Order #{OrderId}: Success={Success}, RefId={RefId}, Message={Message}",
orderId, paymentSuccess, refId, verifyResult.Message);
}
else
{
_logger.LogWarning("Payment cancelled by user for Order #{OrderId}", orderId);
}
// تکمیل سفارش از طریق CQRS
var completeResult = await _sender.Send(new CompleteOrderPaymentCommand
{
OrderId = orderId,
TransactionId = transaction?.Id ?? 0,
PaymentSuccess = paymentSuccess,
RefId = refId
}, cancellationToken);
// Redirect به FrontOffice
if (paymentSuccess && completeResult.Success)
{
_logger.LogInformation("Payment completed successfully for Order #{OrderId}", orderId);
return Redirect(
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=success");
}
else
{
_logger.LogWarning("Payment failed for Order #{OrderId}", orderId);
return Redirect(
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=failed");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment callback error for Order #{OrderId}", orderId);
return Redirect(
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=error");
}
}
}