Files
clinicpro/.claude/prompt/sms-price-and-wallet-settings.md
T

9.5 KiB
Raw Blame History

قیمت هر پیامک در تنظیمات + رفع تنظیمات ارسال پیامک کیف‌پول

پروژه

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:

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

// src/Config/Controller/SiteConfigController.php
private const ALLOWED_KEYS = [
    // ...
    'sms_panel_fee_rials',
    'appointment_fee_rials',
    // ...
];

بخش پیامکِ SettingsPage.tsx فقط وضعیت کلید API را نشان می‌دهد (فیلد قیمت ندارد):

{current.id === 'sms' && (
  // فقط sms_api_key_configured نمایش داده می‌شود
)}

فرم تنظیماتِ SmsWalletPage.tsx — state محلی بعد از ذخیره ریست نمی‌شود:

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):
    <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 به دادهٔ تازهٔ سرور برگردد (وضعیت تأیید متن، مقادیر نرمال‌شده):

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 ساخته می‌شود).