11 KiB
اصلاح جریان پرداخت: انتخاب فقط درگاههای فعالِ بکاند + بازگشت به دامنه مبدأ
پروژه
nobat724_front (frontend) + clinicpro (backend) — cross-repo.
بهروزرسانی (جریان انتقال به بانک از طریق بکاند): بهجای اینکه frontend مستقیم
redirect_urlبانک را باز کند، مرورگر باید به یک endpoint بکاند برود و بکاند انتقال به درگاه را انجام دهد. این در بکاند باGET /api/v1/payment/pay/{orderId}پیاده شد (302 برای درگاه GET مثل سپ، فرم auto-submit با POST برای ملت — چون startpay ملت با POST باز میشود).POST /api/v1/payment/appointmentحالا علاوه برredirect_urlیکpay_urlهم برمیگرداند؛ frontend بایدpay_urlرا باز کند. جریان کامل: کلیک → XHR authenticated (تأیید صلاحیت اوردر + init درگاه) → مرورگر بهpay_url(بکاند) → بکاند به بانک → پرداخت → callback بکاند (verify) → 302 بهfrontend_address(دامنه مبدأ).
زمینه
جریان پرداخت نوبت بهصورت زیر است و سمت بکاند کامل پیادهسازی شده:
- کاربر روی «پرداخت» میزند → frontend با
POST /api/v1/payment/appointment(همراه JWT) و بدنهٔ{ appointment_uuid, gateway, frontend_address }درخواست میدهد. - بکاند صلاحیت اوردر را چک میکند (مالکیت کاربر، وضعیت نوبت
pending|confirmed)،frontend_addressرا در برابر هاستهای مجاز اعتبارسنجی میکند، درگاهِ فعال را resolve میکند (resolveGateway→isGatewayEnabled)، به درگاه request میزند وredirect_urlرا برمیگرداند. - frontend کاربر را با
window.location.href = redirect_urlبه درگاه میفرستد. - بعد از پرداخت، درگاه به
GET/POST /api/v1/payment/callback/{gateway}(عمومی، محدود به IP شاپرک) برمیگردد؛ بکاند صلاحیت پرداخت را verify میکند (مبلغ، replay، مرجع)، وضعیت را ست میکند، نوبت را confirm میکند و در پایان باredirectToFrontendیک 302 بهfrontend_addressمیزند:frontend_address?payment_uuid=...&status=.... - چون
frontend_addressهمان دامنهٔ مبدأ کاربر است، بازگشت به همان دامنهای که از اول آمده بود انجام میشود.
پس منطق «ریدایرکت به بکاند → تأیید اوردر → درگاه → بازگشت به بکاند → تأیید پرداخت → بازگشت به دامنه مبدأ» از قبل کار میکند. مشکل فعلی سمت frontend است.
مشکل / هدف
دو ایراد در components/appointment/paying/index.js:
- لیست درگاهها هاردکد شده و به «درگاههای فعالِ بکاند» توجه نمیکند. آرایهٔ ثابت
banks(mellat, sep) در Select نمایش داده میشود، در حالیکهGET /api/v1/payment/configفیلدgatewaysرا برمیگرداند که فقط شامل درگاههای پیکربندیشده و فعال است (activeGatewaysدر بکاند). اگر ادمین یک درگاه را در بکاند غیرفعال کند، همچنان در frontend نمایش داده میشود و انتخاب آن باعث خطای422 درگاه پرداخت نامعتبر یا غیرفعال استمیشود. gatewaysاز config نادیده گرفته میشود؛getPaymentConfigفقطtest_modeوappointment_fee_rialsرا میخواند.
هدف: Select درگاه فقط از config.gateways پر شود، درگاه پیشفرض اولین درگاهِ فعال باشد، و اگر هیچ درگاهی فعال نبود دکمهٔ پرداخت غیرفعال شود.
فایلهای مرتبط
| فایل | نقش |
|---|---|
nobat724_front/components/appointment/paying/index.js |
کامپوننت پرداخت — دکمه، Select درگاه، فراخوانی API |
nobat724_front/services/response.js |
getPaymentConfig، postAppointmentPayment (بدون تغییر) |
nobat724_front/app/payment/result/page.js |
صفحهٔ بازگشت از بکاند؛ payment_uuid را میخواند و به /payment/{uuid} میرود (بدون تغییر) |
قرارداد GET /api/v1/payment/config (از بکاند، بدون تغییر):
{
"success": true,
"data": {
"test_mode": false,
"appointment_fee_rials": 250000,
"gateways": [ { "name": "mellat", "label": "بانک ملت" }, { "name": "sep", "label": "سپ (سامان کیش)" } ]
}
}
در حالت test_mode: true بکاند فقط [{ "name": "mellat", "label": "بانک ملت (آزمایشی)" }] برمیگرداند و درگاه واقعی resolve نمیشود (MockGateway).
وضعیت فعلی
// components/appointment/paying/index.js
const banks = [
{ id: "mellat", title: "بانک ملت" },
{ id: "sep", title: "سامان (سپ)" },
];
function Paying({ ... }) {
const [selectedBank, setSelectedBank] = useState("mellat");
const [testMode, setTestMode] = useState(null);
const [feeRials, setFeeRials] = useState(null);
// ...
useEffect(() => {
request.getPaymentConfig()
.then((res) => {
setTestMode(Boolean(res?.data?.test_mode));
setFeeRials(Number(res?.data?.appointment_fee_rials) || 0);
// ❌ res.data.gateways نادیده گرفته میشود
})
.catch(() => { setTestMode(false); setFeeRials(0); });
}, []);
const handlePayment = async () => {
// ...
const res = await request.postAppointmentPayment({
appointment_uuid: appointmentId,
gateway: testMode ? "mellat" : selectedBank, // ❌ selectedBank از لیست هاردکد
frontend_address: `${window.location.origin}/payment/result`,
});
const redirectUrl = res?.data?.redirect_url;
if (redirectUrl) window.location.href = redirectUrl;
else setStep((prev) => prev + 1);
};
// ...
{/* Select درگاه از آرایهٔ ثابت banks پر میشود */}
}
وظایف
۱. خواندن gateways از config و نگهداری در state
آرایهٔ ثابت banks را حذف کن و بهجای آن از config.gateways استفاده کن:
const [gateways, setGateways] = useState([]); // [{ name, label }]
const [selectedBank, setSelectedBank] = useState(""); // خالی تا config برسد
useEffect(() => {
request.getPaymentConfig()
.then((res) => {
const gws = Array.isArray(res?.data?.gateways) ? res.data.gateways : [];
setTestMode(Boolean(res?.data?.test_mode));
setFeeRials(Number(res?.data?.appointment_fee_rials) || 0);
setGateways(gws);
setSelectedBank(gws[0]?.name ?? ""); // پیشفرض = اولین درگاه فعال
})
.catch(() => { setTestMode(false); setFeeRials(0); setGateways([]); });
}, []);
۲. پر کردن Select از gateways فعال
در JSX، banks.map را با gateways.map جایگزین کن (کلید/مقدار = name، متن = label):
<Select
labelId="bank-select-label"
value={selectedBank}
label="انتخاب درگاه پرداخت"
onChange={(e) => setSelectedBank(e.target.value)}
>
{gateways.map((g) => (
<MenuItem key={g.name} value={g.name}>{g.label}</MenuItem>
))}
</Select>
اگر !testMode && gateways.length === 0 بود، بهجای Select یک پیام «درگاه پرداخت فعالی موجود نیست» نشان بده و دکمهٔ پرداخت را غیرفعال کن.
۳. ارسال درگاه انتخابشده و غیرفعالسازی دکمه در نبود درگاه
در handlePayment، gateway را از selectedBank بفرست (در test_mode بکاند بههرحال Mock را resolve میکند، ولی مقدار mellat سازگار است):
gateway: testMode ? "mellat" : selectedBank,
قبل از ارسال، اگر !testMode && !selectedBank بود return کن. شرط disabled دکمهٔ پرداخت را گسترش بده:
disabled={
loading || testMode === null || feeRials === null ||
(!testMode && !selectedBank)
}
frontend_address: ${window.location.origin}/payment/result`` را بدون تغییر نگهدار — همین تضمین میکند بازگشت به همان دامنهٔ مبدأ (multi-domain) انجام شود.
نکات مهم
- بازگشت به دامنه مبدأ از قبل درست است: چون
frontend_addressازwindow.location.originساخته میشود، بکاند در callback به همان دامنهای که کاربر از آن آمده 302 میزند. این را تغییر نده. - بررسی تنظیمات بکاند (بدون تغییر کد): بکاند در
initiateAppointmentمقدارfrontend_addressرا باisAllowedFrontendدر برابرpayment_allowed_frontend_hosts(SiteConfig، fallback به env) چک میکند؛ اگر لیست خالی باشد یا هاست دامنهٔ شهر در آن نباشد، پاسخ422 آدرس بازگشت مجاز نیستاست و پرداخت شروع نمیشود. برای پشتیبانی از همهٔ دامنههای چند-شهری، مطمئن شو هاست همهٔ دامنهها (بدونhttps://، فقط host مثلyazd-nobat.ir) در این تنظیم موجود است. این تغییر داده/کانفیگ است، نه کد. - درگاهها را در frontend هاردکد نکن؛ منبع واحدِ حقیقت
GET /api/v1/payment/config→gatewaysاست. - الگوی فراخوانی API همان
services/response.js→request.*با{ requireAuth: true }است؛ متد جدیدی لازم نیست. - بعد از تغییر:
npm run buildدرnobat724_frontتا خطای صفحه/کامپوننت گرفته شود. تست دستی: انتخاب درگاه فعال، رفتن به درگاه، بازگشت به/payment/resultروی همان دامنه، و نمایش نتیجه در/payment/{uuid}. - edge case: اگر
test_modeروشن است، Select نمایش داده نمیشود (کارت «درگاه آزمایشی») و باید مثل الان کار کند؛ فقط مطمئن شو منطق جدیدِgatewaysجریان test_mode را نمیشکند (در test_mode همgatewaysیک آیتم دارد ولی UI آن را نشان نمیدهد).