From e6b55c6bad1e58b689c8d94792c4ed6238795183 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Thu, 2 Jul 2026 17:08:52 +0330 Subject: [PATCH] feat: refactor payment handling to use direct backend redirect instead of API call --- .../prompt/payment-single-flow-frontend.md | 97 +++++++++++++++++++ app/payment/[uuid]/page.js | 24 ++--- components/appointment/paying/index.js | 32 +++--- services/response.js | 2 - 4 files changed, 116 insertions(+), 39 deletions(-) create mode 100644 .claude/prompt/payment-single-flow-frontend.md diff --git a/.claude/prompt/payment-single-flow-frontend.md b/.claude/prompt/payment-single-flow-frontend.md new file mode 100644 index 0000000..18f0559 --- /dev/null +++ b/.claude/prompt/payment-single-flow-frontend.md @@ -0,0 +1,97 @@ +# entry پرداخت به‌صورت ریدایرکت خالص به Backend (بدون XHR) — سایت عمومی + +## پروژه + +`nobat724_front` (سایت عمومی). **cross-repo** — پرامپت همتا (Backend، اول اجرا شود): `clinicpro/.claude/prompt/payment-single-flow-consolidation.md`. + +## زمینه + +طبق معماری واحد پرداخت، سایت عمومی نباید هیچ API برای **شروع** پرداخت صدا بزند؛ فقط مرورگر را به Backend ریدایرکت کند. Backend یک entry ریدایرکتِ خالص دارد: + +``` +GET {API}/api/v1/payment/order/{appointmentUuid}?gateway=&return= +``` + +Backend این آدرس را می‌گیرد، سفارش را اعتبارسنجی می‌کند، `Payment` می‌سازد، به بانک وصل می‌شود و کاربر را به شاپرک می‌فرستد؛ در پایان به `return` (همان دامنهٔ مبدأ) با `?status=...` برمی‌گردد. + +وضعیت فعلی سایت: `components/appointment/paying/index.js` هنوز با **XHR** (`request.postAppointmentPayment`) پرداخت را شروع می‌کند و بعد به `pay_url` می‌رود. طبق نیاز جدید باید این XHR حذف شود و دکمهٔ پرداخت مستقیماً به `payment/order/{uuid}` ریدایرکت کند. + +## مشکل / هدف + +حذف کامل فراخوانی API برای شروع پرداخت در سایت عمومی؛ دکمهٔ پرداخت = ریدایرکت مرورگر به `GET {API}/api/v1/payment/order/{appointmentUuid}?gateway=...&return=...`. انتخاب درگاه (در صورت چند درگاه) قبل از ریدایرکت انجام شود؛ اگر یک درگاه فعال باشد، خودکار. + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `components/appointment/paying/index.js` | دکمهٔ پرداخت + انتخاب درگاه (از `getPaymentConfig`) | +| `services/response.js` | `getPaymentConfig` (برای فهرست درگاه‌های فعال) — بدون تغییر | +| `app/payment/result/page.js` | بازگشت از Backend با `status` — بدون تغییر | +| `app/payment/[uuid]/page.js` | نمایش نتیجه — بدون تغییر | + +## وضعیت فعلی + +```js +// components/appointment/paying/index.js +const handlePayment = async () => { + if (!appointmentId) return; + if (!testMode && !selectedBank) return; + setLoading(true); + try { + const res = await request.postAppointmentPayment({ // ❌ XHR برای شروع پرداخت + appointment_uuid: appointmentId, + gateway: testMode ? "mellat" : selectedBank, + frontend_address: `${window.location.origin}/payment/result`, + }); + const payUrl = res?.data?.pay_url; + if (payUrl) { window.location.href = payUrl; return; } + throw new Error("pay_url missing"); + } catch (error) { alert("خطا در شروع پرداخت..."); setLoading(false); } +}; +``` + +`getPaymentConfig` از قبل درگاه‌های فعال را در `gateways` می‌دهد و `selectedBank`/`testMode` ست می‌شوند (بدون تغییر می‌مانند). + +## وظایف + +### ۱. تبدیل دکمهٔ پرداخت به ریدایرکتِ خالص (حذف XHR) + +`handlePayment` را طوری بازنویسی کن که به‌جای XHR، مستقیماً مرورگر را به entry بک‌اند ببرد: + +```js +const handlePayment = () => { + if (!appointmentId) return; + const gateway = testMode ? "mellat" : selectedBank; + if (!gateway) return; // باید یک درگاه انتخاب شده باشد + setLoading(true); + + const apiBase = process.env.NEXT_PUBLIC_API_URL; + const ret = encodeURIComponent(`${window.location.origin}/payment/result`); + window.location.href = + `${apiBase}/api/v1/payment/order/${appointmentId}` + + `?gateway=${encodeURIComponent(gateway)}&return=${ret}`; +}; +``` + +- `handlePayment` دیگر `async` نیست و هیچ `request.*` صدا نمی‌زند. +- `NEXT_PUBLIC_API_URL` همان base بک‌اند است (مثل بقیهٔ سایت). +- `appointmentId` همان `appointment_uuid` است (capability؛ در URL می‌رود). + +### ۲. حفظ انتخاب درگاه (مرحلهٔ ۲ نیاز) + +- `getPaymentConfig` و state `gateways`/`selectedBank`/`testMode` بدون تغییر بمانند؛ Select فقط وقتی چند درگاه فعال است نمایش داده شود. +- اگر فقط یک درگاه فعال باشد، `selectedBank` از قبل روی همان ست است (پیش‌فرض `gateways[0]`), پس مرحله خودکار است. +- دکمه وقتی `!testMode && !selectedBank` است `disabled` بماند (از قبل هست). + +### ۳. پاک‌سازی + +- اگر `request.postAppointmentPayment` دیگر جای دیگری استفاده نمی‌شود، آن را از `services/response.js` حذف کن (بررسی با grep). اگر جای دیگری استفاده می‌شود، نگه‌دار. +- `app/payment/result/page.js` و `app/payment/[uuid]/page.js` بدون تغییر (بازگشت با `status` را از قبل مدیریت می‌کنند). + +## نکات مهم + +- **هیچ فراخوانی API برای شروع پرداخت نباید بماند** — فقط ریدایرکت full-page به `{API}/api/v1/payment/order/{uuid}`. Backend همهٔ ارتباط با بانک را انجام می‌دهد. +- **بازگشت به دامنهٔ مبدأ:** `return=${window.location.origin}/payment/result` تضمین می‌کند Backend بعد از پرداخت به همین دامنهٔ چند-شهری برگردد؛ Backend این آدرس را در برابر `payment_allowed_frontend_hosts` اعتبارسنجی می‌کند (host دامنه باید whitelist باشد). +- انتخاب درگاه همچنان از `GET /api/v1/payment/config` (`gateways` فعال) است — درگاه هاردکد نکن. +- App Router؛ `paying` یک client component است (`use client`). فونت/استایل موجود (MUI + Tailwind, RTL). +- بعد از تغییر: `npm run build`. تست دستی: کلیک پرداخت → مرورگر به `{API}/api/v1/payment/order/...` می‌رود (نه XHR)، سپس شاپرک، سپس بازگشت به `/payment/result?status=...`. diff --git a/app/payment/[uuid]/page.js b/app/payment/[uuid]/page.js index 40663db..e6afa52 100644 --- a/app/payment/[uuid]/page.js +++ b/app/payment/[uuid]/page.js @@ -14,24 +14,16 @@ export default function PaymentDetailsPage() { const [error, setError] = useState(null); const [paying, setPaying] = useState(false); - const handleRetryPayment = async () => { + const handleRetryPayment = () => { if (!payment?.appointment_uuid) return; setPaying(true); - try { - const res = await request.postAppointmentPayment({ - appointment_uuid: payment.appointment_uuid, - gateway: payment.gateway || "mellat", - frontend_address: `${window.location.origin}/payment/result`, - }); - const payUrl = res?.data?.pay_url; - if (payUrl) { - window.location.href = payUrl; - } else { - setPaying(false); - } - } catch { - setPaying(false); - } + // بدون XHR: ریدایرکت خالص به entry واحد پرداخت در بک‌اند. + const apiBase = process.env.NEXT_PUBLIC_API_URL; + const gateway = payment.gateway || "mellat"; + const ret = encodeURIComponent(`${window.location.origin}/payment/result`); + window.location.href = + `${apiBase}/api/v1/payment/order/${payment.appointment_uuid}` + + `?gateway=${encodeURIComponent(gateway)}&return=${ret}`; }; useEffect(() => { diff --git a/components/appointment/paying/index.js b/components/appointment/paying/index.js index 6901c30..a2a4e30 100644 --- a/components/appointment/paying/index.js +++ b/components/appointment/paying/index.js @@ -51,29 +51,19 @@ function Paying({ setStep, appointmentId, expiresAt, doctor, data, isForAnother, const expired = timeLeft === 0; const progress = Math.max(0, Math.min(100, (timeLeft / PAYMENT_TTL) * 100)); - const handlePayment = async () => { + const handlePayment = () => { if (!appointmentId) return; - if (!testMode && !selectedBank) return; + const gateway = testMode ? "mellat" : selectedBank; + if (!gateway) return; setLoading(true); - try { - const res = await request.postAppointmentPayment({ - appointment_uuid: appointmentId, - gateway: testMode ? "mellat" : selectedBank, - frontend_address: `${window.location.origin}/payment/result`, - }); - // فقط به بک‌اند می‌رویم؛ بک‌اند صلاحیت را تأیید و به درگاه بانک منتقل می‌کند. - // کلاینت هرگز مستقیم به بانک ریکوست نمی‌زند. - const payUrl = res?.data?.pay_url; - if (payUrl) { - window.location.href = payUrl; - return; - } - throw new Error("pay_url missing"); - } catch (error) { - console.error("Payment error:", error); - alert("خطا در شروع پرداخت. لطفاً دوباره تلاش کنید."); - setLoading(false); - } + + // بدون هیچ XHR: مرورگر مستقیماً به entry بک‌اند می‌رود؛ بک‌اند صلاحیت را تأیید، + // Payment را می‌سازد و به درگاه بانک منتقل می‌کند. + const apiBase = process.env.NEXT_PUBLIC_API_URL; + const ret = encodeURIComponent(`${window.location.origin}/payment/result`); + window.location.href = + `${apiBase}/api/v1/payment/order/${appointmentId}` + + `?gateway=${encodeURIComponent(gateway)}&return=${ret}`; }; return ( diff --git a/services/response.js b/services/response.js index dbe949a..1e916e6 100644 --- a/services/response.js +++ b/services/response.js @@ -60,8 +60,6 @@ export const request = { }), postAppointment: (data) => api.post(`api/v1/appointment`, data, { requireAuth: true }), getMyAppointments: (params) => api.get(`api/v1/appointments/user`, { params, requireAuth: true }), - postAppointmentPayment: (data) => - api.post(`api/v1/payment/appointment`, data, { requireAuth: true }), getPaymentConfig: () => api.get(`api/v1/payment/config`, { requireAuth: true }), getPayment: (uuid) => api.get(`api/v1/payment/${uuid}`, { requireAuth: true }), getMyPayments: (params) => api.get(`api/v1/my/payments`, { params, requireAuth: true }),