Files
nobat724_front/.claude/prompt/fix-payment-gateway-flow.md
T

162 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# اصلاح جریان پرداخت: انتخاب فقط درگاه‌های فعالِ بک‌اند + بازگشت به دامنه مبدأ
## پروژه
`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` (دامنه مبدأ).
## زمینه
جریان پرداخت نوبت به‌صورت زیر است و **سمت بک‌اند کامل پیاده‌سازی شده**:
1. کاربر روی «پرداخت» می‌زند → frontend با `POST /api/v1/payment/appointment` (همراه JWT) و بدنهٔ `{ appointment_uuid, gateway, frontend_address }` درخواست می‌دهد.
2. بک‌اند **صلاحیت اوردر** را چک می‌کند (مالکیت کاربر، وضعیت نوبت `pending|confirmed``frontend_address` را در برابر هاست‌های مجاز اعتبارسنجی می‌کند، **درگاهِ فعال** را resolve می‌کند (`resolveGateway``isGatewayEnabled`)، به درگاه request می‌زند و `redirect_url` را برمی‌گرداند.
3. frontend کاربر را با `window.location.href = redirect_url` به درگاه می‌فرستد.
4. بعد از پرداخت، درگاه به `GET/POST /api/v1/payment/callback/{gateway}` (عمومی، محدود به IP شاپرک) برمی‌گردد؛ بک‌اند **صلاحیت پرداخت** را verify می‌کند (مبلغ، replay، مرجع)، وضعیت را ست می‌کند، نوبت را confirm می‌کند و در پایان با `redirectToFrontend` یک **302 به `frontend_address`** می‌زند: `frontend_address?payment_uuid=...&status=...`.
5. چون `frontend_address` همان دامنهٔ مبدأ کاربر است، بازگشت به **همان دامنه‌ای که از اول آمده بود** انجام می‌شود.
پس منطق «ریدایرکت به بک‌اند → تأیید اوردر → درگاه → بازگشت به بک‌اند → تأیید پرداخت → بازگشت به دامنه مبدأ» از قبل کار می‌کند. مشکل فعلی سمت frontend است.
## مشکل / هدف
دو ایراد در `components/appointment/paying/index.js`:
1. **لیست درگاه‌ها هاردکد شده** و به «درگاه‌های فعالِ بک‌اند» توجه نمی‌کند. آرایهٔ ثابت `banks` (mellat, sep) در Select نمایش داده می‌شود، در حالی‌که `GET /api/v1/payment/config` فیلد `gateways` را برمی‌گرداند که فقط شامل درگاه‌های **پیکربندی‌شده و فعال** است (`activeGateways` در بک‌اند). اگر ادمین یک درگاه را در بک‌اند غیرفعال کند، همچنان در frontend نمایش داده می‌شود و انتخاب آن باعث خطای `422 درگاه پرداخت نامعتبر یا غیرفعال است` می‌شود.
2. **`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` (از بک‌اند، بدون تغییر):
```json
{
"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`).
## وضعیت فعلی
```js
// 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` استفاده کن:
```js
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`):
```jsx
<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` سازگار است):
```js
gateway: testMode ? "mellat" : selectedBank,
```
قبل از ارسال، اگر `!testMode && !selectedBank` بود return کن. شرط `disabled` دکمهٔ پرداخت را گسترش بده:
```jsx
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 آن را نشان نمی‌دهد).