Files
nobat724_front/.claude/prompt/fix-appointment-payment-flow.md
T
hamedandClaude Opus 4.8 59d29cd7c7 fix(appointment): correct payment flow and account-info prefill
- Booking response is double-nested: read appointment uuid/expires_at from
  res.data.data so the payment countdown and gateway redirect actually fire.
- Payment result page (/payment/[uuid]): unwrap res.data.data, use real
  backend fields (amount_rials, gateway, created_at, type) and statuses
  (pending/success/failed/canceled/refunded); the "pay" button now re-initiates
  via postAppointmentPayment instead of building a URL on the API origin.
- Add /payment/result interstitial that reads payment_uuid from the gateway
  callback and forwards to /payment/[uuid].
- Prefill account info correctly in the booking form: name from profile.label,
  insurances from *_id, gender as string, national_code editable unless approved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 10:06:56 +03:30

128 lines
12 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` همه‌ی endpointهای لازم را دارد و درست کار می‌کند: `POST /api/v1/appointment` (با `expires_at`)، `GET /api/v1/payment/config` (`test_mode`)، `POST /api/v1/payment/appointment` (`redirect_url` — در حالت تست از `MockGateway`)، `POST|GET /api/v1/payment/callback/{gateway}` (که نوبت را confirm و به `frontend_address` ریدایرکت می‌کند)، و `GET /api/v1/payment/{uuid}` (وضعیت پرداخت). نیازی به تغییر بک‌اند نیست. مرجع قرارداد: `clinicpro/docs/api/payment.md` و `docs/api/appointment.md`.
## زمینه
در فرایند نوبت‌گیری آنلاین (`/appointment/[doctorId]`) سه مشکل گزارش شده: (۱) اطلاعات حساب کاربری به‌درستی در فیلدها ست نمی‌شود، (۲) شمارنده‌ی زمان پرداخت کار نمی‌کند، (۳) درگاه پرداخت تست کار نمی‌کند. ریشه‌ی هر سه، **اشتباه در خواندن شکل پاسخ بک‌اند** در سمت frontend است — مخصوصاً پاسخ‌های double-nested بک‌اند.
یادآوری معماری پاسخ بک‌اند: `BaseController::success(['data' => X])` تولید می‌کند `{ success, data: { data: X } }`. interceptor در `services/api.js` یک بار `response.data` را برمی‌گرداند، پس مقدار واقعی در **`res.data.data`** است (نه `res.data`).
## مشکل / هدف
۱. **شمارنده + appointmentId**: پس از `POST /appointment`، فرانت `res.data.uuid` و `res.data.expires_at` را می‌خواند، اما چون پاسخ double-nested است این‌ها `undefined` می‌شوند → `appointmentId` خالی (پرداخت کار نمی‌کند) و `expiresAt` خالی (شمارنده روی مقدار ثابت `PAYMENT_TTL` می‌ماند و واقعی نیست).
۲. **درگاه تست**: درگاه تست خودِ بک‌اند است (`MockGateway`؛ وقتی `payment_test_mode=1`). فرانت باید: `redirect_url` بازگشتی از `POST /payment/appointment` را باز کند (که به callback بک‌اند می‌رود، نوبت را confirm می‌کند و به `frontend_address` با `?payment_uuid=...&status=...` برمی‌گردد). مشکل: `frontend_address` فعلی به `/payment/result` اشاره دارد که **route وجود ندارد**؛ صفحه‌ی واقعی `/payment/[uuid]` است و callback `payment_uuid` را برمی‌گرداند.
۳. **صفحه‌ی نتیجه‌ی پرداخت** (`/payment/[uuid]`): پاسخ `GET /payment/{uuid}` را به‌اشتباه می‌خواند (`setPayment(response)` به‌جای `response.data.data`) و statusها اشتباه‌اند (`completed`/`canceled` به‌جای `success`/`failed`).
۴. **اطلاعات حساب کاربری**: در `buildProfileData` کلیدها اصلاح شد (`label`→نام، `basic_insurance_id`، `gender` رشته، `national_code_approved`)؛ این وظیفه تثبیت/تأیید آن است.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `components/appointment/detail/SubmitData.js` | پس از `postAppointment`، `appointmentId`/`expiresAt` را ست می‌کند — **double-nest باگ** |
| `components/appointment/paying/index.js` | شمارنده (`expiresAt` prop)، `getPaymentConfig`، `postAppointmentPayment` (`redirect_url``frontend_address` |
| `app/payment/[uuid]/page.js` | صفحه‌ی نتیجه‌ی پرداخت — مصرف `getPayment` با شکل اشتباه + statusهای اشتباه |
| `services/response.js` | `getPayment`، `postAppointmentPayment`، `getPaymentConfig` (urlها درست‌اند) |
| `components/appointment/index.js` | `buildProfileData` — اطلاعات حساب (وظیفه ۴، عمدتاً انجام‌شده) |
## وضعیت فعلی (کد واقعی)
### باگ ۱ — SubmitData: خواندن نتیجه‌ی نوبت
```js
const res = await request.postAppointment(appointmentPayload);
const appointmentUuid = res?.data?.uuid; // ❌ undefined — باید res.data.data.uuid
if (appointmentUuid) {
setAppointmentId(appointmentUuid);
setAppointmentExpiresAt?.(res?.data?.expires_at ?? null); // ❌ undefined
}
```
بک‌اند `POST /appointment` پاسخ می‌دهد: `{ success, data: { data: { uuid, slot_start, slot_end, expires_at, status, ... } } }`.
### باگ ۲ — paying: frontend_address و config
```js
const res = await request.postAppointmentPayment({
appointment_uuid: appointmentId,
gateway: testMode ? "mellat" : selectedBank,
frontend_address: `${window.location.origin}/payment/result`, // ❌ route وجود ندارد
});
const redirectUrl = res?.data?.redirect_url; // ❌ باید res.data.data.redirect_url
```
- `getPaymentConfig` نیز: `res?.data?.test_mode` خوانده می‌شود ولی پاسخ `{ success, data: { test_mode } }` است (config با `success([...])` بدون nest اضافه ساخته شده؛ یعنی `res.data.test_mode` درست است — **این یکی را تأیید کن**، چون `config()` از `$this->success([...])` مستقیم استفاده می‌کند نه `['data'=>...]`).
- callback بک‌اند به `frontend_address` با `?payment_uuid=<uuid>&status=<status>` برمی‌گردد.
### باگ ۳ — payment/[uuid]/page.js
```js
const response = await request.getPayment(params.uuid);
setPayment(response); // ❌ باید response.data.data
// ...
const statusMap = { pending:..., completed:..., failed:..., canceled:... }; // ❌ بک‌اند: pending|success|failed|refunded
```
بک‌اند `GET /payment/{uuid}``{ success, data: { data: { uuid, order_id, amount_rials, status, gateway, type, reference_id, appointment_uuid, created_at } } }`. statusهای واقعی: `pending`, `success`, `failed`, `refunded`. مبلغ در `amount_rials` (ریال) است.
## وظایف
### ۱. رفع double-nest در SubmitData (شمارنده + appointmentId)
در `components/appointment/detail/SubmitData.js`:
```js
const res = await request.postAppointment(appointmentPayload);
const appt = res?.data?.data; // unwrap درست
const appointmentUuid = appt?.uuid;
if (appointmentUuid) {
setAppointmentId(appointmentUuid);
setAppointmentExpiresAt?.(appt?.expires_at ?? null);
}
```
> نکته: `expires_at` بک‌اند Unix ثانیه است؛ `paying/index.js` همان را در `computeLeft` با `expiresAt * 1000` به میلی‌ثانیه تبدیل می‌کند — درست است؛ فقط باید مقدار واقعی برسد.
### ۲. اصلاح frontend_address و redirect_url در paying
در `components/appointment/paying/index.js`، تابع `handlePayment`:
- `frontend_address` را به مسیر معتبری بده که نتیجه را نشان می‌دهد. چون callback `?payment_uuid=...&status=...` را به همان آدرس append می‌کند و صفحه‌ی نتیجه `/payment/[uuid]` است، یک **route نتیجه** لازم است که `payment_uuid` را از query بخواند و کاربر را به نتیجه ببرد. دو گزینه:
- **الف (ساده):** `frontend_address` را روی `${window.location.origin}/payment/result` بگذار و یک صفحه‌ی سبک `app/payment/result/page.js` بساز که `payment_uuid` و `status` را از `searchParams` می‌خواند و به `/payment/${payment_uuid}` ریدایرکت (یا مستقیم وضعیت را نشان) می‌دهد. (پیشنهادی)
- **ب:** اگر صفحه‌ی `[uuid]` را نتیجه‌ی نهایی می‌گیری، می‌توان مستقیم آن را هدف frontend_address نکرد چون uuid را callback append نمی‌کند (فقط `payment_uuid`)؛ پس گزینه الف تمیزتر است.
- `redirect_url` را از `res?.data?.data?.redirect_url` بخوان (double-nest):
```js
const res = await request.postAppointmentPayment({
appointment_uuid: appointmentId,
gateway: testMode ? "mellat" : selectedBank,
frontend_address: `${window.location.origin}/payment/result`,
});
const redirectUrl = res?.data?.data?.redirect_url;
if (redirectUrl) window.location.href = redirectUrl;
```
> در حالت تست، بک‌اند `MockGateway` یک `redirect_url` می‌دهد که مستقیم به callback بک‌اند می‌رود (`/api/v1/payment/callback/mock?...&mock=1&ResCode=0`) و خودِ بک‌اند نوبت را confirm و به `frontend_address` برمی‌گرداند. پس «درگاه تست» کاملاً سمت بک‌اند است و response واقعی می‌دهد — فرانت فقط `redirect_url` را باز می‌کند. **هیچ شبیه‌سازی پرداخت سمت فرانت اضافه نکن.**
### ۳. تأیید getPaymentConfig
`config()` بک‌اند از `$this->success(['test_mode' => ...])` استفاده می‌کند → پاسخ `{ success, data: { test_mode } }`. پس `res?.data?.test_mode` در `paying/index.js` **درست** است. فقط تأیید کن این مقدار را درست می‌خواند و دکمه بر اساسش «پرداخت آزمایشی»/درگاه را نشان می‌دهد.
### ۴. اصلاح صفحه‌ی نتیجه `app/payment/[uuid]/page.js`
```js
const response = await request.getPayment(params.uuid);
setPayment(response?.data?.data ?? null); // unwrap درست
```
- statusها را با مقادیر واقعی بک‌اند هماهنگ کن: `pending` (در انتظار)، `success` (پرداخت موفق)، `failed` (ناموفق)، `refunded` (مسترد). برچسب/رنگ هر کدام.
- مبلغ از `payment.amount_rials` است (ریال) — برای نمایش تومان `/10`.
- روش پرداخت از `payment.gateway` (`mellat`/`sep`/`mock`).
- تاریخ از `payment.created_at` (Unix ثانیه).
### ۵. صفحه‌ی واسط نتیجه (اگر گزینه الف انتخاب شد)
`app/payment/result/page.js` بساز:
- `payment_uuid` و `status` را از `useSearchParams` بخوان.
- اگر `payment_uuid` بود → `router.replace('/payment/' + payment_uuid)`؛ در غیر این صورت پیام خطا.
- این صفحه فقط یک واسط ریدایرکت سبک است (با لودینگ).
### ۶. تثبیت اطلاعات حساب کاربری (انجام‌شده — تأیید)
در `components/appointment/index.js` تابع `buildProfileData` قبلاً اصلاح شده: `name` از `profile.label`، `basic_insurance`/`supplementary_insurance` از `*_id` به‌صورت `{id}`، `gender` رشته‌ی `profile.gender`، و `national_code.isEdit = !profile.national_code_approved`. تأیید کن این فیلدها در فرم (`detail/Form.js`) درست نمایش داده می‌شوند و در `SubmitData` درست ارسال می‌شوند (که سازگار است). اگر چیزی جا مانده اصلاح کن.
## نکات مهم
- **بک‌اند تغییر نمی‌کند.** همه‌ی endpointها موجود و درست‌اند؛ فقط مصرف frontend اصلاح می‌شود.
- پاسخ‌های `appointment` و `payment/{uuid}` و `payment/appointment` همگی **double-nested** اند (`res.data.data`)؛ ولی `payment/config` نیست (`res.data.test_mode`). به این تفاوت دقت کن.
- شمارنده باید از `expires_at` واقعیِ بک‌اند (Unix ثانیه) تغذیه شود؛ منطق فعلی `computeLeft` درست است، فقط ورودی‌اش باید برسد.
- پرداخت تست = درگاه تست بک‌اند (`MockGateway`)؛ فرانت فقط `redirect_url` را باز می‌کند و نتیجه را از callback/صفحه‌ی نتیجه می‌گیرد. شبیه‌سازی فرانت ممنوع.
- App Router؛ صفحه‌ی نتیجه `"use client"` (به `useSearchParams`/`useParams` نیاز دارد). RTL، فونت Vazir، MUI v5.
- بعد از تغییر: `npm run build` بدون خطا؛ جریان را end-to-end تست کن (نوبت → شمارنده فعال → پرداخت آزمایشی → callback → صفحه‌ی نتیجه با وضعیت `success`).