feat: integrate PYMS payment gateway, add blog/sitepage/image services, local file manager
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m44s

Payment Gateway:
- Add PYMSPaymentService: IPaymentGatewayService via gRPC to PYMS microservice
- Add ZarinPalPaymentService: direct ZarinPal integration (backup)
- Register 'pyms' payment provider in DI ConfigureServices
- Add PYMS proto files (pyms_transaction.proto, pyms_public_messages.proto)
- Fix VerifyDiscountWalletCharge: pass 'OK' as status instead of Authority
- Update appsettings: PaymentProvider=pyms, sandbox mode, merchant ID

Blog System:
- Add BlogCategory, BlogPost, BlogPostImage entities and CQRS
- Add proto files and gRPC services for blog management
- Add Mapster profiles for blog responses

Content Management:
- Add SitePage entity and CQRS for static pages
- Add proto and gRPC service for site pages

Image/File Management:
- Add LocalFileManager with disk storage + base64 serving + FMS fallback
- Add ImagePathResolverInterceptor for gRPC responses
- Add ImageResolverService for explicit image resolution
- Add UploadsController for public file serving with FMS fallback
- Add PaymentCallbackController for discount order payment callbacks

Database:
- Add blog and content entity migrations
- Remove ImagePath MaxLength constraints
- Remove old FileManagementService (replaced by LocalFileManager)
This commit is contained in:
masoodafar-web
2026-02-15 23:01:16 +03:30
parent 5a4e4a960d
commit 2502cbbda2
177 changed files with 16632 additions and 487 deletions
@@ -5,6 +5,8 @@ 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;
@@ -12,13 +14,22 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
{
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)
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)
@@ -172,15 +183,102 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
await _context.SaveChangesAsync(cancellationToken);
// اگر مبلغ درگاه > ۰ باشد، باید به درگاه پرداخت متصل شویم
string? paymentUrl = null;
if (finalGatewayAmount > 0)
{
try
{
// آدرس callback — زرین‌پال بعد از پرداخت کاربر را به اینجا هدایت می‌کند
var cmsBaseUrl = _configuration["CmsBaseUrl"] ?? "https://localhost:32846";
var callbackUrl = $"{cmsBaseUrl}/api/payment/discount-order/callback?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;
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.InTransit;
var walletForDeduct = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == request.UserId, cancellationToken);
if (walletForDeduct != null)
walletForDeduct.DiscountBalance -= actualDiscountBalanceUsed;
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 = "سفارش ایجاد شد. لطفا پرداخت را تکمیل کنید",
Message = finalGatewayAmount > 0
? "سفارش ایجاد شد. در حال انتقال به درگاه پرداخت..."
: "سفارش با موفقیت ثبت و پرداخت شد",
OrderId = order.Id,
TransactionId = transaction.Id,
TotalAmount = totalAmount,
DiscountBalanceUsed = actualDiscountBalanceUsed,
GatewayAmountRequired = finalGatewayAmount
GatewayAmountRequired = finalGatewayAmount,
PaymentUrl = paymentUrl
};
}
}