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

11 KiB
Raw Permalink Blame History

اصلاح جریان پرداخت: انتخاب فقط درگاه‌های فعالِ بک‌اند + بازگشت به دامنه مبدأ

پروژه

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|confirmedfrontend_address را در برابر هاست‌های مجاز اعتبارسنجی می‌کند، درگاهِ فعال را resolve می‌کند (resolveGatewayisGatewayEnabled)، به درگاه 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 (از بک‌اند، بدون تغییر):

{
  "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/configgateways است.
  • الگوی فراخوانی API همان services/response.jsrequest.* با { requireAuth: true } است؛ متد جدیدی لازم نیست.
  • بعد از تغییر: npm run build در nobat724_front تا خطای صفحه/کامپوننت گرفته شود. تست دستی: انتخاب درگاه فعال، رفتن به درگاه، بازگشت به /payment/result روی همان دامنه، و نمایش نتیجه در /payment/{uuid}.
  • edge case: اگر test_mode روشن است، Select نمایش داده نمی‌شود (کارت «درگاه آزمایشی») و باید مثل الان کار کند؛ فقط مطمئن شو منطق جدیدِ gateways جریان test_mode را نمی‌شکند (در test_mode هم gateways یک آیتم دارد ولی UI آن را نشان نمی‌دهد).