- کاربر بتواند علاوه بر واریز دستی (کارت/شبا)، از **پرداخت آنلاین** کیف را شارژ کند.
- تمام credentialها و انتخاب درگاه **فقط سمت سرور** بماند.
- معماری طوری باشد که بعداً چند نوع درگاه (Montify پارتنر، درگاه مستقیم، …) اضافه شود.
---
## ۲. جریان کلی (از دید کاربر و سیستم)
```
اپ کاربر (khaneghi_user)
│ مبلغ را وارد میکند → مدال راهنمای شارژ
│ GET /customer/wallet/payment/options → { online_enabled }
│ POST /customer/wallet/payment/initiate → { payment_url }
▼
سرور ما
│ Withdraw (sts=1) میسازد
│ درگاه فعال را از Settings انتخاب میکند (مثلاً montify)
│ با HMAC به Montify: POST /external-payment/initiate
▼
Montify → payment_url → درگاه بانکی
│
├─► Browser return → GET /customer/wallet/payment/return/{provider}
└─► Webhook → POST /customer/wallet/payment/webhook/{provider}
│
└─► شارژ idempotent کیف (sts=2 + Transaction)
```
**منبع حقیقت شارژ:** webhook (یا verify امن سمت سرور). فقط به query برگشت مرورگر اعتماد نکنید؛ امضا را verify کنید و مبلغ/`reference_id` را با `Withdraw` تطبیق دهید.
---
## ۳. امنیت — قوانین اجباری
| درست | غلط |
|------|-----|
| `client_id` / `client_secret` / `base_url` فقط در Settings سرور | گذاشتن secret در JS فرانت |
| فیلتر `api/get_settings` برای کلیدهای محرمانه | برگرداندن `external_payment_*` به اپ کاربر |
| اپ فقط `online_enabled` و بعد `payment_url` میبیند | خواندن provider یا credential از settings در کلاینت |
| initiate فقط با JWT مشتری | ساخت لینک پرداخت در مرورگر |
| verify امضای return و webhook | اعتماد کور به query بدون signature |
| تطبیق `amount` با `Withdraw.cash` | قبول هر مبلغ برگشتی |
### ۳.۱. چه چیزی به اپ کاربر میرسد؟
-`GET /customer/wallet/payment/options` → فقط `{ "online_enabled": true|false }`
-`POST /customer/wallet/payment/initiate` → فقط `{ "payment_url": "https://..." }`
- **هیچ** `client_id`، `client_secret`، `base_url`، نام درگاه داخلی، یا `wallet_payment_provider` در Network تب اپ نباید دیده شود.
### ۳.۲. فیلتر `get_settings`
در `App\Models\Setting::isSecretOrServerOnlyKey()` و `SettingsController::authorizeSettingsPayload()`:
- درخواست عمومی (`api/get_settings` بدون auth ادمین) → کلیدهای محرمانه حذف میشوند.
- پنل ادمین (`admin/get_settings` با `auth:user`) → همه کلیدها قابل مشاهده/ویرایش طبق `can_edit`.
کلیدهای مرتبط با پرداخت که به اپ عمومی نمیروند شامل:
- همه `external_payment_*`
-`wallet_payment_provider`
- همه `payping_*`
- الگوهای `*_secret` / `*_token` / `*_password` و لیست ثابت توکنها
- فرانت **همه** پارامترهای query صفحه بازگشت را بدون فیلتر میفرستد؛ حذف حتی یک پارامتر امضا را میشکند.
- سرور امضا را با چند قالب payload میسنجد (مقادیر string، بدون مقادیر خالی، `amount`/`timestamp` عددی) تا اختلافهای جزئی پیادهسازی پارتنر باعث رد شدن پرداخت واقعی نشود. دستکاری مقادیر همچنان رد میشود.
- اگر امضا نخواند یا نتیجه هنوز قطعی نباشد، سرور `GET /external-payment/status` را از Montify میپرسد (`SupportsStatusQuery`) و در صورت `paid` کیف را idempotent شارژ میکند. redirect مرورگر هیچوقت منبع حقیقت نیست.
- مالکیت `Withdraw` قبل از هر پاسخی با کاربر لاگینشده تطبیق داده میشود.
| `امضای نامعتبر` در verify-return | `php artisan montify:check-return "<کل URL بازگشت>"` را روی سرور بزنید؛ خروجی نشان میدهد کدام قالب میخواند یا اینکه `client_secret` غلط است. لاگ `montify.return_invalid_signature` هم کلیدهای دریافتی را دارد |
| پرداخت موفق ولی کیف شارژ نشد | `GET /wallet/payment/status?reference_id=WD-…`؛ لاگ `montify.status_failed` و رسیدن webhook را بررسی کنید |