6.6 KiB
جریان پرداخت سمت کلاینت — انتقال از طریق بکاند + صفحات نتیجه (Frontend)
پروژه
nobat724_front (سایت عمومی). cross-repo — پرامپت همتا (Backend، اول اجرا شود): clinicpro/.claude/prompt/payment-flow-architecture.md. قرارداد API توسط این کلاینت مصرف میشود.
زمینه
طبق معمار: هیچ Frontend نباید مستقیم با بانک صحبت کند؛ Backend مرجع است و انتقال به بانک و بازگشت از طریق Backend انجام میشود. بخش زیادی قبلاً پیاده شده:
components/appointment/paying/index.js: درگاهها ازGET /api/v1/payment/config(data.gatewaysفعال) خوانده میشوند؛ کلیک پرداخت →POST /api/v1/payment/appointment(authenticated XHR) → دریافتpay_url→window.location.href = pay_url(انتقال full-page به بکاند؛ بکاند خودش به بانک میرود).frontend_address = ${window.location.origin}/payment/result→ بکاند بعد از callback به همین دامنهٔ مبدأ برمیگردد با?payment_uuid=..&status=...app/payment/result/page.jsوضعیت را میخواند و به/payment/${uuid}هدایت میکند.
مشکل / هدف
طبق spec باید این موارد دقیق و کامل باشند:
- کلاینت هرگز مستقیم به بانک نرود — فقط به
pay_url(بکاند). (عمدتاً انجام شده؛ باید تأیید و تثبیت شود.) - صفحات نتیجهٔ استاندارد —
success/failedبا نمایش وضعیت پرداخت و لینکهای اقدام، بهجای هدایت خام. - سازگاری با تغییر قرارداد Backend — اگر Backend پاسخ
GET /api/v1/payment/{uuid}را از double-nested به flat تغییر داد (وظیفهٔ backend)، خواندن درapp/payment/[uuid]/page.jsباید هماهنگ شود. - حالت انصراف/شکست — وقتی
statusبرابرcanceled/failedاست، پیام مناسب و امکان تلاش مجدد.
فایلهای مرتبط
| فایل | نقش |
|---|---|
components/appointment/paying/index.js |
دکمهٔ پرداخت + انتخاب درگاه + هدایت به pay_url |
app/payment/result/page.js |
صفحهٔ بازگشت از بکاند (?payment_uuid&status) |
app/payment/[uuid]/page.js |
نمایش نتیجهٔ پرداخت بر اساس getPayment |
services/response.js |
postAppointmentPayment, getPaymentConfig, getPayment |
وضعیت فعلی
// components/appointment/paying/index.js (کلیک پرداخت)
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 || res?.data?.redirect_url;
if (payUrl) window.location.href = payUrl; // انتقال full-page به بکاند ✅
// app/payment/result/page.js (بازگشت از بکاند)
const paymentUuid = searchParams.get("payment_uuid");
if (paymentUuid) router.replace(`/payment/${paymentUuid}`);
else router.replace("/dashboard?sidebar=2");
وظایف
۱. تثبیت انتقال از طریق بکاند (بدون تماس مستقیم با بانک)
مطمئن شو در handlePayment هیچ URL بانکی مستقیم باز نمیشود؛ فقط pay_url. اگر pay_url نبود (پاسخ ناقص)، بهجای رفتن به مرحلهٔ بعد، خطای کاربرپسند نشان بده:
const payUrl = res?.data?.pay_url;
if (payUrl) { window.location.href = payUrl; return; }
toast/alert("خطا در شروع پرداخت. دوباره تلاش کنید.");
setLoading(false);
(اتکا به redirect_url بانک را حذف کن؛ منبع درست فقط pay_url است.)
۲. صفحهٔ نتیجه بر اساس status
app/payment/result/page.js علاوه بر payment_uuid، پارامتر status را هم بخواند و بر اساس آن رفتار کند:
const status = searchParams.get("status"); // success | failed | canceled | pending
const paymentUuid = searchParams.get("payment_uuid");
if (status === "success" && paymentUuid) router.replace(`/payment/${paymentUuid}`);
else router.replace(`/payment/${paymentUuid ?? ""}?status=${status ?? "failed"}`);
در app/payment/[uuid]/page.js وضعیت را از getPayment(uuid) بگیر (source of truth بکاند، نه فقط query) و کارت نتیجه را نشان بده: موفق (سبز، جزئیات نوبت/کدرهگیری)، ناموفق/لغو (قرمز، دکمهٔ «تلاش مجدد» → بازگشت به صفحهٔ رزرو پزشک).
۳. سازگاری با شکل پاسخ getPayment
اگر Backend (وظیفهٔ همتا) پاسخ GET /api/v1/payment/{uuid} را flat کرد، خواندن را طوری بنویس که هر دو حالت کار کند:
const data = await request.getPayment(uuid);
const payment = data?.data?.data ?? data?.data; // سازگار با flat و nested
۴. UX انصراف/شکست
اگر status !== success: پیام «پرداخت انجام نشد یا لغو شد» + وضعیت واقعی از payment.status + دکمهٔ تلاش مجدد. هیچ اطلاعات حساسی نمایش نده.
نکات مهم
- قرارداد API را از Backend بگیر:
POST /api/v1/payment/appointment→{ payment_uuid, pay_url, order_id }؛ بازگشت callback →frontend_address?payment_uuid&status. (منبع:clinicpro/docs/api/payment.md.) - چرا یک XHR لازم است: ساخت
Paymentنیازمند احراز مالکیت سفارش است و JWT در کوکیِ همین دامنه است؛ redirect full-page کوکی cross-domain نمیبرد. پس الگوی «XHR authenticated برای ساخت + سپس redirect full-page بهpay_url» درست و امن است — این را حفظ کن (نه fetch مستقیم به بانک). - App Router؛ صفحات نتیجه
generateMetadataباnoindexداشته باشند (صفحهٔ تراکنش نباید ایندکس شود). - استایل MUI v5 + Tailwind، RTL، فونت Vazir، Jalali؛ کلاسهای موجود.
- API client فقط از طریق
services/response.js→request.*؛getPaymentبا{ requireAuth: true }. - بعد از تغییر:
npm run buildدرnobat724_front.