491b8a0d0f
Build and Deploy to Production / build-and-deploy (push) Successful in 10m10s
Root cause: FrontOffice already converts Toman→Rial (×10) before sending to CMS. ZarinPalPaymentService was multiplying by 10 AGAIN, causing amounts to be 10x too large. Example: user enters 500K Toman → FO sends 5M Rial → CMS did ×10 → 50M Rial sent to ZarinPal → showed 5M Toman instead of 500K. Changes: - ZarinPalPaymentService.InitiatePaymentAsync: remove ×10 (amount already Rial) - ZarinPalPaymentService.VerifyPaymentWithAmountAsync: remove ×10 + accept Rial - ZarinPalPaymentService.VerifyResult.Amount: return Rial (no /10 conversion) - VerifyMagicWalletChargeCommandHandler: remove /10 before calling Verify - PaymentCallbackController: update comments (amount is Rial) - Also includes: improved HTTP error logging for non-200 responses - Also includes: production URL fix (kbs1→kbs2)
250 lines
11 KiB
C#
250 lines
11 KiB
C#
using CMSMicroservice.Application.Common.Interfaces;
|
|
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
|
|
using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge;
|
|
using CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge;
|
|
using MediatR;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace CMSMicroservice.WebApi.Controllers;
|
|
|
|
/// <summary>
|
|
/// Callback endpoint for payment gateways (ZarinPal, etc.)
|
|
/// درگاه پرداخت بعد از پرداخت (یا لغو) کاربر را به اینجا redirect میکند
|
|
/// </summary>
|
|
[ApiController]
|
|
[AllowAnonymous] // کاربر از درگاه بانک برمیگردد — JWT ندارد
|
|
[ApiExplorerSettings(GroupName = "cms")]
|
|
public class PaymentCallbackController : ControllerBase
|
|
{
|
|
private readonly ISender _sender;
|
|
private readonly IPaymentGatewayService _paymentGateway;
|
|
private readonly IApplicationDbContext _context;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly ILogger<PaymentCallbackController> _logger;
|
|
|
|
public PaymentCallbackController(
|
|
ISender sender,
|
|
IPaymentGatewayService paymentGateway,
|
|
IApplicationDbContext context,
|
|
IConfiguration configuration,
|
|
ILogger<PaymentCallbackController> logger)
|
|
{
|
|
_sender = sender;
|
|
_paymentGateway = paymentGateway;
|
|
_context = context;
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback برای پرداخت سفارش فروشگاه تخفیفی
|
|
/// زرینپال کاربر را با Authority و Status به این endpoint برمیگرداند
|
|
/// </summary>
|
|
[HttpGet("/api/payment/discount-order/callback")]
|
|
public async Task<IActionResult> DiscountOrderCallback(
|
|
[FromQuery] long orderId,
|
|
[FromQuery(Name = "Authority")] string? authority,
|
|
[FromQuery(Name = "Status")] string? status,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
|
|
|
|
_logger.LogInformation(
|
|
"Payment callback received: OrderId={OrderId}, Authority={Authority}, Status={Status}",
|
|
orderId, authority, status);
|
|
|
|
try
|
|
{
|
|
// پیدا کردن سفارش و تراکنش
|
|
var order = await _context.DiscountOrders
|
|
.Include(o => o.OrderDetails)
|
|
.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
|
|
|
|
if (order == null)
|
|
{
|
|
_logger.LogError("Payment callback: Order #{OrderId} not found", orderId);
|
|
return Redirect($"{frontOfficeBaseUrl}/discount-store/orders?error=order-not-found");
|
|
}
|
|
|
|
var transaction = order.TransactionId.HasValue
|
|
? await _context.Transactions.FirstOrDefaultAsync(
|
|
t => t.Id == order.TransactionId.Value, cancellationToken)
|
|
: null;
|
|
|
|
// تأیید پرداخت از درگاه
|
|
bool paymentSuccess = false;
|
|
string? refId = null;
|
|
|
|
if (string.Equals(status, "OK", StringComparison.OrdinalIgnoreCase)
|
|
&& !string.IsNullOrEmpty(authority))
|
|
{
|
|
// Verify با مبلغ از دیتابیس (ریال)
|
|
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
|
authority,
|
|
status!,
|
|
order.GatewayAmountPaid, // مبلغ به ریال
|
|
cancellationToken);
|
|
|
|
paymentSuccess = verifyResult.IsSuccess;
|
|
refId = verifyResult.TrackingCode ?? verifyResult.RefId;
|
|
|
|
// آپدیت PaymentTransaction با نتیجه verify
|
|
var paymentTx = await _context.PaymentTransactions
|
|
.FirstOrDefaultAsync(pt => pt.Authority == authority, cancellationToken);
|
|
if (paymentTx != null)
|
|
{
|
|
paymentTx.PaymentStatus = verifyResult.IsSuccess;
|
|
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
|
|
paymentTx.VerificationStatusMessage = verifyResult.Message;
|
|
paymentTx.CardPan = verifyResult.CardPan;
|
|
paymentTx.CardHash = verifyResult.CardHash;
|
|
paymentTx.RefId = verifyResult.TrackingCode;
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
}
|
|
|
|
_logger.LogInformation(
|
|
"Payment verification for Order #{OrderId}: Success={Success}, RefId={RefId}, Message={Message}",
|
|
orderId, paymentSuccess, refId, verifyResult.Message);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("Payment cancelled by user for Order #{OrderId}", orderId);
|
|
}
|
|
|
|
// تکمیل سفارش از طریق CQRS
|
|
var completeResult = await _sender.Send(new CompleteOrderPaymentCommand
|
|
{
|
|
OrderId = orderId,
|
|
TransactionId = transaction?.Id ?? 0,
|
|
PaymentSuccess = paymentSuccess,
|
|
RefId = refId
|
|
}, cancellationToken);
|
|
|
|
// Redirect به FrontOffice
|
|
if (paymentSuccess && completeResult.Success)
|
|
{
|
|
_logger.LogInformation("Payment completed successfully for Order #{OrderId}", orderId);
|
|
return Redirect(
|
|
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=success");
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("Payment failed for Order #{OrderId}", orderId);
|
|
return Redirect(
|
|
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=failed");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Payment callback error for Order #{OrderId}", orderId);
|
|
return Redirect(
|
|
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=error");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback برای شارژ کیفپول جادویی — زرینپال بعد از پرداخت کاربر را اینجا برمیگرداند
|
|
/// </summary>
|
|
[HttpGet("/api/wallet/verify-magic-charge")]
|
|
public async Task<IActionResult> MagicChargeCallback(
|
|
[FromQuery(Name = "Authority")] string? authority,
|
|
[FromQuery(Name = "Status")] string? status,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
|
|
|
|
_logger.LogInformation(
|
|
"Magic charge callback received: Authority={Authority}, Status={Status}",
|
|
authority, status);
|
|
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(authority))
|
|
{
|
|
_logger.LogError("Magic charge callback: Authority is missing");
|
|
return Redirect($"{frontOfficeBaseUrl}/magic-wallet?payment=error&reason=no-authority");
|
|
}
|
|
|
|
var result = await _sender.Send(new VerifyMagicWalletChargeCommand
|
|
{
|
|
Authority = authority,
|
|
Status = status ?? "NOK"
|
|
}, cancellationToken);
|
|
|
|
_logger.LogInformation(
|
|
"Magic charge completed successfully. Authority={Authority}",
|
|
authority);
|
|
|
|
return Redirect($"{frontOfficeBaseUrl}/magic-wallet?payment=success");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Magic charge callback error. Authority={Authority}", authority);
|
|
return Redirect($"{frontOfficeBaseUrl}/magic-wallet?payment=failed");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback برای شارژ کیفپول تخفیفی — زرینپال بعد از پرداخت کاربر را اینجا برمیگرداند
|
|
/// </summary>
|
|
[HttpGet("/api/wallet/verify-discount-charge")]
|
|
public async Task<IActionResult> DiscountChargeCallback(
|
|
[FromQuery(Name = "Authority")] string? authority,
|
|
[FromQuery(Name = "Status")] string? status,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
|
|
|
|
_logger.LogInformation(
|
|
"Discount charge callback received: Authority={Authority}, Status={Status}",
|
|
authority, status);
|
|
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(authority))
|
|
{
|
|
_logger.LogError("Discount charge callback: Authority is missing");
|
|
return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=error&reason=no-authority");
|
|
}
|
|
|
|
// پیدا کردن PaymentTransaction برای استخراج UserId و Amount
|
|
var paymentTx = await _context.PaymentTransactions
|
|
.FirstOrDefaultAsync(pt => pt.Authority == authority, cancellationToken);
|
|
|
|
if (paymentTx == null || !paymentTx.UserId.HasValue)
|
|
{
|
|
_logger.LogError("Discount charge callback: PaymentTransaction not found for Authority={Authority}", authority);
|
|
return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=error&reason=tx-not-found");
|
|
}
|
|
|
|
if (!string.Equals(status, "OK", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_logger.LogWarning("Discount charge cancelled by user. Authority={Authority}", authority);
|
|
return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=cancelled");
|
|
}
|
|
|
|
var result = await _sender.Send(new VerifyDiscountWalletChargeCommand
|
|
{
|
|
UserId = paymentTx.UserId.Value,
|
|
Amount = paymentTx.Amount,
|
|
Authority = authority
|
|
}, cancellationToken);
|
|
|
|
_logger.LogInformation(
|
|
"Discount charge completed successfully. Authority={Authority}, UserId={UserId}",
|
|
authority, paymentTx.UserId.Value);
|
|
|
|
return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=success");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Discount charge callback error. Authority={Authority}", authority);
|
|
return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=failed");
|
|
}
|
|
}
|
|
}
|