feat(payment): unify payment callback endpoint for all gateways and types
This commit is contained in:
@@ -296,13 +296,20 @@ class PaymentController extends BaseController
|
||||
// ── Payment Callback (public — no JWT) ───────────────────────────────────
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/v1/payment/callback/{gateway}',
|
||||
summary: 'Payment gateway callback (public, IP-restricted)',
|
||||
path: '/api/v1/payment/callback',
|
||||
summary: 'Payment gateway callback — single endpoint for every gateway and payment type (public)',
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'gateway',
|
||||
in: 'path',
|
||||
name: 'order_id',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string', example: 'ORD-1712345678-ab12')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'gateway',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description: 'Falls back to the gateway stored on the payment when omitted',
|
||||
schema: new OA\Schema(type: 'string', enum: ['mellat', 'sep'])
|
||||
),
|
||||
],
|
||||
@@ -322,10 +329,23 @@ class PaymentController extends BaseController
|
||||
new OA\Response(response: 404, description: 'Payment not found'),
|
||||
]
|
||||
)]
|
||||
#[Route('/api/v1/payment/callback/{gateway}', methods: ['POST', 'GET'])]
|
||||
public function callback(string $gateway, Request $request): \Symfony\Component\HttpFoundation\Response
|
||||
#[Route(PaymentManager::CALLBACK_PATH, methods: ['POST', 'GET'])]
|
||||
public function callback(Request $request): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$clientIp = $request->getClientIp() ?? '';
|
||||
$clientIp = $request->getClientIp() ?? '';
|
||||
$callbackData = array_merge($request->query->all(), $request->request->all());
|
||||
$orderId = $callbackData['order_id'] ?? $callbackData['ResNum'] ?? '';
|
||||
|
||||
// تک مسیر برای همهٔ درگاهها: نام درگاه از query میآید و در نبودش از خودِ
|
||||
// رکورد پرداخت خوانده میشود، تا آدرسِ ثبتشده نزد بانک هیچوقت عوض نشود.
|
||||
$gateway = (string) ($callbackData['gateway'] ?? '');
|
||||
if ($gateway === '') {
|
||||
$gateway = $this->paymentRepo->findByOrderId((string) $orderId)?->getGateway() ?? '';
|
||||
}
|
||||
if ($gateway === '') {
|
||||
return $this->renderPaymentResult('notfound');
|
||||
}
|
||||
|
||||
// درگاههای ملت و سپ نتیجه را با ریدایرکتِ مرورگرِ کاربر (POST/GET) برمیگردانند،
|
||||
// نه server-to-server؛ پس IP دریافتی، IPِ کاربر است و allowlist شاپرک اعمال نمیشود
|
||||
// (در غیر این صورت هر callback واقعی — از جمله «لغو» — رد میشد). امنیت از طریق چک
|
||||
@@ -336,9 +356,6 @@ class PaymentController extends BaseController
|
||||
return $this->renderPaymentResult('forbidden');
|
||||
}
|
||||
|
||||
$callbackData = array_merge($request->query->all(), $request->request->all());
|
||||
$orderId = $callbackData['order_id'] ?? $callbackData['ResNum'] ?? '';
|
||||
|
||||
// verify امن (transaction + قفل + idempotent + post-action + log) در سرویس.
|
||||
$payment = $this->paymentManager->processCallback($gateway, $callbackData, $clientIp, $orderId);
|
||||
if ($payment === null) {
|
||||
@@ -433,38 +450,8 @@ class PaymentController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/v1/subscription-payment/callback/{gateway}',
|
||||
summary: 'Subscription payment gateway callback (public, IP-restricted)',
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'gateway',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string', enum: ['mellat', 'sep'])
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Callback processed — either a redirect or JSON result',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean'),
|
||||
new OA\Property(property: 'payment', type: 'object'),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(response: 302, description: 'Redirect to frontend with payment result'),
|
||||
new OA\Response(response: 403, description: 'Forbidden — IP not in allowed Shaparak ranges'),
|
||||
new OA\Response(response: 404, description: 'Payment not found'),
|
||||
]
|
||||
)]
|
||||
#[Route('/api/v1/subscription-payment/callback/{gateway}', methods: ['POST', 'GET'])]
|
||||
public function subscriptionCallback(string $gateway, Request $request): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
return $this->callback($gateway, $request);
|
||||
}
|
||||
// پرداخت اشتراک callback اختصاصی ندارد؛ همان `callback()` مشترک همهٔ نوعها را
|
||||
// پردازش میکند و نوع را از رکورد پرداخت میخواند.
|
||||
|
||||
// ── Status ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -249,12 +249,18 @@ final class PaymentManager
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* تک آدرس بازگشت برای همهٔ درگاهها و همهٔ نوعهای پرداخت؛ درگاه و سفارش
|
||||
* بهصورت query param میروند تا مسیر ثابت و قابل ثبت در پنل بانک بماند.
|
||||
*/
|
||||
public const CALLBACK_PATH = '/api/v1/payment/callback';
|
||||
|
||||
public function callbackUrl(Payment $payment): string
|
||||
{
|
||||
$prefix = $payment->getType() === Payment::TYPE_SUBSCRIPTION
|
||||
? '/api/v1/subscription-payment/callback/'
|
||||
: '/api/v1/payment/callback/';
|
||||
return $this->appBaseUrl . $prefix . $payment->getGateway() . '?order_id=' . $payment->getOrderId();
|
||||
return $this->appBaseUrl . self::CALLBACK_PATH . '?' . http_build_query([
|
||||
'gateway' => $payment->getGateway(),
|
||||
'order_id' => $payment->getOrderId(),
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Post-actions ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -33,8 +33,7 @@ class SecurityHeadersSubscriber implements EventSubscriberInterface
|
||||
$isPaymentPage =
|
||||
str_starts_with($path, '/api/v1/payment/order/')
|
||||
|| str_starts_with($path, '/api/v1/payment/pay/')
|
||||
|| str_starts_with($path, '/api/v1/payment/callback/')
|
||||
|| str_starts_with($path, '/api/v1/subscription-payment/callback/');
|
||||
|| $path === '/api/v1/payment/callback';
|
||||
|
||||
$response->headers->set(
|
||||
'Content-Security-Policy',
|
||||
|
||||
Reference in New Issue
Block a user