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
};
}