15 KiB
رفع باگهای پروداکشن: CORS + payment/config 500 + درگاههای فعال و callback ملت
پروژه
clinicpro (Backend Symfony + پنل ادمین React). دو مورد cross-repo با سایت عمومی nobat724_front دارد (دامنههای عمومی و CORS) که در همینجا فقط سمت backend اصلاح میشود.
زمینه
در پروداکشن (backend روی https://clinic-pro.ir، سایت عمومی روی دامنههای چند-شهری مثل https://yasuj-nobat.ir) سه باگ گزارش شده:
- CORS: از
https://yasuj-nobat.irدرخواست بهhttps://clinic-pro.ir/api/v1/user/send-codeبا خطای preflight رد میشود:No 'Access-Control-Allow-Origin' header is present. - payment/config 500: در
https://clinic-pro.ir/admin/subscriptionفراخوانیGET /api/v1/payment/configمکرراً500میدهد. - درگاهها: الان فقط درگاه ملت فعال است ولی UI هر دو درگاه (ملت + سپ) را نشان میدهد؛ باید فقط درگاههای فعال قابلانتخاب باشند. همچنین هر درگاه باید callback مخصوص خودش داشته باشد و مطابق راهنمای IPG ملت (
callBackUrlباید روی دامنهٔ ثبتشده باشد).
نکته تشخیصی:
GET /api/v1/payment/configروی محیط لوکال ddev با توکن ادمین 200 برمیگرداند ({"success":true,"data":{"test_mode":false,"appointment_fee_rials":15000}}). پس 500 مختص محیط/دادهٔ پروداکشن است و نباید حدس زد — ابتدا استثنای واقعی از لاگ پروداکشن استخراج شود، سپس endpoint مقاومسازی شود.
فایلهای مرتبط
| فایل | نقش |
|---|---|
config/packages/nelmio_cors.yaml |
تنظیم CORS، مبتنی بر env CORS_ALLOW_ORIGIN (regex) |
.env / .env.example (+ env پروداکشن) |
مقدار CORS_ALLOW_ORIGIN و ALLOWED_FRONTEND_HOSTS و APP_BASE_URL |
src/Payment/Controller/PaymentController.php |
config() (خط ۵۴۶)، initiateAppointment()، callback()، resolveGateway() |
src/Payment/Gateway/MellatGateway.php |
درگاه ملت؛ اعتبارنامه از SiteConfig با fallback به env |
src/Payment/Gateway/SepGateway.php |
درگاه سپ |
src/Config/Repository/SiteConfigRepository.php |
کلیدهای mellat_*, sep_terminal_id, payment_test_mode, appointment_fee_rials |
assets/admin/pages/SubscriptionPage.tsx |
انتخاب درگاه (خط ۳۲۸ آرایهٔ hardcode ['mellat','sep']) |
assets/admin/pages/AdminSubscriptionPage.tsx |
مشابه، در صورت داشتن انتخاب درگاه |
docs/api/payment.md |
مستند endpointها |
وضعیت فعلی (کد واقعی)
CORS — config/packages/nelmio_cors.yaml
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ["%env(CORS_ALLOW_ORIGIN)%"]
allow_methods: ["GET", "OPTIONS", "POST", "PATCH", "DELETE"]
allow_headers:
[
"Content-Type",
"Authorization",
"X-CSRF-Token",
"Content-Disposition",
]
paths:
"^/api/": { allow_origin: ["%env(CORS_ALLOW_ORIGIN)%"] }
"^/oauth/": { allow_origin: ["%env(CORS_ALLOW_ORIGIN)%"] }
"^/health": { allow_origin: ["%env(CORS_ALLOW_ORIGIN)%"] }
.env فعلی (لوکال) — فقط دامنههای ddev/localhost را مجاز میکند:
CORS_ALLOW_ORIGIN='^https?://([a-z0-9-]+\.)*(clinic-pro\.ddev\.site|localhost|127\.0\.0\.1)(:[0-9]+)?$'
ALLOWED_FRONTEND_HOSTS=clinic-pro.ddev.site,localhost,yazd-nobat.localhost
.env.example هنوز placeholder دارد: CORS_ALLOW_ORIGIN='^https://your-domain\.com$'.
payment/config — PaymentController::config() (خط ۵۴۶)
#[IsGranted('IS_AUTHENTICATED_FULLY')]
#[Route('/api/v1/payment/config', methods: ['GET'])]
public function config(): JsonResponse
{
return $this->success([
'test_mode' => $this->configRepo->get('payment_test_mode') === '1',
'appointment_fee_rials' => (int) $this->configRepo->get('appointment_fee_rials'),
]);
}
resolveGateway + callback (کد واقعی)
private function resolveGateway(string $name): ?PaymentGatewayInterface
{
if ($this->configRepo->get('payment_test_mode') === '1') return $this->mock;
return match ($name) {
'mellat' => $this->mellat,
'sep' => $this->sep,
default => null,
};
}
// در initiateAppointment: callback هر درگاه از APP_BASE_URL ساخته میشود
$callbackUrl = $this->appBaseUrl . '/api/v1/payment/callback/' . $gatewayName . '?order_id=' . $payment->getOrderId();
$result = $gateway->initiate($payment->getAmountRials(), $payment->getOrderId(), $callbackUrl);
روتهای callback موجود (per-gateway، عمومی و IP-restricted):
POST|GET /api/v1/payment/callback/{gateway}
POST|GET /api/v1/subscription-payment/callback/{gateway}
MellatGateway — تشخیص فعالبودن
private function cfg(string $key, string $envFallback): string
{
return $this->configRepo->get($key) ?: $envFallback;
}
// اعتبارنامهها: mellat_terminal_id / mellat_username / mellat_password
فرانت — SubscriptionPage.tsx
const GATEWAY_LABELS: Record<string, string> = { mellat: 'بانک ملت', sep: 'سپ (سامان کیش)' };
const [selectedGateway, setSelectedGateway] = useState<'mellat' | 'sep'>('mellat');
// ...
{(['mellat', 'sep'] as const).map((gw) => ( /* دکمهٔ انتخاب هر دو درگاه، همیشه */ ))}
وظایف
۱. رفع CORS پروداکشن
ریشه: regex CORS_ALLOW_ORIGIN در env پروداکشن دامنههای عمومی چند-شهری (*-nobat.ir) و خودِ clinic-pro.ir را پوشش نمیدهد؛ در نتیجه preflight OPTIONS /api/v1/user/send-code هدر Access-Control-Allow-Origin نمیگیرد و مرورگر بلاک میکند.
- در env پروداکشن (و
.env/.env.exampleبهعنوان مرجع) مقدارCORS_ALLOW_ORIGINرا به regexی تغییر بده که همهٔ دامنههای عمومی<city>-nobat.ir(با/بدون subdomain وwww) و پنلclinic-pro.irرا مجاز کند. نمونهٔ پیشنهادی:
CORS_ALLOW_ORIGIN='^https://([a-z0-9-]+\.)*([a-z0-9-]+-nobat\.ir|clinic-pro\.ir)$'
ALLOWED_FRONTEND_HOSTS(لیست میزبانهای مجاز برای redirect پرداخت، درPaymentController::allowedHosts()) نیز باید شامل دامنههای عمومی پروداکشن باشد، مثلاً:
ALLOWED_FRONTEND_HOSTS=clinic-pro.ir,yasuj-nobat.ir,yazd-nobat.ir
.env.exampleرا از placeholderyour-domain.comبه همین الگو بهروزرسانی کن تا برای دیپلویهای بعدی درست باشد.- بعد از تغییر env روی سرور:
php bin/console cache:clear(regex در کش کانتینر خوانده میشود). - تأیید: با
curl -i -X OPTIONS 'https://clinic-pro.ir/api/v1/user/send-code' -H 'Origin: https://yasuj-nobat.ir' -H 'Access-Control-Request-Method: POST'باید هدرAccess-Control-Allow-Origin: https://yasuj-nobat.irبرگردد.
۲. رفع payment/config 500 + فقط درگاههای فعال
۲.۱ استخراج علت واقعی 500 (بدون حدس): روی پروداکشن لاگ استثنای /api/v1/payment/config را بگیر (var/log/prod.log یا لاگ کانتینر/docker logs). چون لوکال 200 میدهد، علت محتمل یکی از اینهاست — تأیید کن، حدس نزن:
- مهاجرتهای اجرانشده در پروداکشن (جدول
site_configیا ستونها) →php bin/console doctrine:migrations:migrateروی prod. - خطای اتصال/کوئری
SiteConfigRepositoryهنگام خواندن کلید. - ناسازگاری کد دیپلویشده با DEFAULTS جدید در
SiteConfigRepository.
۲.۲ مقاومسازی و افزودن درگاههای فعال به پاسخ: config() را طوری تغییر بده که علاوه بر test_mode و appointment_fee_rials، لیست درگاههای فعال را برگرداند تا فرانت فقط همانها را نشان دهد. یک درگاه «فعال» است اگر اعتبارنامههایش (در SiteConfig یا env) ست شده باشند:
#[Route('/api/v1/payment/config', methods: ['GET'])]
public function config(): JsonResponse
{
$testMode = $this->configRepo->get('payment_test_mode') === '1';
$cfg = fn(string $key): string => (string) ($this->configRepo->get($key) ?? '');
$gateways = [];
if ($testMode) {
$gateways[] = ['name' => 'mellat', 'label' => 'بانک ملت (آزمایشی)'];
} else {
$mellatActive = $cfg('mellat_terminal_id') !== '' && $cfg('mellat_username') !== '' && $cfg('mellat_password') !== '';
$sepActive = $cfg('sep_terminal_id') !== '';
if ($mellatActive) $gateways[] = ['name' => 'mellat', 'label' => 'بانک ملت'];
if ($sepActive) $gateways[] = ['name' => 'sep', 'label' => 'سپ (سامان کیش)'];
}
return $this->success([
'test_mode' => $testMode,
'appointment_fee_rials' => (int) ($this->configRepo->get('appointment_fee_rials') ?: 0),
'gateways' => $gateways,
]);
}
اگر env پروداکشن هم fallback اعتبارنامهٔ ملت را دارد، توجه کن که
MellatGateway::cfg()اولSiteConfigبعد env را میخواند؛ برای همراستایی، تشخیص «فعال» را هم به همین ترتیب انجام بده (اول کلید DB، اگر خالی بود env مربوطه). در صورت نیاز یک helper خصوصی مشابهcfg($key, $envFallback)در کنترلر اضافه کن.
۲.۳ فرانت — فقط درگاههای فعال: در SubscriptionPage.tsx (و AdminSubscriptionPage.tsx اگر انتخاب درگاه دارد):
- لیست درگاهها را از
GET /api/v1/payment/config→data.gatewaysبخوان، نه آرایهٔ hardcode['mellat','sep']. selectedGatewayرا به اولین درگاه فعال مقداردهی اولیه کن؛ اگر فقط یک درگاه فعال بود، همان بهصورت پیشفرض انتخاب و بقیه نمایش داده نشوند.- اگر
gatewaysخالی بود (هیچ درگاه فعالی نیست) پیام مناسب نشان بده و دکمهٔ پرداخت را غیرفعال کن.
۳. Callback هر درگاه مطابق راهنمای IPG ملت
callback بهازای هر درگاه از قبل وجود دارد (/api/v1/payment/callback/{gateway} و نسخهٔ subscription، ساختهشده از APP_BASE_URL). طبق راهنمای ملت (نگارش ۱.۳۸) نکات الزامی که باید تضمین شوند:
callBackUrlباید روی دامنهٔ ثبتشدهٔ پذیرنده باشد، نه IP (در غیر این صورت کد پاسخ62: «مسیر back call در دامنهٔ ثبتشده نیست»). یعنی در پروداکشنAPP_BASE_URLباید دقیقاًhttps://clinic-pro.ir(دامنهٔ ثبتشده نزد ملت/شاپرک) باشد. env پروداکشن را بررسی و اصلاح کن.- هدر
Refererهنگام Redirect بهstartpay.mellatباید دامنهٔ ثبتشده باشد؛ چون این POST سمت مرورگر انجام میشود، مطمئن شو صفحهای که کاربر را redirect میکند رویclinic-pro.irسرو میشود. - نکتهٔ امنیتی callback (ص ۳۴ راهنما): پس از دریافت Call Back باید
RefIdوSaleOrderIdدریافتی دقیقاً همان مقادیر تراکنش اولیه باشند و در صورت عدم تطابق، تراکنش نامعتبر و ازbpVerifyخودداری شود. درPaymentController::callback()بررسی کن کهorder_idبه Payment درست bind میشود و مبلغ/RefNum تکراری (replay) رد میشود (منطق underpayment/replay موجود است — تأیید و در صورت نقص تکمیل کن). - مستندسازی: در
docs/api/payment.mdجدول callback هر درگاه را دقیق کن:- ملت:
POST/GET {APP_BASE_URL}/api/v1/payment/callback/mellat?order_id=... - سپ:
POST/GET {APP_BASE_URL}/api/v1/payment/callback/sep?order_id=... - نسخهٔ اشتراک:
.../api/v1/subscription-payment/callback/{gateway}
- ملت:
نکات مهم
- همه پاسخها از
BaseController($this->success()/$this->error())؛ ساختار پاسخpayment/configنباید بشکند (فرانتdata?.dataمیخواند). - تغییر قرارداد payment/config فیلد جدید
gatewaysاضافه میکند — علاوه برSubscriptionPage، هر مصرفکنندهٔ دیگر (nobat724_frontاگر این endpoint را صدا میزند) باید سازگار بماند؛ فیلدهای قبلی حذف نشوند (backward-compatible). - بدون Entity جدید → migration لازم نیست؛ فقط اگر پروداکشن مهاجرت اجرانشده دارد، همان اجرا شود.
- CORS:
origin_regex: trueاست، پس مقدار env یک regex است نه لیست دامنه؛ regex را تست کن که همhttps://yasuj-nobat.irو همhttps://www.yasuj-nobat.irو همhttps://clinic-pro.irرا match کند و دامنهٔ ناخواسته را match نکند. - تستها:
ddev exec php -lروی فایلهای PHP تغییریافته.ddev exec php bin/phpunit tests/Payment(تستهای callback موجود نشکند).ddev exec php bin/console cache:clearبعد از تغییر nelmio/env.ddev exec npx tsc --noEmitوddev exec yarn devبرای فرانت.- تست زندهٔ
GET /api/v1/payment/configبا توکن ادمین → بایدgatewaysرا برگرداند.
- مستندات: بعد از تغییر
PaymentController،docs/api/payment.mdرا در همین session بهروز کن (قانون standing پروژه). - بعد از پایان:
graphify update ..