d22eb1617f
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 9m58s
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>
243 lines
10 KiB
C#
243 lines
10 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|