feat(settings): add appointment fee configuration and update payment logic
This commit is contained in:
@@ -29,6 +29,7 @@ const schema = z.object({
|
||||
tax_enabled: z.string(),
|
||||
tax_percent: z.string(),
|
||||
sms_panel_fee_rials: z.string(),
|
||||
appointment_fee_rials: z.string(),
|
||||
// payment gateways
|
||||
payment_test_mode: z.string(),
|
||||
mellat_terminal_id: z.string(),
|
||||
@@ -55,6 +56,7 @@ interface Settings {
|
||||
tax_enabled: string;
|
||||
tax_percent: string;
|
||||
sms_panel_fee_rials: string;
|
||||
appointment_fee_rials: string;
|
||||
payment_test_mode: string;
|
||||
mellat_terminal_id: string;
|
||||
mellat_username: string;
|
||||
@@ -112,6 +114,7 @@ export default function SettingsPage() {
|
||||
tax_enabled: settings.tax_enabled ?? '0',
|
||||
tax_percent: settings.tax_percent ?? '10',
|
||||
sms_panel_fee_rials: settings.sms_panel_fee_rials ?? '1500000',
|
||||
appointment_fee_rials: settings.appointment_fee_rials ?? '150000',
|
||||
payment_test_mode: settings.payment_test_mode ?? '0',
|
||||
mellat_terminal_id: settings.mellat_terminal_id ?? '',
|
||||
mellat_username: settings.mellat_username ?? '',
|
||||
@@ -313,6 +316,16 @@ export default function SettingsPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* مبلغ هر نوبت */}
|
||||
<div style={{ maxWidth: 320 }}>
|
||||
<label style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 6 }}>مبلغ هر نوبت (ریال)</label>
|
||||
<input {...register('appointment_fee_rials')} type="number" min={0} dir="ltr" placeholder="150000"
|
||||
style={{ width: 200, height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13.5, boxSizing: 'border-box' }} />
|
||||
<p className="muted" style={{ fontSize: 12, marginTop: 6 }}>
|
||||
مبلغی که بیمار هنگام رزرو نوبت آنلاین پرداخت میکند. ۱۵۰٬۰۰۰ ریال = ۱۵٬۰۰۰ تومان.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* هزینه پنل پیامک */}
|
||||
<div style={{ maxWidth: 320 }}>
|
||||
<label style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 6 }}>هزینه ثابت پنل پیامک (ریال)</label>
|
||||
@@ -522,6 +535,7 @@ export default function SettingsPage() {
|
||||
tax_enabled: settings.tax_enabled,
|
||||
tax_percent: settings.tax_percent,
|
||||
sms_panel_fee_rials: settings.sms_panel_fee_rials,
|
||||
appointment_fee_rials: settings.appointment_fee_rials,
|
||||
payment_test_mode: settings.payment_test_mode,
|
||||
mellat_terminal_id: settings.mellat_terminal_id,
|
||||
mellat_username: settings.mellat_username,
|
||||
|
||||
@@ -981,6 +981,7 @@ Reject a pending request. **Permission:** `ROLE_ADMIN`
|
||||
| `tax_enabled` | `0` | فعالسازی مالیات بر ارزش افزوده |
|
||||
| `tax_percent` | `10` | درصد مالیات |
|
||||
| `sms_panel_fee_rials` | `1500000` | هزینه ثابت پنل پیامک به ریال (از نوبت و اشتراک کسر میشود) |
|
||||
| `appointment_fee_rials` | `150000` | مبلغ هر نوبت به ریال؛ مبلغی که بیمار هنگام رزرو آنلاین پرداخت میکند. backend از همین کلید میخواند و در `GET /api/v1/payment/config` expose میشود |
|
||||
|
||||
**ترتیب محاسبه** (در `CommissionService`): ۱) کسر `sms_panel_fee_rials` ۲) مالیاتِ استخراجی `afterSms × tax/(100+tax)` ۳) پورسانت = `netAfterTax × percent/100`. سهم نماینده به کیفپولش (`WalletTransaction` credit) واریز و یک ردیف `FinancialBreakdown` ثبت میشود (idempotent بر اساس `payment_id`).
|
||||
|
||||
|
||||
+5
-1
@@ -16,7 +16,8 @@
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"test_mode": true
|
||||
"test_mode": true,
|
||||
"appointment_fee_rials": 150000
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -24,6 +25,7 @@
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `test_mode` | boolean | `true` = درگاه آزمایشی فعال است — backend از MockGateway استفاده میکند و پول واقعی کسر نمیشود |
|
||||
| `appointment_fee_rials` | integer | مبلغ هر نوبت به ریال (از تنظیمات سایت، کلید `appointment_fee_rials`). frontend برای نمایش «مبلغ قابل پرداخت» از این میخواند؛ مبلغِ واقعیِ تراکنش هم در backend از همین کلید خوانده میشود (نه از client) |
|
||||
|
||||
**نکته:** این endpoint هیچ اطلاعات حساسی (terminal_id، password، ...) را expose نمیکند. تنها یک boolean برای مصرف frontend است.
|
||||
|
||||
@@ -105,6 +107,8 @@ Initiate payment for an appointment. Returns a redirect URL to the payment gatew
|
||||
}
|
||||
```
|
||||
|
||||
> **مبلغ نوبت:** مبلغِ پرداخت از کلید `appointment_fee_rials` تنظیمات سایت خوانده میشود (نه از client و نه hardcode). برای تغییر، در `/admin/settings` ویرایش کنید.
|
||||
>
|
||||
> **On successful callback** for an appointment payment, the booking is transitioned `pending → confirmed` (its 15-minute `expires_at` is cleared) and a confirmation SMS is dispatched to the patient's mobile. If the booking already lapsed to `expired` before payment confirmed, it is **not** re-confirmed (the transition is rejected) — handle refund out of band.
|
||||
>
|
||||
> **پورسانت نماینده:** اگر پزشک نوبت `representation_id` داشته باشد و `appointment_commission_enabled=1` باشد، پس از confirm شدن `CommissionService` هزینه پنل پیامک و مالیات را کسر و سهم نماینده را به کیفپولش واریز میکند (ردیف `FinancialBreakdown` ثبت میشود). برای پرداخت اشتراک هم اگر `upgrade_commission_enabled=1` و پزشک/کلینیک `representation_id` داشته باشد همین منطق با درصد `upgrade_commission_percent` اعمال میشود. کلیدهای تنظیمات و ترتیب محاسبه در `docs/api/admin.md`.
|
||||
|
||||
@@ -23,6 +23,7 @@ class SiteConfigController extends BaseController
|
||||
'tax_enabled',
|
||||
'tax_percent',
|
||||
'sms_panel_fee_rials',
|
||||
'appointment_fee_rials',
|
||||
'site_name',
|
||||
'support_phone',
|
||||
'max_cancel_hours_before',
|
||||
|
||||
@@ -16,7 +16,8 @@ class SiteConfigRepository extends ServiceEntityRepository
|
||||
'upgrade_commission_percent' => '20',
|
||||
'tax_enabled' => '0',
|
||||
'tax_percent' => '10',
|
||||
'sms_panel_fee_rials' => '1500000',
|
||||
'sms_panel_fee_rials' => '150',
|
||||
'appointment_fee_rials' => '15000',
|
||||
'site_name' => 'ClinicPro',
|
||||
'support_phone' => '',
|
||||
'max_cancel_hours_before' => '24',
|
||||
|
||||
@@ -186,7 +186,8 @@ class PaymentController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_PAYMENT_001, ErrorCodes::message(ErrorCodes::ERR_PAYMENT_001), 503);
|
||||
}
|
||||
|
||||
$payment = new Payment($user, 150000, $gatewayName, Payment::TYPE_APPOINTMENT, $frontendAddress);
|
||||
$feeRials = (int) $this->configRepo->get('appointment_fee_rials');
|
||||
$payment = new Payment($user, $feeRials, $gatewayName, Payment::TYPE_APPOINTMENT, $frontendAddress);
|
||||
$payment->setAppointment($appointment);
|
||||
$this->paymentRepo->save($payment);
|
||||
|
||||
@@ -519,7 +520,8 @@ class PaymentController extends BaseController
|
||||
public function config(): JsonResponse
|
||||
{
|
||||
return $this->success([
|
||||
'test_mode' => $this->configRepo->get('payment_test_mode') === '1',
|
||||
'test_mode' => $this->configRepo->get('payment_test_mode') === '1',
|
||||
'appointment_fee_rials' => (int) $this->configRepo->get('appointment_fee_rials'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user