Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3df738e496 | |||
| dba0b63a70 |
+26
@@ -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; }
|
||||||
|
}
|
||||||
+303
@@ -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 System.Collections.Generic;
|
||||||
using CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
|
using CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
|
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
|
||||||
using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge;
|
using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge;
|
||||||
@@ -251,94 +251,25 @@ public class ZarinpalReconciliationJob
|
|||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Cannot parse OrderId '{paymentTx.OrderId}' for package purchase {paymentTx.Id}");
|
$"Cannot parse OrderId '{paymentTx.OrderId}' for package purchase {paymentTx.Id}");
|
||||||
|
|
||||||
var purchase = await _context.UserPackagePurchases
|
var result = await _sender.Send(new VerifyUserPackagePurchasePaymentCommand
|
||||||
.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)
|
|
||||||
{
|
{
|
||||||
transaction.PaymentStatus = PaymentStatus.Success;
|
PurchaseId = purchaseId,
|
||||||
transaction.PaymentDate = DateTime.UtcNow;
|
Authority = paymentTx.Authority ?? string.Empty,
|
||||||
transaction.RefId = verifyResult.RefId;
|
Status = "OK"
|
||||||
|
}, ct);
|
||||||
|
|
||||||
var wallet = await _context.UserWallets
|
if (!result.Success)
|
||||||
.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)
|
|
||||||
{
|
{
|
||||||
transaction.PaymentStatus = PaymentStatus.Reject;
|
throw new InvalidOperationException(
|
||||||
|
$"Package verify failed for purchase #{purchaseId}, authority={paymentTx.Authority}: {result.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _context.SaveChangesAsync(ct);
|
if (result.AlreadyPaid)
|
||||||
|
|
||||||
if (verifyResult.IsSuccess)
|
|
||||||
{
|
{
|
||||||
try
|
_logger.LogInformation(
|
||||||
{
|
"ZarinpalReconciliation: package purchase #{PurchaseId} already verified (authority={Authority})",
|
||||||
await _sender.Send(new ActivateClubMembershipCommand
|
purchaseId,
|
||||||
{
|
paymentTx.Authority);
|
||||||
UserId = purchase.UserId,
|
|
||||||
ForceActivation = false
|
|
||||||
}, ct);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
ex,
|
|
||||||
"ZarinpalReconciliation: club activation failed for userId={UserId} (purchase #{PurchaseId})",
|
|
||||||
purchase.UserId, purchaseId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus;
|
|||||||
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackages;
|
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackages;
|
||||||
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackageDetails;
|
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackageDetails;
|
||||||
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPurchaseHistory;
|
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPurchaseHistory;
|
||||||
|
using CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.Common;
|
using CMSMicroservice.Application.Common;
|
||||||
using CMSMicroservice.Application.Common.Exceptions;
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
@@ -17,7 +18,6 @@ using AppModels = CMSMicroservice.Application.Common.Models;
|
|||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CMSMicroservice.Protobuf.Protos;
|
using CMSMicroservice.Protobuf.Protos;
|
||||||
using Microsoft.EntityFrameworkCore;
|
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
|
try
|
||||||
{
|
{
|
||||||
return await _paymentLock.ExecuteAsync(
|
var result = await _sender.Send(new VerifyUserPackagePurchasePaymentCommand
|
||||||
PaymentLockScopes.Verify(purchaseUserId.Value, verifyKey),
|
{
|
||||||
PaymentLockStrategy.WaitForRelease,
|
PurchaseId = request.OrderId,
|
||||||
lockCt => CustomerVerifyPackagePurchaseCore(request, lockCt),
|
Authority = request.Authority ?? string.Empty,
|
||||||
ct);
|
Status = request.Status ?? "NOK"
|
||||||
|
}, context.CancellationToken);
|
||||||
|
|
||||||
|
return MapVerifyPackagePurchaseResponse(result);
|
||||||
}
|
}
|
||||||
catch (PaymentInProgressException ex)
|
catch (PaymentInProgressException ex)
|
||||||
{
|
{
|
||||||
@@ -295,240 +286,25 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchaseCore(
|
private static CustomerVerifyPackagePurchaseResponse MapVerifyPackagePurchaseResponse(
|
||||||
CustomerVerifyPackagePurchaseRequest request,
|
VerifyUserPackagePurchasePaymentResult result)
|
||||||
CancellationToken ct)
|
|
||||||
{
|
{
|
||||||
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
|
return new CustomerVerifyPackagePurchaseResponse
|
||||||
{
|
{
|
||||||
Success = true,
|
Success = result.Success,
|
||||||
Message = alreadyPaid ? "پرداخت قبلاً تأیید شده بود" : "خرید پکیج با موفقیت تایید شد",
|
Message = result.Message,
|
||||||
TransactionId = transaction?.Id ?? 0,
|
TransactionId = result.TransactionId,
|
||||||
ReferenceCode = refCode,
|
ReferenceCode = result.ReferenceCode,
|
||||||
PurchaseInfo = new PackagePurchaseInfo
|
PurchaseInfo = result.Success
|
||||||
{
|
? new PackagePurchaseInfo
|
||||||
PackageId = purchase.PackageId,
|
{
|
||||||
PackageName = purchase.Package?.Title ?? string.Empty,
|
PackageId = result.PackageId,
|
||||||
AmountPaid = purchase.Amount,
|
PackageName = result.PackageName,
|
||||||
PurchaseDate = Timestamp.FromDateTime(DateTime.SpecifyKind(purchase.PurchasedAt, DateTimeKind.Utc))
|
AmountPaid = result.AmountPaid,
|
||||||
}
|
PurchaseDate = Timestamp.FromDateTime(
|
||||||
|
DateTime.SpecifyKind(result.PurchasedAt, DateTimeKind.Utc))
|
||||||
|
}
|
||||||
|
: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user