feat: refactor payment handling to use direct backend redirect instead of API call
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# entry پرداخت بهصورت ریدایرکت خالص به Backend (بدون XHR) — سایت عمومی
|
||||
|
||||
## پروژه
|
||||
|
||||
`nobat724_front` (سایت عمومی). **cross-repo** — پرامپت همتا (Backend، اول اجرا شود): `clinicpro/.claude/prompt/payment-single-flow-consolidation.md`.
|
||||
|
||||
## زمینه
|
||||
|
||||
طبق معماری واحد پرداخت، سایت عمومی نباید هیچ API برای **شروع** پرداخت صدا بزند؛ فقط مرورگر را به Backend ریدایرکت کند. Backend یک entry ریدایرکتِ خالص دارد:
|
||||
|
||||
```
|
||||
GET {API}/api/v1/payment/order/{appointmentUuid}?gateway=<name>&return=<frontend_return_url>
|
||||
```
|
||||
|
||||
Backend این آدرس را میگیرد، سفارش را اعتبارسنجی میکند، `Payment` میسازد، به بانک وصل میشود و کاربر را به شاپرک میفرستد؛ در پایان به `return` (همان دامنهٔ مبدأ) با `?status=...` برمیگردد.
|
||||
|
||||
وضعیت فعلی سایت: `components/appointment/paying/index.js` هنوز با **XHR** (`request.postAppointmentPayment`) پرداخت را شروع میکند و بعد به `pay_url` میرود. طبق نیاز جدید باید این XHR حذف شود و دکمهٔ پرداخت مستقیماً به `payment/order/{uuid}` ریدایرکت کند.
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
حذف کامل فراخوانی API برای شروع پرداخت در سایت عمومی؛ دکمهٔ پرداخت = ریدایرکت مرورگر به `GET {API}/api/v1/payment/order/{appointmentUuid}?gateway=...&return=...`. انتخاب درگاه (در صورت چند درگاه) قبل از ریدایرکت انجام شود؛ اگر یک درگاه فعال باشد، خودکار.
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `components/appointment/paying/index.js` | دکمهٔ پرداخت + انتخاب درگاه (از `getPaymentConfig`) |
|
||||
| `services/response.js` | `getPaymentConfig` (برای فهرست درگاههای فعال) — بدون تغییر |
|
||||
| `app/payment/result/page.js` | بازگشت از Backend با `status` — بدون تغییر |
|
||||
| `app/payment/[uuid]/page.js` | نمایش نتیجه — بدون تغییر |
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
```js
|
||||
// components/appointment/paying/index.js
|
||||
const handlePayment = async () => {
|
||||
if (!appointmentId) return;
|
||||
if (!testMode && !selectedBank) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await request.postAppointmentPayment({ // ❌ XHR برای شروع پرداخت
|
||||
appointment_uuid: appointmentId,
|
||||
gateway: testMode ? "mellat" : selectedBank,
|
||||
frontend_address: `${window.location.origin}/payment/result`,
|
||||
});
|
||||
const payUrl = res?.data?.pay_url;
|
||||
if (payUrl) { window.location.href = payUrl; return; }
|
||||
throw new Error("pay_url missing");
|
||||
} catch (error) { alert("خطا در شروع پرداخت..."); setLoading(false); }
|
||||
};
|
||||
```
|
||||
|
||||
`getPaymentConfig` از قبل درگاههای فعال را در `gateways` میدهد و `selectedBank`/`testMode` ست میشوند (بدون تغییر میمانند).
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. تبدیل دکمهٔ پرداخت به ریدایرکتِ خالص (حذف XHR)
|
||||
|
||||
`handlePayment` را طوری بازنویسی کن که بهجای XHR، مستقیماً مرورگر را به entry بکاند ببرد:
|
||||
|
||||
```js
|
||||
const handlePayment = () => {
|
||||
if (!appointmentId) return;
|
||||
const gateway = testMode ? "mellat" : selectedBank;
|
||||
if (!gateway) return; // باید یک درگاه انتخاب شده باشد
|
||||
setLoading(true);
|
||||
|
||||
const apiBase = process.env.NEXT_PUBLIC_API_URL;
|
||||
const ret = encodeURIComponent(`${window.location.origin}/payment/result`);
|
||||
window.location.href =
|
||||
`${apiBase}/api/v1/payment/order/${appointmentId}` +
|
||||
`?gateway=${encodeURIComponent(gateway)}&return=${ret}`;
|
||||
};
|
||||
```
|
||||
|
||||
- `handlePayment` دیگر `async` نیست و هیچ `request.*` صدا نمیزند.
|
||||
- `NEXT_PUBLIC_API_URL` همان base بکاند است (مثل بقیهٔ سایت).
|
||||
- `appointmentId` همان `appointment_uuid` است (capability؛ در URL میرود).
|
||||
|
||||
### ۲. حفظ انتخاب درگاه (مرحلهٔ ۲ نیاز)
|
||||
|
||||
- `getPaymentConfig` و state `gateways`/`selectedBank`/`testMode` بدون تغییر بمانند؛ Select فقط وقتی چند درگاه فعال است نمایش داده شود.
|
||||
- اگر فقط یک درگاه فعال باشد، `selectedBank` از قبل روی همان ست است (پیشفرض `gateways[0]`), پس مرحله خودکار است.
|
||||
- دکمه وقتی `!testMode && !selectedBank` است `disabled` بماند (از قبل هست).
|
||||
|
||||
### ۳. پاکسازی
|
||||
|
||||
- اگر `request.postAppointmentPayment` دیگر جای دیگری استفاده نمیشود، آن را از `services/response.js` حذف کن (بررسی با grep). اگر جای دیگری استفاده میشود، نگهدار.
|
||||
- `app/payment/result/page.js` و `app/payment/[uuid]/page.js` بدون تغییر (بازگشت با `status` را از قبل مدیریت میکنند).
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **هیچ فراخوانی API برای شروع پرداخت نباید بماند** — فقط ریدایرکت full-page به `{API}/api/v1/payment/order/{uuid}`. Backend همهٔ ارتباط با بانک را انجام میدهد.
|
||||
- **بازگشت به دامنهٔ مبدأ:** `return=${window.location.origin}/payment/result` تضمین میکند Backend بعد از پرداخت به همین دامنهٔ چند-شهری برگردد؛ Backend این آدرس را در برابر `payment_allowed_frontend_hosts` اعتبارسنجی میکند (host دامنه باید whitelist باشد).
|
||||
- انتخاب درگاه همچنان از `GET /api/v1/payment/config` (`gateways` فعال) است — درگاه هاردکد نکن.
|
||||
- App Router؛ `paying` یک client component است (`use client`). فونت/استایل موجود (MUI + Tailwind, RTL).
|
||||
- بعد از تغییر: `npm run build`. تست دستی: کلیک پرداخت → مرورگر به `{API}/api/v1/payment/order/...` میرود (نه XHR)، سپس شاپرک، سپس بازگشت به `/payment/result?status=...`.
|
||||
@@ -14,24 +14,16 @@ export default function PaymentDetailsPage() {
|
||||
const [error, setError] = useState(null);
|
||||
const [paying, setPaying] = useState(false);
|
||||
|
||||
const handleRetryPayment = async () => {
|
||||
const handleRetryPayment = () => {
|
||||
if (!payment?.appointment_uuid) return;
|
||||
setPaying(true);
|
||||
try {
|
||||
const res = await request.postAppointmentPayment({
|
||||
appointment_uuid: payment.appointment_uuid,
|
||||
gateway: payment.gateway || "mellat",
|
||||
frontend_address: `${window.location.origin}/payment/result`,
|
||||
});
|
||||
const payUrl = res?.data?.pay_url;
|
||||
if (payUrl) {
|
||||
window.location.href = payUrl;
|
||||
} else {
|
||||
setPaying(false);
|
||||
}
|
||||
} catch {
|
||||
setPaying(false);
|
||||
}
|
||||
// بدون XHR: ریدایرکت خالص به entry واحد پرداخت در بکاند.
|
||||
const apiBase = process.env.NEXT_PUBLIC_API_URL;
|
||||
const gateway = payment.gateway || "mellat";
|
||||
const ret = encodeURIComponent(`${window.location.origin}/payment/result`);
|
||||
window.location.href =
|
||||
`${apiBase}/api/v1/payment/order/${payment.appointment_uuid}` +
|
||||
`?gateway=${encodeURIComponent(gateway)}&return=${ret}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -51,29 +51,19 @@ function Paying({ setStep, appointmentId, expiresAt, doctor, data, isForAnother,
|
||||
const expired = timeLeft === 0;
|
||||
const progress = Math.max(0, Math.min(100, (timeLeft / PAYMENT_TTL) * 100));
|
||||
|
||||
const handlePayment = async () => {
|
||||
const handlePayment = () => {
|
||||
if (!appointmentId) return;
|
||||
if (!testMode && !selectedBank) return;
|
||||
const gateway = testMode ? "mellat" : selectedBank;
|
||||
if (!gateway) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
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;
|
||||
if (payUrl) {
|
||||
window.location.href = payUrl;
|
||||
return;
|
||||
}
|
||||
throw new Error("pay_url missing");
|
||||
} catch (error) {
|
||||
console.error("Payment error:", error);
|
||||
alert("خطا در شروع پرداخت. لطفاً دوباره تلاش کنید.");
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
// بدون هیچ XHR: مرورگر مستقیماً به entry بکاند میرود؛ بکاند صلاحیت را تأیید،
|
||||
// Payment را میسازد و به درگاه بانک منتقل میکند.
|
||||
const apiBase = process.env.NEXT_PUBLIC_API_URL;
|
||||
const ret = encodeURIComponent(`${window.location.origin}/payment/result`);
|
||||
window.location.href =
|
||||
`${apiBase}/api/v1/payment/order/${appointmentId}` +
|
||||
`?gateway=${encodeURIComponent(gateway)}&return=${ret}`;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -60,8 +60,6 @@ export const request = {
|
||||
}),
|
||||
postAppointment: (data) => api.post(`api/v1/appointment`, data, { requireAuth: true }),
|
||||
getMyAppointments: (params) => api.get(`api/v1/appointments/user`, { params, requireAuth: true }),
|
||||
postAppointmentPayment: (data) =>
|
||||
api.post(`api/v1/payment/appointment`, data, { requireAuth: true }),
|
||||
getPaymentConfig: () => api.get(`api/v1/payment/config`, { requireAuth: true }),
|
||||
getPayment: (uuid) => api.get(`api/v1/payment/${uuid}`, { requireAuth: true }),
|
||||
getMyPayments: (params) => api.get(`api/v1/my/payments`, { params, requireAuth: true }),
|
||||
|
||||
Reference in New Issue
Block a user