- 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>
12 KiB
رفع جریان پرداخت و اطلاعات حساب در نوبتگیری آنلاین
پروژه
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: خواندن نتیجهی نوبت
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).