feat(payment): unify payment flow with new pure redirect entry and update related endpoints
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
# قیمت هر پیامک در تنظیمات + رفع تنظیمات ارسال پیامک کیفپول
|
||||
|
||||
## پروژه
|
||||
|
||||
`clinicpro` (Backend + Admin SPA).
|
||||
|
||||
## زمینه
|
||||
|
||||
دو مشکل در بخش پیامک پنل ادمین:
|
||||
|
||||
1. **`/admin/settings` → بخش «پیامک»**: امکان تعیین «هزینه هر پیامک» وجود ندارد (قبلاً بود). قیمت هر پیامک اکنون در بکاند **هاردکد** است: `SmsWalletController::SMS_PRICE_RIALS = 500`. باید به یک تنظیمِ قابلویرایش در همین صفحه تبدیل شود.
|
||||
2. **`/admin/sms-wallet` → «تنظیمات ارسال پیامک»**: درست کار نمیکند — بعد از ذخیره، وضعیت واقعی سرور (بهویژه وضعیت تأیید متن پیامک بعد از ویزیت) در UI منعکس نمیشود، چون state محلی بعد از ذخیره/رفچ ریست نمیشود.
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
- افزودن کلید تنظیم `sms_price_rials` (قیمت هر پیامک) که در `/admin/settings` بخش پیامک قابل ویرایش باشد و بکاند بهجای مقدار هاردکد از آن استفاده کند.
|
||||
- رفع باگِ state کهنه در فرم تنظیمات ارسالِ `/admin/sms-wallet`.
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `src/Config/Controller/SiteConfigController.php` | `ALLOWED_KEYS` تنظیمات سایت (باید `sms_price_rials` اضافه شود) |
|
||||
| `src/Sms/Controller/SmsWalletController.php` | استفاده از قیمت پیامک (هاردکد) + `resolveEntity` + endpoint تنظیمات |
|
||||
| `assets/admin/pages/SettingsPage.tsx` | فرم تنظیمات؛ بخش `sms` |
|
||||
| `assets/admin/pages/SmsWalletPage.tsx` | فرم «تنظیمات ارسال پیامک» (state محلی) |
|
||||
| `docs/api/sms.md` | مستندات (بهروزرسانی) |
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
قیمت هاردکد و balance:
|
||||
|
||||
```php
|
||||
// src/Sms/Controller/SmsWalletController.php
|
||||
private const SMS_PRICE_RIALS = 500;
|
||||
// ...
|
||||
$smsPriceRials = self::SMS_PRICE_RIALS;
|
||||
$estimatedSms = (int) floor($balanceRials / $smsPriceRials);
|
||||
return $this->success([
|
||||
'balance_rials' => $balanceRials,
|
||||
'sms_price_rials' => $smsPriceRials,
|
||||
'estimated_sms_count' => $estimatedSms,
|
||||
]);
|
||||
```
|
||||
|
||||
کلیدهای مجاز تنظیمات (بدون `sms_price_rials`):
|
||||
|
||||
```php
|
||||
// src/Config/Controller/SiteConfigController.php
|
||||
private const ALLOWED_KEYS = [
|
||||
// ...
|
||||
'sms_panel_fee_rials',
|
||||
'appointment_fee_rials',
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
بخش پیامکِ `SettingsPage.tsx` فقط وضعیت کلید API را نشان میدهد (فیلد قیمت ندارد):
|
||||
|
||||
```tsx
|
||||
{current.id === 'sms' && (
|
||||
// فقط sms_api_key_configured نمایش داده میشود
|
||||
)}
|
||||
```
|
||||
|
||||
فرم تنظیماتِ `SmsWalletPage.tsx` — state محلی بعد از ذخیره ریست نمیشود:
|
||||
|
||||
```tsx
|
||||
const [localSettings, setLocalSettings] = useState<SmsSettings | null>(null);
|
||||
const currentSettings = localSettings ?? settings;
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (body: SmsSettings) => api.patch('/api/v1/sms/settings', body),
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['sms-settings'] }); toast.success('تنظیمات ذخیره شد'); },
|
||||
// ❌ localSettings ریست نمیشود → currentSettings همان نسخهٔ ویرایششده میماند،
|
||||
// دادهی تازهٔ سرور (مثل post_visit_text_status = 'pending') نمایش داده نمیشود
|
||||
});
|
||||
```
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. افزودن کلید `sms_price_rials` به تنظیمات سایت (Backend)
|
||||
|
||||
در `SiteConfigController::ALLOWED_KEYS` کلید `'sms_price_rials'` را اضافه کن (کنار `sms_panel_fee_rials`). با این کار GET/PATCH `/api/v1/admin/settings` این کلید را برمیگرداند/ذخیره میکند.
|
||||
|
||||
### ۲. استفادهٔ بکاند از قیمتِ قابلتنظیم بهجای هاردکد
|
||||
|
||||
در `SmsWalletController`:
|
||||
- `SiteConfigRepository` را به constructor تزریق کن (اگر نیست).
|
||||
- یک helper خصوصی بساز: `private function smsPriceRials(): int { return max(1, (int) ($this->configRepo->get('sms_price_rials') ?: self::SMS_PRICE_RIALS)); }` و `SMS_PRICE_RIALS = 500` را بهعنوان **fallback** نگهدار.
|
||||
- در `balance()` بهجای `self::SMS_PRICE_RIALS` از `$this->smsPriceRials()` استفاده کن.
|
||||
- **جستجو کن** آیا جای دیگری قیمت هر پیامک برای کسر از کیفپول هنگام ارسال استفاده میشود (مثلاً `SmsWalletService` یا مسیر ارسال پیامک). اگر بله، همانجا هم از `sms_price_rials` (config) استفاده شود تا کسر و «تعداد تخمینی» همخوان باشند. اگر جایی مقدار ثابت دیگری هست، آن را هم به config متصل کن.
|
||||
|
||||
### ۳. فیلد «هزینه هر پیامک» در `SettingsPage.tsx` (Admin)
|
||||
|
||||
- به `FormValues` schema و `defaultValues` کلید `sms_price_rials` را اضافه کن (مثل `sms_panel_fee_rials`؛ نوع string، پیشفرض مثلاً `'500'`).
|
||||
- در بخش `current.id === 'sms'` یک `Field` با `input type="number"` برای `sms_price_rials` اضافه کن (الگوی دقیقاً مشابه فیلد `sms_panel_fee_rials` در بخش financial):
|
||||
```tsx
|
||||
<Field label="هزینه هر پیامک" hint="مبلغ کسرشده از کیف پول به ازای هر پیامک ارسالی (ریال).">
|
||||
<input {...register('sms_price_rials')} type="number" min={0} dir="ltr" className="input" style={{ maxWidth: 200 }} placeholder="500" />
|
||||
</Field>
|
||||
```
|
||||
- مطمئن شو مقدار اولیه از `data` (پاسخ GET `/api/v1/admin/settings`) خوانده و در PATCH ارسال میشود (چون کل `FormValues` ارسال میشود، با افزودن به schema/defaults خودکار انجام میشود).
|
||||
|
||||
### ۴. رفع state کهنهٔ فرم تنظیمات در `SmsWalletPage.tsx`
|
||||
|
||||
بعد از ذخیرهٔ موفق، `localSettings` را ریست کن تا `currentSettings` به دادهٔ تازهٔ سرور برگردد (وضعیت تأیید متن، مقادیر نرمالشده):
|
||||
|
||||
```tsx
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (body: SmsSettings) => api.patch('/api/v1/sms/settings', body),
|
||||
onSuccess: () => {
|
||||
setLocalSettings(null); // ← افزوده شود
|
||||
qc.invalidateQueries({ queryKey: ['sms-settings'] });
|
||||
toast.success('تنظیمات ذخیره شد');
|
||||
},
|
||||
onError: (e: any) => toast.error(e.message),
|
||||
});
|
||||
```
|
||||
|
||||
- همچنین اگر لازم است که با هر بار تازهشدن `settingsData` هم state محلی صفر شود (برای جلوگیری از ماندگاری ویرایشهای ذخیرهنشده پس از رفچ)، یک `useEffect(() => { setLocalSettings(null); }, [settingsData])` اضافه کن.
|
||||
- در بدنهٔ PATCH فقط فیلدهای موردنیاز فرستاده شوند (`reminder_enabled`, `reminder_hours_before`, `post_visit_enabled`, `post_visit_text`)؛ backend بقیه را نادیده میگیرد ولی برای تمیزی میتوان همینها را صریح فرستاد.
|
||||
|
||||
### ۵. دسترسی صفحهٔ کیفپول برای کاربرانِ بدون entity (بررسی و تصمیم)
|
||||
|
||||
`SmsWalletController::resolveEntity` فقط برای `ROLE_DOCTOR` و `ROLE_CLINIC` entity برمیگرداند؛ برای بقیه (از جمله ادمینِ بدون پروفایل، secretary، clinic_doctor) `['unknown', null]` → همهٔ endpointها `403 پروفایل یافت نشد` میدهند و صفحه «کار نمیکند».
|
||||
|
||||
- اگر صفحهٔ `/admin/sms-wallet` باید برای نقشهای دیگر (مثل `clinic_doctor`) هم کار کند، `resolveEntity` را گسترش بده تا آن نقشها را هم به doctor/clinic نگاشت کند.
|
||||
- اگر کیفپول فقط برای doctor/clinic معنا دارد، در `SmsWalletPage.tsx` هنگام خطای 403 یک empty-state مناسب («کیف پول پیامک فقط برای پزشک/کلینیک فعال است») نمایش بده تا صفحه سفید/خراب نشود.
|
||||
- تصمیم را بر اساس نقشهای واقعی پروژه بگیر و در گزارش ذکر کن.
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- کلید تنظیم `sms_price_rials` باید همان واحد ریال باشد؛ balance→count و کسرِ هنگام ارسال باید از **یک** مقدار بخوانند تا ناسازگاری نشود.
|
||||
- `SMS_PRICE_RIALS = 500` بهعنوان fallback بماند (نصبهای بدون مقدار config نشکنند).
|
||||
- الگوهای موجود پنل: `Field` + `register` (React Hook Form)، پاسخها `data?.data`، تاریخها Unix.
|
||||
- بعد از تغییر Backend/endpoint: `ddev exec php -l ...`، `ddev exec php bin/console cache:clear`، `ddev exec php vendor/bin/phpstan analyse src/Sms src/Config`؛ و بعد از تغییر TS: `ddev exec npx tsc --noEmit --project tsconfig.json`.
|
||||
- طبق قانون پروژه، `docs/api/sms.md` (و در صورت لزوم `docs/api/admin.md` برای کلید تنظیم جدید) در همین session بهروز شود: افزودن `sms_price_rials` به لیست تنظیمات و توضیح استفادهٔ آن.
|
||||
- Entity تغییر نمیکند → migration لازم نیست (فقط یک ردیف در جدول تنظیمات سایت که با set ساخته میشود).
|
||||
Reference in New Issue
Block a user