refactor(payment): unify package verify for callback and reconciliation job
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 10m40s
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user