Files
CMS/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyDiscountWalletCharge/VerifyDiscountWalletChargeCommandHandler.cs
T
masoodafar-web d22eb1617f
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 9m58s
feat(payment): add per-user in-memory lock for gateway operations
Introduce IUserPaymentLock to serialize payment initiate and verify flows
per user, preventing concurrent duplicate gateway requests across services.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 01:55:00 +03:30

175 lines
6.8 KiB
C#

using CMSMicroservice.Application.Common;
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge;
public class VerifyDiscountWalletChargeCommandHandler
: IRequestHandler<VerifyDiscountWalletChargeCommand, bool>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ILogger<VerifyDiscountWalletChargeCommandHandler> _logger;
private readonly IUserPaymentLock _paymentLock;
public VerifyDiscountWalletChargeCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
ILogger<VerifyDiscountWalletChargeCommandHandler> logger,
IUserPaymentLock paymentLock)
{
_context = context;
_paymentGateway = paymentGateway;
_logger = logger;
_paymentLock = paymentLock;
}
public Task<bool> Handle(
VerifyDiscountWalletChargeCommand request,
CancellationToken cancellationToken) =>
_paymentLock.ExecuteAsync(
PaymentLockScopes.Verify(request.UserId, request.Authority),
PaymentLockStrategy.WaitForRelease,
ct => HandleCore(request, ct),
cancellationToken);
private async Task<bool> HandleCore(
VerifyDiscountWalletChargeCommand request,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation(
"Verifying discount wallet charge. UserId: {UserId}, Amount: {Amount}, Authority: {Authority}",
request.UserId,
request.Amount,
request.Authority
);
// 1. بررسی کاربر
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
if (user == null)
{
_logger.LogWarning("User not found: {UserId}", request.UserId);
throw new NotFoundException(nameof(User), request.UserId);
}
// واکشی PaymentTransaction برای گرفتن مبلغ (تومان) جهت verify
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, cancellationToken);
var amountInToman = (decimal)(paymentTx?.Amount ?? 0);
// 2. Verify با درگاه (مبلغ به تومان — تبدیل به ریال در ZarinPalService)
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority,
"OK", // وقتی این handler فراخوانی میشه یعنی کاربر از درگاه برگشته — Status باید OK باشه
amountInToman,
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(
"Discount wallet charge verification failed for UserId {UserId}: {Message}",
request.UserId,
verifyResult.Message
);
throw new Exception($"تراکنش ناموفق: {verifyResult.Message}");
}
// 3. شارژ DiscountBalance
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == user.Id, cancellationToken);
if (wallet == null)
{
_logger.LogError("Wallet not found for UserId: {UserId}", request.UserId);
throw new NotFoundException($"کیف پول کاربر با شناسه {request.UserId} یافت نشد");
}
var oldBalance = wallet.DiscountBalance;
wallet.DiscountBalance += request.Amount;
_logger.LogInformation(
"Charging discount balance for UserId {UserId}: {OldBalance} -> {NewBalance}",
request.UserId,
oldBalance,
wallet.DiscountBalance
);
// 4. ثبت Transaction
var transaction = new Transaction
{
Amount = request.Amount,
Description = $"شارژ کیف پول تخفیفی - کاربر {user.Id}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.Now,
RefId = verifyResult.RefId,
Type = TransactionType.DiscountWalletCharge
};
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(cancellationToken);
// ثبت لاگ تغییرات کیف پول (بعد از ایجاد Transaction برای داشتن TransactionId)
_context.UserWalletHistories.Add(new Domain.Entities.UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = 0,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = request.Amount,
IsIncrease = true,
RefrenceId = transaction.Id
});
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,
transaction.Id,
verifyResult.RefId
);
return true;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in VerifyDiscountWalletChargeCommand for UserId: {UserId}",
request.UserId
);
throw;
}
}
}