From 8c0b3d828273c3a7521bc9084814257386613f37 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Mon, 15 Jun 2026 11:35:23 +0330 Subject: [PATCH] feat(payment): add endpoint for payment configuration and update UI for test mode --- .claude/prompt/payment-test-mode-ui.md | 177 +++++++++++++++++++ assets/admin/pages/SmsWalletPage.tsx | 92 ++++++---- assets/admin/pages/SubscriptionPage.tsx | 81 ++++++--- docs/api/payment.md | 24 +++ src/Payment/Controller/PaymentController.php | 9 + 5 files changed, 322 insertions(+), 61 deletions(-) create mode 100644 .claude/prompt/payment-test-mode-ui.md diff --git a/.claude/prompt/payment-test-mode-ui.md b/.claude/prompt/payment-test-mode-ui.md new file mode 100644 index 00000000..3102e47e --- /dev/null +++ b/.claude/prompt/payment-test-mode-ui.md @@ -0,0 +1,177 @@ +# نمایش درگاه تست در رابط کاربری هنگام فعال بودن حالت آزمایشی + +## زمینه + +سیستم پرداخت یک حالت آزمایشی (`payment_test_mode`) دارد که در `SiteConfig` ذخیره می‌شود. وقتی این حالت فعال است، backend در `resolveGateway()` (هر دو `PaymentController` و `SmsWalletController`) بجای درگاه واقعی، درگاه `MockGateway` را به‌کار می‌برد و پول واقعی کسر نمی‌شود. + +**مشکل:** Frontend هیچ اطلاعی از حالت تست ندارد. کاربر در `SmsWalletPage` و `SubscriptionPage` انتخاب درگاه می‌بیند (ملت / سپ)، حتی وقتی backend همه را به Mock هدایت می‌کند. این گیج‌کننده است. + +**هدف:** وقتی `payment_test_mode = '1'` است: +- انتخاب‌کننده درگاه واقعی پنهان شود +- بجای آن یک نوار زرد/نارنجی نمایش داده شود: «درگاه آزمایشی فعال است — پول واقعی کسر نخواهد شد» + +--- + +## مشکل / هدف + +دو صفحه frontend نیاز به تغییر دارند: +1. `SmsWalletPage.tsx` — modal شارژ کیف پول پیامک +2. `SubscriptionPage.tsx` — modal خرید اشتراک + +همچنین یک endpoint عمومی (برای همه کاربران احراز هویت شده) باید ساخته شود چون `GET /api/v1/admin/settings` فقط برای `ROLE_ADMIN` است. + +--- + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `src/Config/Controller/SiteConfigController.php` | endpoint تنظیمات — فعلاً فقط ROLE_ADMIN | +| `src/Config/Repository/SiteConfigRepository.php` | خواندن `payment_test_mode` | +| `src/Payment/Controller/PaymentController.php` | `resolveGateway()` — backend قبلاً درست است | +| `assets/admin/pages/SmsWalletPage.tsx` | modal شارژ — نمایش‌دهنده انتخاب‌کننده درگاه | +| `assets/admin/pages/SubscriptionPage.tsx` | modal اشتراک — نمایش‌دهنده انتخاب‌کننده درگاه | + +--- + +## وضعیت فعلی + +### Backend — endpoint تنظیمات فقط ادمین است +```php +// src/Config/Controller/SiteConfigController.php +#[IsGranted('ROLE_ADMIN')] +class SiteConfigController extends BaseController +{ + #[Route('/api/v1/admin/settings', methods: ['GET'])] + public function get(): JsonResponse + { + return $this->success($this->configRepo->getAll()); + } +} +``` + +### Frontend — SmsWalletPage modal (خط ۳۴۸–۳۷۳) +```tsx +// همیشه دو گزینه mellat و sep نمایش می‌دهد +{(['mellat', 'sep'] as const).map((gw) => ( +
setGateway(gw)} ...> +
{gw === 'mellat' ? 'ملت' : 'سپ'}
+ ... +
+))} +``` + +### Frontend — SubscriptionPage modal (خط ۳۰۷–۳۲۷) +```tsx +// همیشه دو گزینه mellat و sep نمایش می‌دهد +{(['mellat', 'sep'] as const).map((gw) => ( + +))} +``` + +--- + +## وظایف + +### ۱. Backend — endpoint عمومی `GET /api/v1/payment/config` + +در `src/Payment/Controller/PaymentController.php` یک متد جدید اضافه کن: + +```php +#[Route('/api/v1/payment/config', methods: ['GET'])] +public function config(): JsonResponse +{ + return $this->success([ + 'test_mode' => $this->configRepo->get('payment_test_mode') === '1', + ]); +} +``` + +- **Permission:** `IS_AUTHENTICATED_FULLY` (کلاس `PaymentController` قبلاً این را دارد) +- **مهم:** هرگز credential های درگاه (terminal_id، password، ...) را expose نکن +- پاسخ فقط یک boolean کافی است +- مستندات را در `docs/api/payment.md` اضافه کن + +### ۲. Frontend — hook برای خواندن حالت تست + +در `assets/admin/pages/SmsWalletPage.tsx` و `assets/admin/pages/SubscriptionPage.tsx`، یک `useQuery` برای خواندن config اضافه کن: + +```tsx +const { data: paymentConfigData } = useQuery>({ + queryKey: ['payment-config'], + queryFn: () => api.get('/api/v1/payment/config'), + staleTime: 5 * 60 * 1000, // 5 دقیقه cache +}); + +const isTestMode = paymentConfigData?.data?.test_mode ?? false; +``` + +### ۳. Frontend — SmsWalletPage: پنهان کردن انتخاب‌کننده درگاه در حالت تست + +در modal شارژ (`SmsWalletPage.tsx`)، قسمت «انتخاب درگاه پرداخت» را با شرط جایگزین کن: + +```tsx +{isTestMode ? ( +
+ ⚠️ +
+
+ درگاه آزمایشی فعال است +
+
+ پول واقعی کسر نخواهد شد — این تراکنش آزمایشی است +
+
+
+) : ( + // کد فعلی انتخاب درگاه (mellat / sep) +
...
+)} +``` + +همچنین در mutation، وقتی `isTestMode` است، مقدار `gateway` را به `'mock'` تنظیم کن (یا هر رشته‌ای — backend به هر حال Mock را استفاده می‌کند، ولی باید یک مقدار ارسال کنیم). + +### ۴. Frontend — SubscriptionPage: پنهان کردن انتخاب‌کننده درگاه در حالت تست + +همان pattern را در `SubscriptionPage.tsx` در قسمت «انتخاب درگاه پرداخت» داخل modal اعمال کن. + +همچنین در دکمه پرداخت زیر modal، متن را در حالت تست تغییر بده: +```tsx +{isTestMode + ? `پرداخت آزمایشی ${formatRial(purchaseTarget.period.price_rials)}` + : `پرداخت ${formatRial(purchaseTarget.period.price_rials)}` +} +``` + +--- + +## نکات مهم + +- **Backend قبلاً درست است:** `resolveGateway()` در هر دو `PaymentController` و `SmsWalletController` حالت تست را بررسی می‌کند. فقط UI نیاز به تغییر دارد. +- **Security:** endpoint جدید فقط یک boolean برمی‌گرداند — هیچ اطلاعات حساسی expose نمی‌شود. +- **قرارداد API:** پاسخ باید `{ success: true, data: { test_mode: boolean } }` باشد. در frontend: `data?.data?.test_mode`. +- **در حالت تست:** state انتخاب درگاه (`gateway` / `selectedGateway`) بی‌تأثیر است چون backend آن را نادیده می‌گیرد — ولی باید یک مقدار ارسال شود (از مقدار default `'mellat'` استفاده کن). +- **Stale time:** این تنظیم خیلی نادر تغییر می‌کند، 5 دقیقه staleTime کافی است. +- **Routing ترتیب:** مطمئن شو route `/api/v1/payment/config` قبل از `/api/v1/payment/{uuid}` تعریف شده تا با UUID conflict نکند. در Symfony اگر method‌ها مختلف است مشکلی نیست ولی اگر هر دو GET هستند، route خاص‌تر باید اول باشد. + +--- + +## ترتیب اجرا + +1. Backend: اضافه کردن `config()` به `PaymentController` +2. تست route: `ddev exec php bin/console debug:router | grep payment` +3. Cache: `ddev exec php bin/console cache:clear` +4. Frontend: `SmsWalletPage.tsx` — اضافه کردن query و شرط حالت تست +5. Frontend: `SubscriptionPage.tsx` — همان تغییر +6. Build: `ddev exec yarn dev` +7. TypeScript check: `ddev exec npx tsc --noEmit --project tsconfig.json 2>&1 | head -20` +8. مستندات: `docs/api/payment.md` diff --git a/assets/admin/pages/SmsWalletPage.tsx b/assets/admin/pages/SmsWalletPage.tsx index b79e36a1..8686bf4c 100644 --- a/assets/admin/pages/SmsWalletPage.tsx +++ b/assets/admin/pages/SmsWalletPage.tsx @@ -43,10 +43,17 @@ export default function SmsWalletPage() { queryFn: () => api.get('/api/v1/sms/settings'), }); - const balance = balanceData?.data; - const logs = logsData?.data ?? EMPTY_LOGS; - const total = logsData?.meta?.totalRecords ?? 0; - const settings = settingsData?.data; + const { data: paymentConfigData } = useQuery>({ + queryKey: ['payment-config'], + queryFn: () => api.get('/api/v1/payment/config'), + staleTime: 5 * 60 * 1000, + }); + + const balance = balanceData?.data; + const logs = logsData?.data ?? EMPTY_LOGS; + const total = logsData?.meta?.totalRecords ?? 0; + const settings = settingsData?.data; + const isTestMode = paymentConfigData?.data?.test_mode ?? false; const chargeForm = useForm({ resolver: zodResolver(chargeSchema) }); const watchAmount = chargeForm.watch('amount_rials'); @@ -345,32 +352,48 @@ export default function SmsWalletPage() { setChargeOpen(false)} title="شارژ کیف پول پیامک">
chargeMutation.mutate(d))}>
- {/* انتخاب درگاه — radio cards */} -
-
انتخاب درگاه پرداخت
-
- {(['mellat', 'sep'] as const).map((gw) => ( -
setGateway(gw)} - style={{ - flex: 1, textAlign: 'center', padding: '12px', - border: `2px solid ${gateway === gw ? 'var(--primary)' : 'var(--border)'}`, - borderRadius: 10, cursor: 'pointer', - background: gateway === gw ? 'var(--primary-subtle)' : 'transparent', - transition: 'all 0.15s', - }} - > -
- {gw === 'mellat' ? 'ملت' : 'سپ'} -
-
- {gw === 'mellat' ? 'بانک ملت' : 'بانک صادرات'} -
-
- ))} + {/* انتخاب درگاه */} + {isTestMode ? ( +
+ ⚠️ +
+
درگاه آزمایشی فعال است
+
پول واقعی کسر نخواهد شد — این تراکنش آزمایشی است
+
-
+ ) : ( +
+
انتخاب درگاه پرداخت
+
+ {(['mellat', 'sep'] as const).map((gw) => ( +
setGateway(gw)} + style={{ + flex: 1, textAlign: 'center', padding: '12px', + border: `2px solid ${gateway === gw ? 'var(--primary)' : 'var(--border)'}`, + borderRadius: 10, cursor: 'pointer', + background: gateway === gw ? 'var(--primary-subtle)' : 'transparent', + transition: 'all 0.15s', + }} + > +
+ {gw === 'mellat' ? 'ملت' : 'سپ'} +
+
+ {gw === 'mellat' ? 'بانک ملت' : 'بانک صادرات'} +
+
+ ))} +
+
+ )}
@@ -386,12 +409,15 @@ export default function SmsWalletPage() { {watchAmount && Number(watchAmount) >= 10000 && (
- پرداخت {formatRial(Number(watchAmount))} از طریق {gateway === 'mellat' ? 'بانک ملت' : 'سپ'} + {isTestMode + ? `پرداخت آزمایشی ${formatRial(Number(watchAmount))}` + : `پرداخت ${formatRial(Number(watchAmount))} از طریق ${gateway === 'mellat' ? 'بانک ملت' : 'سپ'}` + }
)} diff --git a/assets/admin/pages/SubscriptionPage.tsx b/assets/admin/pages/SubscriptionPage.tsx index d35ac298..608c5166 100644 --- a/assets/admin/pages/SubscriptionPage.tsx +++ b/assets/admin/pages/SubscriptionPage.tsx @@ -60,10 +60,17 @@ export default function SubscriptionPage() { queryFn: () => api.get('/api/v1/subscription/my'), }); - const plans = plansData?.data ?? []; - const myRaw = myData?.data; - const my = myRaw?.subscription ?? null; - const usedTrial = myRaw?.used_trial ?? false; + const { data: paymentConfigData } = useQuery>({ + queryKey: ['payment-config'], + queryFn: () => api.get('/api/v1/payment/config'), + staleTime: 5 * 60 * 1000, + }); + + const plans = plansData?.data ?? []; + const myRaw = myData?.data; + const my = myRaw?.subscription ?? null; + const usedTrial = myRaw?.used_trial ?? false; + const isTestMode = paymentConfigData?.data?.test_mode ?? false; const trialMutation = useMutation({ mutationFn: () => api.post('/api/v1/subscription/trial', {}), @@ -300,31 +307,47 @@ export default function SubscriptionPage() {
{/* انتخاب درگاه */} -
-
- انتخاب درگاه پرداخت + {isTestMode ? ( +
+ ⚠️ +
+
درگاه آزمایشی فعال است
+
پول واقعی کسر نخواهد شد — این تراکنش آزمایشی است
+
-
- {(['mellat', 'sep'] as const).map((gw) => ( - - ))} + ) : ( +
+
+ انتخاب درگاه پرداخت +
+
+ {(['mellat', 'sep'] as const).map((gw) => ( + + ))} +
-
+ )} {/* دکمه‌ها */}
@@ -338,7 +361,9 @@ export default function SubscriptionPage() { > {purchaseMutation.isPending ? 'در حال انتقال...' - : `پرداخت ${formatRial(purchaseTarget.period.price_rials)}` + : isTestMode + ? `پرداخت آزمایشی ${formatRial(purchaseTarget.period.price_rials)}` + : `پرداخت ${formatRial(purchaseTarget.period.price_rials)}` }