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
@@ -34,6 +34,7 @@ public interface IApplicationDbContext
DbSet<UserWallet> UserWallets { get; }
DbSet<UserWalletChangeLog> UserWalletChangeLogs { get; }
DbSet<ManualPayment> ManualPayments { get; }
DbSet<PaymentTransaction> PaymentTransactions { get; }
DbSet<PublicMessage> PublicMessages { get; }
DbSet<ClubMembership> ClubMemberships { get; }
DbSet<ClubMembershipHistory> ClubMembershipHistories { get; }
@@ -142,6 +142,21 @@ public class PaymentVerificationResult
/// پیام
/// </summary>
public string? Message { get; set; }
/// <summary>
/// شماره کارت ماسک‌شده (مثلاً 6037-****-****-1234)
/// </summary>
public string? CardPan { get; set; }
/// <summary>
/// هش کارت بانکی
/// </summary>
public string? CardHash { get; set; }
/// <summary>
/// کد وضعیت verify از درگاه (100=موفق، 101=تکراری)
/// </summary>
public int? VerificationCode { get; set; }
}
/// <summary>
@@ -207,6 +207,25 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
{
// ذخیره Authority/RefId در تراکنش برای verify بعدی
transaction.RefId = paymentResult.RefId;
// ثبت PaymentTransaction — جدول جدید با اطلاعات درگاه
var paymentTx = new PaymentTransaction
{
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
Amount = finalGatewayAmount,
CallbackUrl = callbackUrl,
Description = $"فروشگاه تخفیفی - سفارش #{order.Id}",
Mobile = null,
UserId = request.UserId,
RequestStatusCode = 100,
RequestStatusMessage = "Success",
Authority = paymentResult.RefId,
PaymentStatus = false, // هنوز verify نشده
TransactionId = transaction.Id,
OrderId = order.Id.ToString()
};
_context.PaymentTransactions.Add(paymentTx);
await _context.SaveChangesAsync(cancellationToken);
paymentUrl = paymentResult.GatewayUrl;
@@ -2,8 +2,10 @@ using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Entities.Payment;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeDiscountWallet;
@@ -13,15 +15,18 @@ public class ChargeDiscountWalletCommandHandler
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IConfiguration _configuration;
private readonly ILogger<ChargeDiscountWalletCommandHandler> _logger;
public ChargeDiscountWalletCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
IConfiguration configuration,
ILogger<ChargeDiscountWalletCommandHandler> logger)
{
_context = context;
_paymentGateway = paymentGateway;
_configuration = configuration;
_logger = logger;
}
@@ -58,12 +63,15 @@ public class ChargeDiscountWalletCommandHandler
}
// 3. ایجاد درخواست پرداخت
var cmsBaseUrl = _configuration["CmsBaseUrl"] ?? "https://localhost:32846";
var callbackUrl = $"{cmsBaseUrl}/api/wallet/verify-discount-charge";
var paymentRequest = new PaymentRequest
{
Amount = request.Amount,
UserId = user.Id,
Mobile = user.Mobile ?? "",
CallbackUrl = $"https://yourdomain.com/api/wallet/verify-discount-charge",
CallbackUrl = callbackUrl,
Description = $"شارژ کیف پول تخفیفی - کاربر {user.Id}"
};
@@ -80,10 +88,29 @@ public class ChargeDiscountWalletCommandHandler
throw new Exception($"خطا در ارتباط با درگاه پرداخت: {paymentResult.ErrorMessage}");
}
// 4. ثبت PaymentTransaction
var paymentTx = new PaymentTransaction
{
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
Amount = request.Amount,
CallbackUrl = callbackUrl,
Description = $"شارژ کیف پول تخفیفی - کاربر {user.Id}",
Mobile = user.Mobile,
UserId = user.Id,
RequestStatusCode = 100,
RequestStatusMessage = "Success",
Authority = paymentResult.RefId,
PaymentStatus = false
};
_context.PaymentTransactions.Add(paymentTx);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Discount wallet charge initiated. UserId: {UserId}, RefId: {RefId}",
"Discount wallet charge initiated. UserId: {UserId}, RefId: {RefId}, PaymentTxId: {PaymentTxId}",
user.Id,
paymentResult.RefId
paymentResult.RefId,
paymentTx.Id
);
return paymentResult;
@@ -56,6 +56,19 @@ public class VerifyDiscountWalletChargeCommandHandler
"OK" // وقتی این handler فراخوانی میشه یعنی کاربر از درگاه برگشته — Status باید OK باشه
);
// آپدیت PaymentTransaction با نتیجه verify
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.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;
}
if (!verifyResult.IsSuccess)
{
_logger.LogWarning(
@@ -101,6 +114,13 @@ public class VerifyDiscountWalletChargeCommandHandler
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(cancellationToken);
// لینک PaymentTransaction به Transaction داخلی
if (paymentTx != null)
{
paymentTx.TransactionId = transaction.Id;
await _context.SaveChangesAsync(cancellationToken);
}
_logger.LogInformation(
"Discount wallet charged successfully. UserId: {UserId}, TransactionId: {TransactionId}, RefId: {RefId}",
user.Id,