feat(payment): add per-user in-memory lock for gateway operations
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>
This commit is contained in:
masoodafar-web
2026-06-08 01:55:00 +03:30
parent f08d3ac78f
commit d22eb1617f
14 changed files with 543 additions and 66 deletions
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common;
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
@@ -17,20 +18,32 @@ public class ChargeDiscountWalletCommandHandler
private readonly IPaymentGatewayService _paymentGateway;
private readonly IConfiguration _configuration;
private readonly ILogger<ChargeDiscountWalletCommandHandler> _logger;
private readonly IUserPaymentLock _paymentLock;
public ChargeDiscountWalletCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
IConfiguration configuration,
ILogger<ChargeDiscountWalletCommandHandler> logger)
ILogger<ChargeDiscountWalletCommandHandler> logger,
IUserPaymentLock paymentLock)
{
_context = context;
_paymentGateway = paymentGateway;
_configuration = configuration;
_logger = logger;
_paymentLock = paymentLock;
}
public async Task<PaymentInitiateResult> Handle(
public Task<PaymentInitiateResult> Handle(
ChargeDiscountWalletCommand request,
CancellationToken cancellationToken) =>
_paymentLock.ExecuteAsync(
PaymentLockScopes.Initiate(request.UserId),
PaymentLockStrategy.FailFast,
ct => HandleCore(request, ct),
cancellationToken);
private async Task<PaymentInitiateResult> HandleCore(
ChargeDiscountWalletCommand request,
CancellationToken cancellationToken)
{
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common;
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
@@ -18,20 +19,32 @@ public class ChargeMagicWalletCommandHandler
private readonly IPaymentGatewayService _paymentGateway;
private readonly IConfiguration _configuration;
private readonly ILogger<ChargeMagicWalletCommandHandler> _logger;
private readonly IUserPaymentLock _paymentLock;
public ChargeMagicWalletCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
IConfiguration configuration,
ILogger<ChargeMagicWalletCommandHandler> logger)
ILogger<ChargeMagicWalletCommandHandler> logger,
IUserPaymentLock paymentLock)
{
_context = context;
_paymentGateway = paymentGateway;
_configuration = configuration;
_logger = logger;
_paymentLock = paymentLock;
}
public async Task<PaymentInitiateResult> Handle(
public Task<PaymentInitiateResult> Handle(
ChargeMagicWalletCommand request,
CancellationToken cancellationToken) =>
_paymentLock.ExecuteAsync(
PaymentLockScopes.Initiate(request.UserId),
PaymentLockStrategy.FailFast,
ct => HandleCore(request, ct),
cancellationToken);
private async Task<PaymentInitiateResult> HandleCore(
ChargeMagicWalletCommand request,
CancellationToken cancellationToken)
{
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common;
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
@@ -15,18 +16,30 @@ public class VerifyDiscountWalletChargeCommandHandler
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)
ILogger<VerifyDiscountWalletChargeCommandHandler> logger,
IUserPaymentLock paymentLock)
{
_context = context;
_paymentGateway = paymentGateway;
_logger = logger;
_paymentLock = paymentLock;
}
public async Task<bool> Handle(
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)
{
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common;
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
@@ -16,20 +17,44 @@ public class VerifyMagicWalletChargeCommandHandler
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)
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
{