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

12 KiB
Raw Blame History

رفع جریان پرداخت و اطلاعات حساب در نوبت‌گیری آنلاین

پروژه

nobat724_front (سایت عمومی).

این مشکل کاملاً frontend است. بک‌اند clinicpro همه‌ی endpointهای لازم را دارد و درست کار می‌کند: POST /api/v1/appointment (با expires_atGET /api/v1/payment/config (test_modePOST /api/v1/payment/appointment (redirect_url — در حالت تست از MockGatewayPOST|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_urlfrontend_address
app/payment/[uuid]/page.js صفحه‌ی نتیجه‌ی پرداخت — مصرف getPayment با شکل اشتباه + statusهای اشتباه
services/response.js getPayment، postAppointmentPayment، getPaymentConfig (urlها درست‌اند)
components/appointment/index.js buildProfileData — اطلاعات حساب (وظیفه ۴، عمدتاً انجام‌شده)

وضعیت فعلی (کد واقعی)

باگ ۱ — SubmitData: خواندن نتیجه‌ی نوبت

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

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

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:

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):
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

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).