diff --git a/.claude/prompt/payment-flow-frontend.md b/.claude/prompt/payment-flow-frontend.md new file mode 100644 index 0000000..8fbc7e9 --- /dev/null +++ b/.claude/prompt/payment-flow-frontend.md @@ -0,0 +1,101 @@ +# جریان پرداخت سمت کلاینت — انتقال از طریق بک‌اند + صفحات نتیجه (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 باید این موارد دقیق و کامل باشند: + +1. **کلاینت هرگز مستقیم به بانک نرود** — فقط به `pay_url` (بک‌اند). (عمدتاً انجام شده؛ باید تأیید و تثبیت شود.) +2. **صفحات نتیجهٔ استاندارد** — `success` / `failed` با نمایش وضعیت پرداخت و لینک‌های اقدام، به‌جای هدایت خام. +3. **سازگاری با تغییر قرارداد Backend** — اگر Backend پاسخ `GET /api/v1/payment/{uuid}` را از double-nested به flat تغییر داد (وظیفهٔ backend)، خواندن در `app/payment/[uuid]/page.js` باید هماهنگ شود. +4. **حالت انصراف/شکست** — وقتی `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` | + +## وضعیت فعلی + +```js +// 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 به بک‌اند ✅ +``` + +```js +// 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` نبود (پاسخ ناقص)، به‌جای رفتن به مرحلهٔ بعد، خطای کاربرپسند نشان بده: + +```js +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` را هم بخواند و بر اساس آن رفتار کند: + +```js +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 کرد، خواندن را طوری بنویس که هر دو حالت کار کند: + +```js +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`. diff --git a/app/payment/[uuid]/page.js b/app/payment/[uuid]/page.js index b44e736..40663db 100644 --- a/app/payment/[uuid]/page.js +++ b/app/payment/[uuid]/page.js @@ -23,9 +23,9 @@ export default function PaymentDetailsPage() { gateway: payment.gateway || "mellat", frontend_address: `${window.location.origin}/payment/result`, }); - const redirectUrl = res?.data?.redirect_url; - if (redirectUrl) { - window.location.href = redirectUrl; + const payUrl = res?.data?.pay_url; + if (payUrl) { + window.location.href = payUrl; } else { setPaying(false); } @@ -46,7 +46,8 @@ export default function PaymentDetailsPage() { try { setLoading(true); const response = await request.getPayment(params.uuid); - setPayment(response?.data?.data ?? null); + // سازگار با پاسخ flat (جدید) و double-nested (قدیمی) + setPayment(response?.data?.data ?? response?.data ?? null); } catch (err) { setError("خطا در دریافت اطلاعات پرداخت"); console.error("Payment fetch error:", err); @@ -65,6 +66,7 @@ export default function PaymentDetailsPage() { pending: "در انتظار پرداخت", success: "پرداخت موفق", failed: "پرداخت ناموفق", + canceled: "پرداخت لغو شد", refunded: "مسترد شده", }; return statusMap[status] || status; @@ -75,6 +77,7 @@ export default function PaymentDetailsPage() { pending: "text-yellow-600 bg-yellow-50", success: "text-green-600 bg-green-50", failed: "text-red-600 bg-red-50", + canceled: "text-red-600 bg-red-50", refunded: "text-gray-600 bg-gray-50", }; return colorMap[status] || "text-gray-600 bg-gray-50"; @@ -207,22 +210,19 @@ export default function PaymentDetailsPage() { {/* دکمه‌ها */}
- {payment.status === "pending" ? ( - <> - - - + {["pending", "failed", "canceled"].includes(payment.status) && + payment.appointment_uuid ? ( + ) : null}