Files
CMS/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommandHandler.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

243 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CMSMicroservice.Application.Common;
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Entities.Payment;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge;
public class VerifyMagicWalletChargeCommandHandler
: IRequestHandler<VerifyMagicWalletChargeCommand, bool>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ILogger<VerifyMagicWalletChargeCommandHandler> _logger;
private readonly IUserPaymentLock _paymentLock;
public VerifyMagicWalletChargeCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
ILogger<VerifyMagicWalletChargeCommandHandler> logger,
IUserPaymentLock paymentLock)
{
_context = context;
_paymentGateway = paymentGateway;
_logger = logger;
_paymentLock = paymentLock;
}
public async Task<bool> Handle(
VerifyMagicWalletChargeCommand request,
CancellationToken cancellationToken)
{
var paymentTx = await _context.PaymentTransactions
.AsNoTracking()
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, cancellationToken);
if (paymentTx == null)
throw new NotFoundException("تراکنش پرداخت یافت نشد");
if (!paymentTx.UserId.HasValue)
throw new BadRequestException("شناسه کاربر در تراکنش پرداخت یافت نشد");
return await _paymentLock.ExecuteAsync(
PaymentLockScopes.Verify(paymentTx.UserId.Value, request.Authority),
PaymentLockStrategy.WaitForRelease,
ct => HandleCore(request, ct),
cancellationToken);
}
private async Task<bool> HandleCore(
VerifyMagicWalletChargeCommand request,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation(
"Verifying magic wallet charge. Authority: {Authority}, Status: {Status}",
request.Authority,
request.Status
);
// 1. پیدا کردن PaymentTransaction از Authority
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, cancellationToken);
if (paymentTx == null)
{
_logger.LogError("PaymentTransaction not found for Authority: {Authority}", request.Authority);
throw new NotFoundException("تراکنش پرداخت یافت نشد");
}
if (paymentTx.PaymentStatus)
{
_logger.LogWarning("PaymentTransaction already verified: {Authority}", request.Authority);
return true; // قبلاً تأیید شده
}
var userId = paymentTx.UserId
?? throw new BadRequestException("شناسه کاربر در تراکنش پرداخت یافت نشد");
var depositAmount = paymentTx.Amount; // مبلغ واقعی واریزی (تومان)
// 2. بررسی وضعیت برگشتی از درگاه
if (!string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning(
"Magic charge cancelled by user. UserId: {UserId}, Authority: {Authority}",
userId, request.Authority
);
paymentTx.PaymentStatus = false;
paymentTx.VerificationStatusMessage = "پرداخت توسط کاربر لغو شد";
await _context.SaveChangesAsync(cancellationToken);
throw new BadRequestException("پرداخت توسط کاربر لغو شد");
}
// 3. Verify با درگاه (مبلغ به تومان — تبدیل به ریال در ZarinPalService)
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority,
request.Status,
depositAmount,
cancellationToken
);
// آپدیت PaymentTransaction
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(
"Magic wallet charge verification failed for UserId {UserId}: {Message}",
userId,
verifyResult.Message
);
await _context.SaveChangesAsync(cancellationToken);
throw new BadRequestException($"تراکنش ناموفق: {verifyResult.Message}");
}
// 4. بررسی کیف‌پول و حالت جادویی
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == userId, cancellationToken);
if (wallet == null)
{
_logger.LogError("Wallet not found for UserId: {UserId}", userId);
throw new NotFoundException("کیف پول کاربر یافت نشد");
}
if (wallet.WalletMode != WalletMode.Magic)
{
_logger.LogError(
"Wallet is not in Magic mode during verify. UserId: {UserId}, Mode: {Mode}",
userId, wallet.WalletMode
);
throw new BadRequestException("کیف‌پول در حالت جادویی نیست");
}
// 4.5. بارگذاری پکیج کاربر برای ضریب جادویی
var currentCycle = await _context.ClubMembershipCycles
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, cancellationToken);
var package = currentCycle != null
? await _context.Packages.FirstOrDefaultAsync(p => p.Id == currentCycle.PackageId, cancellationToken)
: await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken);
if (package == null)
throw new NotFoundException("پکیج یافت نشد");
// 5. محاسبه اعتبار
var creditAmount = (long)(depositAmount * package.MagicWalletMultiplier); // مبلغ × 2.5
var bonusAmount = creditAmount - depositAmount; // بونوس = مبلغ × 1.5
// 6. ثبت تراکنش واریز واقعی (MagicWalletDeposit)
var depositTransaction = new Transaction
{
Amount = depositAmount,
Description = $"شارژ کیف‌پول جادویی - واریز واقعی - کاربر {userId}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
RefId = verifyResult.RefId,
Type = TransactionType.MagicWalletDeposit
};
_context.Transactions.Add(depositTransaction);
await _context.SaveChangesAsync(cancellationToken);
// 7. ثبت تراکنش بونوس (MagicWalletBonus)
var bonusTransaction = new Transaction
{
Amount = bonusAmount,
Description = $"شارژ کیف‌پول جادویی - بونوس ×{package.MagicWalletMultiplier - 1} - کاربر {userId}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
RefId = $"MAGIC_BONUS_{depositTransaction.Id}",
Type = TransactionType.MagicWalletBonus
};
_context.Transactions.Add(bonusTransaction);
await _context.SaveChangesAsync(cancellationToken);
// 8. شارژ Balance و آپدیت شمارنده‌ها
wallet.Balance += creditAmount;
wallet.MagicTotalDeposited += depositAmount;
wallet.MagicTotalCredited += creditAmount;
// 9. ثبت WalletHistory (اجباری)
var walletLog = new UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = creditAmount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = 0,
IsIncrease = true,
RefrenceId = depositTransaction.Id
};
_context.UserWalletHistories.Add(walletLog);
// 10. لینک PaymentTransaction به Transaction داخلی
paymentTx.TransactionId = depositTransaction.Id;
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Magic wallet charged successfully. UserId: {UserId}, " +
"Deposit: {Deposit}, Credit: {Credit} (×{Multiplier}), Bonus: {Bonus}, " +
"TotalDeposited: {TotalDeposited}/{MaxDeposit}, NewBalance: {NewBalance}",
userId,
depositAmount,
creditAmount,
package.MagicWalletMultiplier,
bonusAmount,
wallet.MagicTotalDeposited,
package.MagicWalletMaxDeposit,
wallet.Balance
);
return true;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in VerifyMagicWalletChargeCommand. Authority: {Authority}",
request.Authority
);
throw;
}
}
}