diff --git a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommand.cs b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommand.cs new file mode 100644 index 0000000..e946dc6 --- /dev/null +++ b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommand.cs @@ -0,0 +1,26 @@ +using MediatR; + +namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment; + +/// +/// Shared verify + wallet credit path for package IPG payments (callback UI and reconciliation job). +/// +public class VerifyUserPackagePurchasePaymentCommand : IRequest +{ + 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; } +} diff --git a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommandHandler.cs b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommandHandler.cs new file mode 100644 index 0000000..42c61ec --- /dev/null +++ b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommandHandler.cs @@ -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 +{ + private readonly IApplicationDbContext _context; + private readonly IPaymentGatewayService _paymentGateway; + private readonly ISender _sender; + private readonly IUserPaymentLock _paymentLock; + private readonly ILogger _logger; + + public VerifyUserPackagePurchasePaymentCommandHandler( + IApplicationDbContext context, + IPaymentGatewayService paymentGateway, + ISender sender, + IUserPaymentLock paymentLock, + ILogger logger) + { + _context = context; + _paymentGateway = paymentGateway; + _sender = sender; + _paymentLock = paymentLock; + _logger = logger; + } + + public async Task 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 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 + }; + } +} diff --git a/src/CMSMicroservice.Infrastructure/BackgroundJobs/ZarinpalReconciliationJob.cs b/src/CMSMicroservice.Infrastructure/BackgroundJobs/ZarinpalReconciliationJob.cs index 36be607..4e402f7 100644 --- a/src/CMSMicroservice.Infrastructure/BackgroundJobs/ZarinpalReconciliationJob.cs +++ b/src/CMSMicroservice.Infrastructure/BackgroundJobs/ZarinpalReconciliationJob.cs @@ -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); } } diff --git a/src/CMSMicroservice.WebApi/Services/PackageService.cs b/src/CMSMicroservice.WebApi/Services/PackageService.cs index 1b8bf84..89b333f 100644 --- a/src/CMSMicroservice.WebApi/Services/PackageService.cs +++ b/src/CMSMicroservice.WebApi/Services/PackageService.cs @@ -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 CustomerVerifyPackagePurchase(CustomerVerifyPackagePurchaseRequest request, ServerCallContext context) + public override async Task 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 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 }; }