feat: add PaymentTransaction table for gateway-level tracking
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m6s

- 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
This commit is contained in:
masoodafar-web
2026-02-15 23:53:28 +03:30
parent 207f53ef80
commit a39a36e66d
15 changed files with 5017 additions and 5 deletions
@@ -90,6 +90,20 @@ public class PaymentCallbackController : ControllerBase
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);
@@ -14,6 +14,7 @@ using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackages;
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackageDetails;
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPurchaseHistory;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Payment;
using AppModels = CMSMicroservice.Application.Common.Models;
using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
@@ -23,6 +24,7 @@ using CMSMicroservice.Protobuf.Protos;
using Microsoft.EntityFrameworkCore;
using MediatR;
using Mapster;
using Microsoft.Extensions.Configuration;
namespace CMSMicroservice.WebApi.Services;
public class PackageService : PackageContract.PackageContractBase
@@ -32,19 +34,22 @@ public class PackageService : PackageContract.PackageContractBase
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IConfiguration _configuration;
public PackageService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService,
IPaymentGatewayService paymentGateway)
IPaymentGatewayService paymentGateway,
IConfiguration configuration)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_paymentGateway = paymentGateway;
_configuration = configuration;
}
public override async Task<CreateNewPackageResponse> CreateNewPackage(CreateNewPackageRequest request, ServerCallContext context)
{
@@ -225,6 +230,25 @@ public class PackageService : PackageContract.PackageContractBase
// Save RefId
transaction.RefId = paymentResult.RefId;
// ثبت PaymentTransaction
var paymentTx = new PaymentTransaction
{
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
Amount = package.Price,
CallbackUrl = request.CallbackUrl,
Description = $"خرید پکیج {package.Title}",
Mobile = user?.Mobile,
UserId = userId,
RequestStatusCode = 100,
RequestStatusMessage = "Success",
Authority = paymentResult.RefId,
PaymentStatus = false,
TransactionId = transaction.Id,
OrderId = purchase.Id.ToString()
};
_context.PaymentTransactions.Add(paymentTx);
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerPurchasePackageResponse
@@ -275,6 +299,19 @@ public class PackageService : PackageContract.PackageContractBase
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Status, context.CancellationToken);
// آپدیت PaymentTransaction
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.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;
}
if (verifyResult.IsSuccess && transaction != null)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Success;
@@ -10,11 +10,13 @@ using CMSMicroservice.Application.TransactionsCQ.Commands.RefundTransaction;
using CMSMicroservice.Application.TransactionsCQ.Queries.GetCustomerTransaction;
using CMSMicroservice.Application.TransactionsCQ.Queries.GetCustomerTransactionsByFilter;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Payment;
using AppModels = CMSMicroservice.Application.Common.Models;
using Grpc.Core;
using MediatR;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
@@ -25,19 +27,22 @@ public class TransactionsService : TransactionsContract.TransactionsContractBase
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IConfiguration _configuration;
public TransactionsService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService,
IPaymentGatewayService paymentGateway)
IPaymentGatewayService paymentGateway,
IConfiguration configuration)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_paymentGateway = paymentGateway;
_configuration = configuration;
}
public override async Task<CreateNewTransactionsResponse> CreateNewTransactions(CreateNewTransactionsRequest request, ServerCallContext context)
{
@@ -179,6 +184,24 @@ public class TransactionsService : TransactionsContract.TransactionsContractBase
// Save RefId from gateway
transaction.RefId = paymentResult.RefId;
// ثبت PaymentTransaction
var paymentTx = new PaymentTransaction
{
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
Amount = request.Amount,
CallbackUrl = request.CallbackUrl,
Description = request.Description ?? "پرداخت آنلاین",
Mobile = request.Mobile ?? user?.Mobile,
UserId = userId,
RequestStatusCode = 100,
RequestStatusMessage = "Success",
Authority = paymentResult.RefId,
PaymentStatus = false,
TransactionId = transaction.Id
};
_context.PaymentTransactions.Add(paymentTx);
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerPaymentRequestResponse
@@ -216,6 +239,19 @@ public class TransactionsService : TransactionsContract.TransactionsContractBase
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Status, context.CancellationToken);
// آپدیت PaymentTransaction
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.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;
}
if (verifyResult.IsSuccess)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Success;