Files
CMS/src/CMSMicroservice.Application/DiscountShopCQ/Commands/PlaceOrder/PlaceOrderCommandHandler.cs
T
masoodafar-web 1e7c17f090
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m22s
feat: Update payment processing and callback mechanisms
- Refactor ActivateClubMembershipCommandHandler to use UserPackagePurchases instead of UserOrders for package activation.
- Modify PlaceOrderCommandHandler to redirect payment callbacks to the front office.
- Update ChargeDiscountWalletCommandHandler and ChargeMagicWalletCommandHandler to direct payment callbacks to the front office.
- Remove PaymentCallbackController and integrate payment verification directly into DiscountOrderService and UserWalletService.
- Add CustomerVerifyDiscountOrderPayment RPC to DiscountOrderService for verifying discount order payments.
- Implement VerifyMagicCharge and VerifyDiscountCharge methods in UserWalletService for wallet charge verifications.
- Update appsettings.json to use local URLs for development.
- Remove appsettings.Development.json as it is no longer needed.
- Comment out history tracking methods in ClubMembershipCycle and Package classes.
- Update PackageService to automatically activate club membership after successful payment verification.
- Adjust UserService to generate JWT tokens with user details.
2026-02-28 03:27:46 +03:30

325 lines
13 KiB
C#

