feat(payment): enhance payment flow with new pay endpoint and POST redirect method

This commit is contained in:
hamed
2026-07-02 12:19:43 +03:30
parent 02c34bac8e
commit 1e342a695f
5 changed files with 129 additions and 19 deletions
+84 -14
View File
@@ -178,37 +178,107 @@ class PaymentController extends BaseController
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'آدرس بازگشت مجاز نیست', 422, 'frontend_address');
}
$gateway = $this->resolveGateway($gatewayName);
if ($gateway === null) {
// اعتبارسنجی اولیهٔ درگاه (فعال/معتبر بودن)؛ ارتباط با بانک اینجا انجام
// نمی‌شود — در GET /payment/pay هنگام انتقال مرورگر به درگاه انجام می‌گیرد.
if ($this->resolveGateway($gatewayName) === null) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'درگاه پرداخت نامعتبر یا غیرفعال است', 422, 'gateway');
}
if ($this->circuitBreaker->isOpen($gatewayName)) {
return $this->error(ErrorCodes::ERR_PAYMENT_001, ErrorCodes::message(ErrorCodes::ERR_PAYMENT_001), 503);
}
$feeRials = (int) $this->configRepo->get('appointment_fee_rials');
$payment = new Payment($user, $feeRials, $gatewayName, Payment::TYPE_APPOINTMENT, $frontendAddress);
$payment->setAppointment($appointment);
$this->paymentRepo->save($payment);
// مرورگر به این endpoint بک‌اند می‌رود؛ آنجا صلاحیت نهایی + ارتباط با بانک
// + انتقال به درگاه انجام می‌شود. کلاینت هرگز مستقیم به بانک ریکوست نمی‌زند.
return $this->success([
'payment_uuid' => $payment->getUuid(),
'pay_url' => $this->appBaseUrl . '/api/v1/payment/pay/' . $payment->getOrderId(),
'order_id' => $payment->getOrderId(),
]);
}
// ── Gateway hand-off (public — browser redirect to the bank) ───────────────
#[OA\Get(
path: '/api/v1/payment/pay/{orderId}',
summary: 'Redirect the browser to the payment gateway for a pending payment',
parameters: [
new OA\Parameter(name: 'orderId', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(response: 302, description: 'Redirect (GET gateway) or auto-submitting POST form (POST gateway)'),
new OA\Response(response: 404, description: 'Payment not found'),
]
)]
#[Route('/api/v1/payment/pay/{orderId}', methods: ['GET'])]
public function pay(string $orderId): \Symfony\Component\HttpFoundation\Response
{
$payment = $this->paymentRepo->findByOrderId($orderId);
if ($payment === null) {
return new JsonResponse(['success' => false, 'message' => 'payment not found'], 404);
}
// فقط پرداخت در انتظار قابل انتقال به درگاه است (جلوگیری از پرداخت تکراری/replay).
if ($payment->getStatus() !== Payment::STATUS_PENDING) {
return $this->redirectToFrontend($payment, $payment->getStatus() === Payment::STATUS_SUCCESS);
}
$gatewayName = $payment->getGateway();
$gateway = $this->resolveGateway($gatewayName);
$testMode = $this->configRepo->get('payment_test_mode') === '1';
if ($gateway === null || (!$testMode && $this->circuitBreaker->isOpen($gatewayName))) {
$payment->setStatus(Payment::STATUS_FAILED);
$this->paymentRepo->save($payment);
return $this->redirectToFrontend($payment, false);
}
// ارتباط با بانک (init) از سمت بک‌اند انجام می‌شود.
$callbackUrl = $this->appBaseUrl . '/api/v1/payment/callback/' . $gatewayName . '?order_id=' . $payment->getOrderId();
$result = $gateway->initiate($payment->getAmountRials(), $payment->getOrderId(), $callbackUrl);
if (!$result->success) {
$this->circuitBreaker->recordFailure($gatewayName);
return $this->error(ErrorCodes::ERR_PAYMENT_001, $result->errorMessage, 503);
if (!$testMode) {
$this->circuitBreaker->recordFailure($gatewayName);
}
$payment->setStatus(Payment::STATUS_FAILED);
$this->paymentRepo->save($payment);
return $this->redirectToFrontend($payment, false);
}
$this->circuitBreaker->recordSuccess($gatewayName);
if (!$testMode) {
$this->circuitBreaker->recordSuccess($gatewayName);
}
$payment->setGatewayToken($result->token);
$this->paymentRepo->save($payment);
return $this->success([
'payment_uuid' => $payment->getUuid(),
'redirect_url' => $result->redirectUrl,
'order_id' => $payment->getOrderId(),
]);
// انتقال مرورگر به درگاه: 302 برای درگاه GET (سپ/mock) یا فرم auto-submit POST (ملت).
if ($result->redirectMethod === 'POST') {
return $this->autoSubmitForm(strtok($result->redirectUrl, '?'), $result->redirectParams);
}
return new RedirectResponse($result->redirectUrl);
}
/** یک صفحهٔ HTML با فرمی که به‌صورت خودکار (POST) به درگاه ارسال می‌شود. */
private function autoSubmitForm(string $action, array $params): \Symfony\Component\HttpFoundation\Response
{
$fields = '';
foreach ($params as $name => $value) {
$fields .= sprintf(
'<input type="hidden" name="%s" value="%s">',
htmlspecialchars((string) $name, ENT_QUOTES),
htmlspecialchars((string) $value, ENT_QUOTES)
);
}
$safeAction = htmlspecialchars($action, ENT_QUOTES);
$html = <<<HTML
<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><title>در حال انتقال به درگاه پرداخت…</title></head>
<body onload="document.forms[0].submit()"><p style="font-family:Tahoma,sans-serif;text-align:center;margin-top:40px">در حال انتقال به درگاه پرداخت…</p>
<form method="POST" action="{$safeAction}">{$fields}<noscript><button type="submit">ادامه</button></noscript></form></body></html>
HTML;
return new \Symfony\Component\HttpFoundation\Response($html, 200, ['Content-Type' => 'text/html; charset=utf-8']);
}
// ── Payment Callback (public — no JWT) ───────────────────────────────────