refactor(payment): unify package verify for callback and reconciliation job
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 10m40s

Extract VerifyUserPackagePurchasePaymentCommand with idempotency, DB lock,
and IUserPaymentLock so ZarinpalReconciliationJob cannot double-credit wallets.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
masoodafar-web
2026-06-08 22:44:12 +03:30
parent 541b0c2ce9
commit dba0b63a70
4 changed files with 371 additions and 335 deletions
@@ -0,0 +1,26 @@
using MediatR;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
/// <summary>
/// Shared verify + wallet credit path for package IPG payments (callback UI and reconciliation job).
/// </summary>
public class VerifyUserPackagePurchasePaymentCommand : IRequest<VerifyUserPackagePurchasePaymentResult>
{
public long PurchaseId { get; set; }
public string Authority { get; set; } = string.Empty;
public string Status { get; set; } = "OK";
}
public class VerifyUserPackagePurchasePaymentResult
{
public bool Success { get; init; }
public bool AlreadyPaid { get; init; }
public string Message { get; init; } = string.Empty;
public long TransactionId { get; init; }
public string ReferenceCode { get; init; } = string.Empty;
public long PackageId { get; init; }
public string PackageName { get; init; } = string.Empty;
public long AmountPaid { get; init; }
public DateTime PurchasedAt { get; init; }
}
@@ -0,0 +1,303 @@
using System.Data;
using CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
using CMSMicroservice.Application.Common;
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
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.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
public class VerifyUserPackagePurchasePaymentCommandHandler
: IRequestHandler<VerifyUserPackagePurchasePaymentCommand, VerifyUserPackagePurchasePaymentResult>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ISender _sender;
private readonly IUserPaymentLock _paymentLock;
private readonly ILogger<VerifyUserPackagePurchasePaymentCommandHandler> _logger;
public VerifyUserPackagePurchasePaymentCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
ISender sender,
IUserPaymentLock paymentLock,
ILogger<VerifyUserPackagePurchasePaymentCommandHandler> logger)
{
_context = context;
_paymentGateway = paymentGateway;
_sender = sender;
_paymentLock = paymentLock;
_logger = logger;
}
public async Task<VerifyUserPackagePurchasePaymentResult> Handle(
VerifyUserPackagePurchasePaymentCommand request,
CancellationToken cancellationToken)
{
var purchaseUserId = await _context.UserPackagePurchases
.Where(p => p.Id == request.PurchaseId && !p.IsDeleted)
.Select(p => (long?)p.UserId)
.FirstOrDefaultAsync(cancellationToken);
if (purchaseUserId is null or 0)
throw new NotFoundException("سفارش یافت نشد");
var verifyKey = !string.IsNullOrEmpty(request.Authority)
? request.Authority
: request.PurchaseId.ToString();
return await _paymentLock.ExecuteAsync(
PaymentLockScopes.Verify(purchaseUserId.Value, verifyKey),
PaymentLockStrategy.WaitForRelease,
ct => HandleCoreAsync(request, ct),
cancellationToken);
}
private async Task<VerifyUserPackagePurchasePaymentResult> HandleCoreAsync(
VerifyUserPackagePurchasePaymentCommand request,
CancellationToken ct)
{
var purchase = await _context.UserPackagePurchases
.Include(p => p.Package)
.Where(p => p.Id == request.PurchaseId && !p.IsDeleted)
.FirstOrDefaultAsync(ct);
if (purchase == null)
throw new NotFoundException("سفارش یافت نشد");
var transaction = purchase.TransactionId.HasValue
? await _context.Transactions
.FirstOrDefaultAsync(t => t.Id == purchase.TransactionId.Value, ct)
: null;
var paymentTx = !string.IsNullOrEmpty(request.Authority)
? await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, ct)
: null;
if (IsPaymentAlreadyCompleted(transaction, paymentTx))
{
return BuildResult(purchase, transaction, paymentTx, alreadyPaid: true);
}
if (!string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase))
{
if (transaction != null && transaction.PaymentStatus != PaymentStatus.Success)
{
transaction.PaymentStatus = PaymentStatus.Reject;
await _context.SaveChangesAsync(ct);
}
return new VerifyUserPackagePurchasePaymentResult
{
Success = false,
Message = "پرداخت توسط کاربر لغو شد",
PackageId = purchase.PackageId,
PackageName = purchase.Package?.Title ?? string.Empty,
AmountPaid = purchase.Amount,
PurchasedAt = purchase.PurchasedAt
};
}
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Status, (decimal)amountInToman, ct);
if (!verifyResult.IsSuccess)
{
if (paymentTx != null)
{
paymentTx.PaymentStatus = false;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
}
if (transaction != null)
transaction.PaymentStatus = PaymentStatus.Reject;
await _context.SaveChangesAsync(ct);
return new VerifyUserPackagePurchasePaymentResult
{
Success = false,
Message = verifyResult.Message ?? "خرید پکیج ناموفق بود",
TransactionId = transaction?.Id ?? 0,
PackageId = purchase.PackageId,
PackageName = purchase.Package?.Title ?? string.Empty,
AmountPaid = purchase.Amount,
PurchasedAt = purchase.PurchasedAt
};
}
await using var dbTx = await _context.Database.BeginTransactionAsync(IsolationLevel.Serializable, ct);
try
{
if (purchase.TransactionId.HasValue)
{
transaction = await _context.Transactions
.FirstOrDefaultAsync(t => t.Id == purchase.TransactionId.Value, ct);
}
if (!string.IsNullOrEmpty(request.Authority))
{
paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, ct);
}
if (IsPaymentAlreadyCompleted(transaction, paymentTx))
{
await dbTx.CommitAsync(ct);
return BuildResult(purchase, transaction, paymentTx, alreadyPaid: true);
}
if (transaction == null)
throw new InvalidOperationException("تراکنش سفارش یافت نشد");
var alreadyCredited = await _context.UserWalletHistories
.AnyAsync(h => h.RefrenceId == transaction.Id && !h.IsDeleted, ct);
if (alreadyCredited)
{
transaction.PaymentStatus = PaymentStatus.Success;
transaction.PaymentDate ??= DateTime.UtcNow;
UpdatePaymentTransaction(paymentTx, verifyResult, markSuccess: true);
await _context.SaveChangesAsync(ct);
await dbTx.CommitAsync(ct);
return BuildResult(purchase, transaction, paymentTx, alreadyPaid: true);
}
UpdatePaymentTransaction(paymentTx, verifyResult, markSuccess: true);
transaction.PaymentStatus = PaymentStatus.Success;
transaction.PaymentDate = DateTime.UtcNow;
transaction.RefId = verifyResult.RefId;
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == purchase.UserId, ct);
if (wallet == null)
{
wallet = new UserWallet
{
UserId = purchase.UserId,
Balance = 0,
DiscountBalance = 0,
NetworkBalance = 0
};
_context.UserWallets.Add(wallet);
await _context.SaveChangesAsync(ct);
}
var discountMultiplier = purchase.Package?.DiscountMultiplier
?? throw new InvalidOperationException("پکیج سفارش یافت نشد");
var discountAmount = (long)(purchase.Amount * (double)discountMultiplier);
wallet.Balance += purchase.Amount;
wallet.DiscountBalance += discountAmount;
_context.UserWalletHistories.Add(new UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = purchase.Amount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = discountAmount,
IsIncrease = true,
RefrenceId = transaction.Id,
PackageId = purchase.PackageId
});
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == purchase.UserId, ct);
if (user != null)
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
await _context.SaveChangesAsync(ct);
await dbTx.CommitAsync(ct);
}
catch
{
await dbTx.RollbackAsync(ct);
throw;
}
try
{
await _sender.Send(new ActivateClubMembershipCommand
{
UserId = purchase.UserId,
ForceActivation = false
}, ct);
}
catch (Exception ex)
{
_logger.LogWarning(
ex,
"Auto club activation failed after package verify. UserId={UserId}, PurchaseId={PurchaseId}",
purchase.UserId,
purchase.Id);
}
_logger.LogInformation(
"Package purchase verified. PurchaseId={PurchaseId}, UserId={UserId}, Authority={Authority}",
purchase.Id,
purchase.UserId,
request.Authority);
return BuildResult(
purchase, transaction, paymentTx, alreadyPaid: false, verifyResult.RefId);
}
private static bool IsPaymentAlreadyCompleted(Transaction? transaction, PaymentTransaction? paymentTx)
{
return transaction?.PaymentStatus == PaymentStatus.Success
|| paymentTx?.PaymentStatus == true;
}
private static void UpdatePaymentTransaction(
PaymentTransaction? paymentTx,
PaymentVerificationResult verifyResult,
bool markSuccess)
{
if (paymentTx == null) return;
paymentTx.PaymentStatus = markSuccess && verifyResult.IsSuccess;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
paymentTx.CardPan = verifyResult.CardPan;
paymentTx.CardHash = verifyResult.CardHash;
paymentTx.RefId = verifyResult.TrackingCode;
}
private static VerifyUserPackagePurchasePaymentResult BuildResult(
UserPackagePurchase purchase,
Transaction? transaction,
PaymentTransaction? paymentTx,
bool alreadyPaid,
string? referenceCode = null)
{
var refCode = referenceCode
?? paymentTx?.RefId
?? transaction?.RefId
?? string.Empty;
return new VerifyUserPackagePurchasePaymentResult
{
Success = true,
AlreadyPaid = alreadyPaid,
Message = alreadyPaid ? "پرداخت قبلاً تأیید شده بود" : "خرید پکیج با موفقیت تایید شد",
TransactionId = transaction?.Id ?? 0,
ReferenceCode = refCode,
PackageId = purchase.PackageId,
PackageName = purchase.Package?.Title ?? string.Empty,
AmountPaid = purchase.Amount,
PurchasedAt = purchase.PurchasedAt
};
}
}
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
using CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge;
@@ -251,94 +251,25 @@ public class ZarinpalReconciliationJob
throw new InvalidOperationException(
$"Cannot parse OrderId '{paymentTx.OrderId}' for package purchase {paymentTx.Id}");
var purchase = await _context.UserPackagePurchases
.Include(p => p.Package)
.FirstOrDefaultAsync(p => p.Id == purchaseId && !p.IsDeleted, ct)
?? throw new InvalidOperationException($"UserPackagePurchase #{purchaseId} not found");
var transaction = purchase.TransactionId.HasValue
? await _context.Transactions.FirstOrDefaultAsync(t => t.Id == purchase.TransactionId.Value, ct)
: null;
var amountInToman = paymentTx.Amount != 0 ? paymentTx.Amount : purchase.Amount;
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
paymentTx.Authority!, "OK", (decimal)amountInToman, ct);
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 && transaction != null)
var result = await _sender.Send(new VerifyUserPackagePurchasePaymentCommand
{
transaction.PaymentStatus = PaymentStatus.Success;
transaction.PaymentDate = DateTime.UtcNow;
transaction.RefId = verifyResult.RefId;
PurchaseId = purchaseId,
Authority = paymentTx.Authority ?? string.Empty,
Status = "OK"
}, ct);
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == purchase.UserId, ct);
if (wallet == null)
{
wallet = new UserWallet { UserId = purchase.UserId };
_context.UserWallets.Add(wallet);
await _context.SaveChangesAsync(ct);
}
var discountMultiplier = purchase.Package?.DiscountMultiplier
?? throw new InvalidOperationException(
$"Package not loaded for UserPackagePurchase #{purchaseId}");
var discountAmount = (long)(purchase.Amount * (double)discountMultiplier);
wallet.Balance += purchase.Amount;
wallet.DiscountBalance += discountAmount;
_context.UserWalletHistories.Add(new UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = purchase.Amount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = discountAmount,
IsIncrease = true,
RefrenceId = transaction.Id,
PackageId = purchase.PackageId
});
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == purchase.UserId, ct);
if (user != null)
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
}
else if (transaction != null)
if (!result.Success)
{
transaction.PaymentStatus = PaymentStatus.Reject;
throw new InvalidOperationException(
$"Package verify failed for purchase #{purchaseId}, authority={paymentTx.Authority}: {result.Message}");
}
await _context.SaveChangesAsync(ct);
if (verifyResult.IsSuccess)
if (result.AlreadyPaid)
{
try
{
await _sender.Send(new ActivateClubMembershipCommand
{
UserId = purchase.UserId,
ForceActivation = false
}, ct);
}
catch (Exception ex)
{
_logger.LogWarning(
ex,
"ZarinpalReconciliation: club activation failed for userId={UserId} (purchase #{PurchaseId})",
purchase.UserId, purchaseId);
}
_logger.LogInformation(
"ZarinpalReconciliation: package purchase #{PurchaseId} already verified (authority={Authority})",
purchaseId,
paymentTx.Authority);
}
}
@@ -9,6 +9,7 @@ using CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus;
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackages;
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackageDetails;
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPurchaseHistory;
using CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common;
using CMSMicroservice.Application.Common.Exceptions;
@@ -17,7 +18,6 @@ using AppModels = CMSMicroservice.Application.Common.Models;
using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using CMSMicroservice.Protobuf.Protos;
using Microsoft.EntityFrameworkCore;
@@ -265,29 +265,20 @@ public class PackageService : PackageContract.PackageContractBase
};
}
public override async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchase(CustomerVerifyPackagePurchaseRequest request, ServerCallContext context)
public override async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchase(
CustomerVerifyPackagePurchaseRequest request,
ServerCallContext context)
{
var ct = context.CancellationToken;
var purchaseUserId = await _context.UserPackagePurchases
.Where(p => p.Id == request.OrderId && !p.IsDeleted)
.Select(p => (long?)p.UserId)
.FirstOrDefaultAsync(ct);
if (purchaseUserId is null or 0)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
var verifyKey = !string.IsNullOrEmpty(request.Authority)
? request.Authority
: request.OrderId.ToString();
try
{
return await _paymentLock.ExecuteAsync(
PaymentLockScopes.Verify(purchaseUserId.Value, verifyKey),
PaymentLockStrategy.WaitForRelease,
lockCt => CustomerVerifyPackagePurchaseCore(request, lockCt),
ct);
var result = await _sender.Send(new VerifyUserPackagePurchasePaymentCommand
{
PurchaseId = request.OrderId,
Authority = request.Authority ?? string.Empty,
Status = request.Status ?? "NOK"
}, context.CancellationToken);
return MapVerifyPackagePurchaseResponse(result);
}
catch (PaymentInProgressException ex)
{
@@ -295,240 +286,25 @@ public class PackageService : PackageContract.PackageContractBase
}
}
private async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchaseCore(
CustomerVerifyPackagePurchaseRequest request,
CancellationToken ct)
private static CustomerVerifyPackagePurchaseResponse MapVerifyPackagePurchaseResponse(
VerifyUserPackagePurchasePaymentResult result)
{
var purchase = await _context.UserPackagePurchases
.Include(p => p.Package)
.Where(p => p.Id == request.OrderId && !p.IsDeleted)
.FirstOrDefaultAsync(ct);
if (purchase == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
var transaction = purchase.TransactionId.HasValue
? await _context.Transactions
.Where(t => t.Id == purchase.TransactionId.Value)
.FirstOrDefaultAsync(ct)
: null;
var paymentTx = !string.IsNullOrEmpty(request.Authority)
? await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, ct)
: null;
// Idempotency: already verified — never credit wallet again
if (IsPackagePaymentAlreadyCompleted(transaction, paymentTx))
{
return BuildVerifyPackagePurchaseResponse(
purchase, transaction, paymentTx, alreadyPaid: true);
}
if (request.Status != "OK")
{
if (transaction != null && transaction.PaymentStatus != Domain.Enums.PaymentStatus.Success)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
await _context.SaveChangesAsync(ct);
}
return new CustomerVerifyPackagePurchaseResponse
{
Success = false,
Message = "پرداخت توسط کاربر لغو شد"
};
}
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Status, (decimal)amountInToman, ct);
if (!verifyResult.IsSuccess)
{
if (paymentTx != null)
{
paymentTx.PaymentStatus = false;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
}
if (transaction != null)
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
await _context.SaveChangesAsync(ct);
return new CustomerVerifyPackagePurchaseResponse
{
Success = false,
Message = verifyResult.Message ?? "خرید پکیج ناموفق بود",
TransactionId = transaction?.Id ?? 0
};
}
// Serializable transaction prevents concurrent double-credit (~1s race from duplicate callbacks)
await using var dbTx = await _context.Database.BeginTransactionAsync(IsolationLevel.Serializable, ct);
try
{
// Reload inside lock scope
if (purchase.TransactionId.HasValue)
{
transaction = await _context.Transactions
.FirstOrDefaultAsync(t => t.Id == purchase.TransactionId.Value, ct);
}
if (!string.IsNullOrEmpty(request.Authority))
{
paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, ct);
}
if (IsPackagePaymentAlreadyCompleted(transaction, paymentTx))
{
await dbTx.CommitAsync(ct);
return BuildVerifyPackagePurchaseResponse(
purchase, transaction, paymentTx, alreadyPaid: true);
}
if (transaction == null)
throw new RpcException(new Status(StatusCode.Internal, "تراکنش سفارش یافت نشد"));
// Wallet history is the idempotency key for financial side-effects
var alreadyCredited = await _context.UserWalletHistories
.AnyAsync(h => h.RefrenceId == transaction.Id && !h.IsDeleted, ct);
if (alreadyCredited)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Success;
transaction.PaymentDate ??= DateTime.UtcNow;
if (paymentTx != null)
{
paymentTx.PaymentStatus = true;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
paymentTx.RefId = verifyResult.TrackingCode;
}
await _context.SaveChangesAsync(ct);
await dbTx.CommitAsync(ct);
return BuildVerifyPackagePurchaseResponse(
purchase, transaction, paymentTx, alreadyPaid: true);
}
if (paymentTx != null)
{
paymentTx.PaymentStatus = true;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
paymentTx.CardPan = verifyResult.CardPan;
paymentTx.CardHash = verifyResult.CardHash;
paymentTx.RefId = verifyResult.TrackingCode;
}
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Success;
transaction.PaymentDate = DateTime.UtcNow;
transaction.RefId = verifyResult.RefId;
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == purchase.UserId, ct);
if (wallet == null)
{
wallet = new CMSMicroservice.Domain.Entities.UserWallet
{
UserId = purchase.UserId,
Balance = 0,
DiscountBalance = 0,
NetworkBalance = 0
};
_context.UserWallets.Add(wallet);
await _context.SaveChangesAsync(ct);
}
var discountMultiplier = purchase.Package?.DiscountMultiplier
?? throw new RpcException(new Status(StatusCode.Internal, "پکیج سفارش یافت نشد"));
var discountAmount = (long)(purchase.Amount * (double)discountMultiplier);
wallet.Balance += purchase.Amount;
wallet.DiscountBalance += discountAmount;
_context.UserWalletHistories.Add(new CMSMicroservice.Domain.Entities.UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = purchase.Amount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = discountAmount,
IsIncrease = true,
RefrenceId = transaction.Id,
PackageId = purchase.PackageId
});
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == purchase.UserId, ct);
if (user != null)
user.PackagePurchaseMethod = Domain.Enums.PackagePurchaseMethod.DirectPurchase;
await _context.SaveChangesAsync(ct);
await dbTx.CommitAsync(ct);
}
catch
{
await dbTx.RollbackAsync(ct);
throw;
}
if (verifyResult.IsSuccess)
{
try
{
await _sender.Send(new CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership.ActivateClubMembershipCommand
{
UserId = purchase.UserId,
ForceActivation = false
}, ct);
}
catch (Exception ex)
{
System.Console.WriteLine($"Auto club activation failed for UserId {purchase.UserId}: {ex.Message}");
}
}
return BuildVerifyPackagePurchaseResponse(
purchase, transaction, paymentTx, alreadyPaid: false, verifyResult.RefId);
}
private static bool IsPackagePaymentAlreadyCompleted(
CMSMicroservice.Domain.Entities.Transaction? transaction,
PaymentTransaction? paymentTx)
{
return transaction?.PaymentStatus == Domain.Enums.PaymentStatus.Success
|| paymentTx?.PaymentStatus == true;
}
private static CustomerVerifyPackagePurchaseResponse BuildVerifyPackagePurchaseResponse(
CMSMicroservice.Domain.Entities.UserPackagePurchase purchase,
CMSMicroservice.Domain.Entities.Transaction? transaction,
PaymentTransaction? paymentTx,
bool alreadyPaid,
string? referenceCode = null)
{
var refCode = referenceCode
?? paymentTx?.RefId
?? transaction?.RefId
?? string.Empty;
return new CustomerVerifyPackagePurchaseResponse
{
Success = true,
Message = alreadyPaid ? "پرداخت قبلاً تأیید شده بود" : "خرید پکیج با موفقیت تایید شد",
TransactionId = transaction?.Id ?? 0,
ReferenceCode = refCode,
PurchaseInfo = new PackagePurchaseInfo
{
PackageId = purchase.PackageId,
PackageName = purchase.Package?.Title ?? string.Empty,
AmountPaid = purchase.Amount,
PurchaseDate = Timestamp.FromDateTime(DateTime.SpecifyKind(purchase.PurchasedAt, DateTimeKind.Utc))
}
Success = result.Success,
Message = result.Message,
TransactionId = result.TransactionId,
ReferenceCode = result.ReferenceCode,
PurchaseInfo = result.Success
? new PackagePurchaseInfo
{
PackageId = result.PackageId,
PackageName = result.PackageName,
AmountPaid = result.AmountPaid,
PurchaseDate = Timestamp.FromDateTime(
DateTime.SpecifyKind(result.PurchasedAt, DateTimeKind.Utc))
}
: null
};
}