using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Services;
using CMSMicroservice.Domain.Entities.DiscountShop;
using CMSMicroservice.Domain.Entities.Payment;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.PlaceOrder;
public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, PlaceOrderResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IInventoryService _inventoryService;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IConfiguration _configuration;
private readonly ILogger<PlaceOrderCommandHandler> _logger;
public PlaceOrderCommandHandler(
IApplicationDbContext context,
IInventoryService inventoryService,
IPaymentGatewayService paymentGateway,
IConfiguration configuration,
ILogger<PlaceOrderCommandHandler> logger)
{
_context = context;
_inventoryService = inventoryService;
_paymentGateway = paymentGateway;
_configuration = configuration;
_logger = logger;
}
public async Task<PlaceOrderResponseDto> Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
{
// Get user wallet
var userWallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == request.UserId, cancellationToken);
if (userWallet == null)
{
return new PlaceOrderResponseDto
{
Success = false,
Message = "کیف پول کاربر یافت نشد"
};
}
// Get cart items with products
var cartItems = await _context.DiscountShoppingCarts
.Where(c => c.UserId == request.UserId)
.Include(c => c.Product)
.ToListAsync(cancellationToken);
if (!cartItems.Any())
{
return new PlaceOrderResponseDto
{
Success = false,
Message = "سبد خرید خالی است"
};
}
// Validate stock and calculate totals
long totalAmount = 0;
long totalDiscountAmount = 0;
var orderDetails = new List<DiscountOrderDetail>();
foreach (var cartItem in cartItems)
{
var product = cartItem.Product;
// Check stock
if (product.RemainingCount < cartItem.Count)
{
return new PlaceOrderResponseDto
{
Success = false,
Message = $"موجودی محصول '{product.Title}' کافی نیست"
};
}
// Check if product is active
if (!product.IsActive)
{
return new PlaceOrderResponseDto
{
Success = false,
Message = $"محصول '{product.Title}' غیرفعال است"
};
}
// Calculate discount for this product
var itemTotal = product.Price * cartItem.Count;
var maxDiscountForItem = (itemTotal * product.MaxDiscountPercent) / 100;
totalAmount += itemTotal;
totalDiscountAmount += maxDiscountForItem;
orderDetails.Add(new DiscountOrderDetail
{
ProductId = product.Id,
Count = cartItem.Count,
UnitPrice = product.Price,
DiscountPercentUsed = product.MaxDiscountPercent,
DiscountAmount = maxDiscountForItem,
FinalPrice = itemTotal - maxDiscountForItem
});
}
// Always apply maximum possible discount (100%) — user cannot choose less
var maxDiscountBalanceUsable = totalDiscountAmount;
// بررسی کافی بودن موجودی کیف پول اعتباری (تخفیفی)
if (userWallet.DiscountBalance < maxDiscountBalanceUsable)
{
return new PlaceOrderResponseDto
{
Success = false,
Message = $"موجودی کیف پول اعتباری کافی نیست. مبلغ مورد نیاز: {maxDiscountBalanceUsable:N0} تومان، موجودی شما: {userWallet.DiscountBalance:N0} تومان"
};
}
var actualDiscountBalanceUsed = maxDiscountBalanceUsable;
_logger.LogInformation(
"Discount auto-applied: max allowed={MaxAllowed}, wallet balance={WalletBalance}, used={Used}",
maxDiscountBalanceUsable, userWallet.DiscountBalance, actualDiscountBalanceUsed);
var gatewayAmountRequired = totalAmount - actualDiscountBalanceUsed;
// Calculate VAT using centralized calculator
var vatBreakdown = VatCalculator.CalculateBreakdown(gatewayAmountRequired);
var vatAmount = vatBreakdown.VatAmount;
var finalGatewayAmount = vatBreakdown.GrossAmount;
// Create transaction for gateway payment
var transaction = new Transaction
{
Amount = finalGatewayAmount,
Description = $"خرید از فروشگاه تخفیف - مبلغ کل: {totalAmount:N0}، اعتبار تخفیف: {actualDiscountBalanceUsed:N0}",
PaymentStatus = PaymentStatus.Pending,
Type = TransactionType.DiscountShopPurchase
};
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(cancellationToken);
// Create order
var order = new DiscountOrder
{
UserId = request.UserId,
TotalAmount = totalAmount,
DiscountBalanceUsed = actualDiscountBalanceUsed,
GatewayAmountPaid = finalGatewayAmount,
VatAmount = vatAmount,
PaymentStatus = PaymentStatus.Pending,
TransactionId = transaction.Id,
UserAddressId = request.UserAddressId,
DeliveryStatus = DeliveryStatus.Pending
};
_context.DiscountOrders.Add(order);
await _context.SaveChangesAsync(cancellationToken);
// Add order details
foreach (var detail in orderDetails)
{
detail.DiscountOrderId = order.Id;
}
_context.DiscountOrderDetails.AddRange(orderDetails);
// رزرو موجودی برای سفارش pending
foreach (var cartItem in cartItems)
{
await _inventoryService.ReserveStockAsync(
cartItem.ProductId,
ProductType.DiscountProduct,
cartItem.Count,
order.Id,
cancellationToken);
}
// Clear cart
_context.DiscountShoppingCarts.RemoveRange(cartItems);
await _context.SaveChangesAsync(cancellationToken);
// اگر مبلغ درگاه > ۰ باشد، باید به درگاه پرداخت متصل شویم
string? paymentUrl = null;
if (finalGatewayAmount > 0)
{
try
{
// آدرس callback — زرین‌پال بعد از پرداخت مستقیم به فرانت‌آفیس هدایت می‌کند
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
var callbackUrl = $"{frontOfficeBaseUrl}/profile/payment-callback?type=discount-order&orderId={order.Id}";
// درخواست به درگاه
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
Amount = finalGatewayAmount,
UserId = request.UserId,
Description = $"فروشگاه تخفیفی - سفارش #{order.Id}",
CallbackUrl = callbackUrl
}, cancellationToken);
if (paymentResult.IsSuccess && !string.IsNullOrEmpty(paymentResult.GatewayUrl))
{
// ذخیره Authority/RefId در تراکنش برای verify بعدی
transaction.RefId = paymentResult.RefId;
// ثبت PaymentTransaction — جدول جدید با اطلاعات درگاه
var paymentTx = new PaymentTransaction
{
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
Amount = finalGatewayAmount,
CallbackUrl = callbackUrl,
Description = $"فروشگاه تخفیفی - سفارش #{order.Id}",
Mobile = null,
UserId = request.UserId,
RequestStatusCode = 100,
RequestStatusMessage = "Success",
Authority = paymentResult.RefId,
PaymentStatus = false, // هنوز verify نشده
TransactionId = transaction.Id,
OrderId = order.Id.ToString()
};
_context.PaymentTransactions.Add(paymentTx);
await _context.SaveChangesAsync(cancellationToken);
paymentUrl = paymentResult.GatewayUrl;
_logger.LogInformation(
"Payment gateway initiated for DiscountOrder #{OrderId}: RefId={RefId}, Url={Url}",
order.Id, paymentResult.RefId, paymentResult.GatewayUrl);
}
else
{
_logger.LogError(
"Payment gateway initiation failed for DiscountOrder #{OrderId}: {Error}",
order.Id, paymentResult.ErrorMessage);
return new PlaceOrderResponseDto
{
Success = false,
Message = $"خطا در اتصال به درگاه پرداخت: {paymentResult.ErrorMessage}",
OrderId = order.Id
};
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment gateway exception for DiscountOrder #{OrderId}", order.Id);
return new PlaceOrderResponseDto
{
Success = false,
Message = $"خطا در اتصال به درگاه پرداخت: {ex.Message}",
OrderId = order.Id
};
}
}
else
{
// اگر کل مبلغ از کیف تخفیفی پرداخت شد — مستقیماً تکمیل شود
transaction.PaymentStatus = PaymentStatus.Success;
transaction.PaymentDate = DateTime.Now;
order.PaymentStatus = PaymentStatus.Success;
order.PaymentDate = DateTime.Now;
order.DeliveryStatus = DeliveryStatus.Pending;
var walletForDeduct = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == request.UserId, cancellationToken);
if (walletForDeduct != null && actualDiscountBalanceUsed > 0)
{
walletForDeduct.DiscountBalance -= actualDiscountBalanceUsed;
// ثبت لاگ تغییرات کیف پول
_context.UserWalletHistories.Add(new Domain.Entities.UserWalletHistory
{
WalletId = walletForDeduct.Id,
CurrentBalance = walletForDeduct.Balance,
ChangeValue = 0,
CurrentNetworkBalance = walletForDeduct.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = walletForDeduct.DiscountBalance,
ChangeDiscountValue = -actualDiscountBalanceUsed,
IsIncrease = false,
RefrenceId = transaction.Id
});
}
foreach (var cartItem in cartItems)
{
await _inventoryService.ConfirmSaleAsync(
cartItem.ProductId, ProductType.DiscountProduct,
cartItem.Count, order.Id, cancellationToken);
cartItem.Product.SaleCount += cartItem.Count;
}
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"DiscountOrder #{OrderId} fully paid via discount balance ({Amount} T)",
order.Id, actualDiscountBalanceUsed);
}
return new PlaceOrderResponseDto
{
Success = true,
Message = finalGatewayAmount > 0
? "سفارش ایجاد شد. در حال انتقال به درگاه پرداخت..."
: "سفارش با موفقیت ثبت و پرداخت شد",
OrderId = order.Id,
TransactionId = transaction.Id,
TotalAmount = totalAmount,
DiscountBalanceUsed = actualDiscountBalanceUsed,
GatewayAmountRequired = finalGatewayAmount,
PaymentUrl = paymentUrl
};
}
}