# شارژ آنلاین کیف پول (Wallet Online Payment) مستند پیاده‌سازی داخلی پروژه برای شارژ کیف پول از طریق درگاه. برای پروتکل Montify (HMAC، initiate، webhook) به سند جداگانه مراجعه کنید: → [`EXTERNAL_PAYMENT_PARTNER.md`](./EXTERNAL_PAYMENT_PARTNER.md) --- ## ۱. هدف - کاربر بتواند علاوه بر واریز دستی (کارت/شبا)، از **پرداخت آنلاین** کیف را شارژ کند. - تمام 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()` و `Setting::forClientApi()`: - درخواست عمومی (`api/get_settings` بدون auth) → فقط ردیف‌های `show_to_user=1`؛ credential حذف می‌شود. - پنل ادمین (`admin/get_settings` با `auth:user`) → همه کلیدها قابل مشاهده/ویرایش طبق `can_edit`. کلیدهای مرتبط با پرداخت که به اپ عمومی نمی‌روند شامل: - همه `external_payment_*` - `wallet_payment_provider` - همه `payping_*` - الگوهای `*_secret` / `*_token` / `*_password` و لیست ثابت توکن‌ها --- ## ۴. Settings (جدول `settings`) پیکربندی از پنل ادمین → گروه **تنظیمات درگاه** (`can_edit=1`). | کلید | نوع | توضیح | |------|-----|--------| | `wallet_payment_provider` | select | `none` = خاموش، `montify` = پارتنر Montify | | `external_payment_enabled` | switch | فعال‌سازی شارژ آنلاین (همراه با پیکربندی کامل) | | `external_payment_base_url` | text | BASE_URL API Montify (بدون اسلش انتهایی) | | `external_payment_client_id` | text | `client_id` پارتنر | | `external_payment_client_secret` | text | `client_secret` — محرمانه، فقط سرور | | `external_payment_gateway` | select | `zarinpall` / `zibal` / `payping` / `sepal` | | `external_payment_return_url` | text | اختیاری؛ خالی = پیش‌فرض سرور | | `external_payment_webhook_url` | text | اختیاری؛ خالی = پیش‌فرض سرور | | `min_deposit` | price | حداقل مبلغ واریز (گروه کارمزد و محدودیت‌ها) | ### مقادیر پیشنهادی برای abaiegold | فیلد Settings | مقدار | |---------------|--------| | `external_payment_return_url` | `https://abaeigold.ir/wallet/payment-result` | | `external_payment_webhook_url` | `https://abaeigold.ir/server/customer/wallet/payment/webhook/montify` | در whitelist پارتنر Montify حداقل این پیشوندها را ثبت کنید: - Return: `https://abaeigold.ir/wallet/payment-result` یا `https://abaeigold.ir/wallet/` - Webhook: همان URL کامل webhook بالا جریان برگشت فرانت: 1. Montify کاربر را به `return_url` با query امضاشده می‌فرستد 2. صفحه `khaneghi_user` → `/wallet/payment-result` 3. فرانت `POST /customer/wallet/payment/verify-return` می‌زند (امضا فقط سمت سرور verify می‌شود) 4. webhook هم موازی برای شارژ قطعی به بک‌اند می‌آید (idempotent) ### Migrationها - `2026_07_24_000001_add_external_payment_settings.php` - `2026_07_24_000002_add_wallet_payment_provider_setting.php` - `2026_07_26_000001_expose_gateway_settings_in_admin_panel.php` ```bash php artisan migrate ``` --- ## ۵. APIهای سرور ما پیشوند: `/customer` Auth مشتری: `auth:customer` + `customer.active` (به‌جز return/webhook) | متد | مسیر | Auth | نقش | |-----|------|------|-----| | GET | `/wallet/payment/options` | مشتری | `{ online_enabled }` | | POST | `/wallet/payment/initiate` | مشتری | Body: `{ value }` (ریال) → `{ payment_url }` | | POST | `/wallet/payment/verify-return` | مشتری | verify امضای بازگشت + شارژ | | GET | `/wallet/payment/status` | مشتری | `?reference_id=WD-123` → استعلام مستقیم از درگاه | | GET | `/wallet/payment/return/{provider}` | عمومی | برگشت مرورگر؛ verify + گزارش | | POST | `/wallet/payment/webhook/{provider}` | عمومی (HMAC) | شارژ کیف | ### verify-return و status - فرانت **همه** پارامترهای query صفحه بازگشت را بدون فیلتر می‌فرستد؛ حذف حتی یک پارامتر امضا را می‌شکند. - سرور امضا را با چند قالب payload می‌سنجد (مقادیر string، بدون مقادیر خالی، `amount`/`timestamp` عددی) تا اختلاف‌های جزئی پیاده‌سازی پارتنر باعث رد شدن پرداخت واقعی نشود. دستکاری مقادیر همچنان رد می‌شود. - اگر امضا نخواند یا نتیجه هنوز قطعی نباشد، سرور `GET /external-payment/status` را از Montify می‌پرسد (`SupportsStatusQuery`) و در صورت `paid` کیف را idempotent شارژ می‌کند. redirect مرورگر هیچ‌وقت منبع حقیقت نیست. - مالکیت `Withdraw` قبل از هر پاسخی با کاربر لاگین‌شده تطبیق داده می‌شود. ### سازگاری قدیمی | مسیر قدیمی | معادل | |------------|--------| | `POST /wallet/external-payment/initiate` | همان `initiate` | | `GET /wallet/external-payment/return` | `return/montify` | | `POST /wallet/external-payment/webhook` | `webhook/montify` | ### Initiate — جزئیات 1. بررسی `WalletPaymentManager::isOnlineDepositAvailable()` 2. اعتبارسنجی مبلغ (≥ ۱۰۰۰ و ≥ `min_deposit`) 3. ساخت `Withdraw` با `sts=1`، `note=واریز آنلاین` 4. `reference_id = WD-{withdraw_id}` 5. فراخوانی `gateway->initiate(...)` 6. ذخیره `session_id` در `tracked_number` در صورت وجود 7. پاسخ فقط `payment_url` ### Webhook / Return — شارژ منطق مشترک در `App\Services\Payment\WalletDepositCreditor`: - قفل ردیف `Withdraw` (idempotent) - اگر قبلاً `sts=2` باشد، دوباره شارژ نمی‌کند - `TransactionInterface` نوع واریز نقدی - همگام‌سازی حسابداری (Pargar) در صورت امکان - SMS ادمین با سوییچ `sms_notif_gateway_charge_admin` --- ## ۶. معماری کد (چنددرگاهی) ``` app/Services/Payment/ Contracts/ WalletPaymentGatewayInterface.php # قرارداد درگاه Gateways/ MontifyPartnerGateway.php # پیاده‌سازی فعلی Montify # PaypingDirectGateway.php # آینده Support/ MontifyHmacSigner.php # HMAC + stableStringify WalletPaymentManager.php # انتخاب درگاه از Settings WalletDepositCreditor.php # شارژ idempotent کیف app/Http/Controllers/pay/ WalletPaymentController.php # options / initiate / return / webhook ``` ### قرارداد درگاه هر درگاه باید این‌ها را پیاده کند: - `key(): string` — مثلاً `montify` - `isAvailable(): bool` — فعال + پیکربندی کامل - `initiate(array $payload): array` — حداقل `payment_url` - `returnUrl()` / `webhookUrl()` - `handleReturn(Request): array` - `handleWebhook(Request): array` ### افزودن درگاه جدید (چک‌لیست) 1. کلاس جدید در `Gateways/` با `WalletPaymentGatewayInterface` 2. ثبت در `WalletPaymentManager::drivers()` 3. افزودن مقدار به optionsی `wallet_payment_provider` در migration/admin (`settingTypes.ts`) 4. Settings مخصوص آن درگاه (و اضافه کردن به فیلتر secret در `Setting::isSecretOrServerOnlyKey`) 5. مسیرهای `return/{provider}` و `webhook/{provider}` از قبل generic هستند 6. تست: options → initiate → webhook → موجودی کیف --- ## ۷. فرانت‌اند (khaneghi_user) | فایل | نقش | |------|-----| | `src/api/wallet.ts` | `getWalletPaymentOptions` / `initiateWalletPayment` | | `src/views/base/wallet/composables/useWalletPage.ts` | بارگذاری `online_enabled`، `payOnline()` | | `src/views/base/wallet/components/WalletDepositGuideModal.vue` | دکمه «پرداخت آنلاین از درگاه» | | `src/views/base/wallet/index.vue` | اتصال props/events مدال | رفتار UI: 1. کاربر مبلغ را وارد می‌کند و واریز می‌زند → مدال راهنما (کارت/شبا + پشتیبانی) 2. اگر `online_enabled` باشد دکمه درگاه نمایش داده می‌شود 3. کلیک → `initiate` → `window.location = payment_url` **نکته:** وضعیت دکمه از endpoint سرور می‌آید، نه از `settingsStore` و کلیدهای `external_payment_*`. --- ## ۸. پنل ادمین - گروه تنظیمات: `admin/.../configuration/settingGroups.ts` → id `external_payment` - انواع/برچسب/قفل: `settingTypes.ts` - ویرایش credentialها فقط در ادمین (و کلیدهای `can_edit=0` فقط دیپلوی) --- ## ۹. مدل داده مرتبط | مدل | نقش | |-----|-----| | `Withdraw` | درخواست واریز؛ `sts`: 1=در انتظار، 2=موفق، 3=ناموفق | | `reference_id` | `WD-{id}` برای Montify | | Wallet `TransactionInterface` | واریز نقدی پس از موفقیت | واریز دستی تغییری نکرده؛ فقط راهنما + کپی کارت/شبا از Settings عمومی (`deposit_card_number` و …) است. --- ## ۱۰. راه‌اندازی عملیاتی 1. `php artisan migrate` 2. در پنل ادمین / دیپلوی: - `wallet_payment_provider` = `montify` - `external_payment_base_url` / `client_id` / `client_secret` / `gateway` 3. در پنل Montify: - ثبت `allowed_return_urls` و `webhook_url` مطابق URLهای سرور 4. تست با مبلغ ≥ `min_deposit` 5. تأیید: Network اپ کاربر فاقد secret باشد؛ فقط `options` و `initiate` دیده شود --- ## ۱۱. عیب‌یابی سریع | مشکل | بررسی | |------|--------| | دکمه درگاه نیست | `options.online_enabled`؛ provider؛ کامل بودن credential؛ `isAvailable()` | | `مشکل در ارتباط با درگاه` | ساعت سرور (NTP)، `stableStringify`، PATH دقیق، body خام همان hash | | شارژ دوبل | باید idempotent باشد (`sts=2` + lock) | | secret در Network | مسیر باید `api/get_settings` باشد نه admin؛ فیلتر `Setting::forPublicApi` | | `امضای نامعتبر` در 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 را بررسی کنید | جزئیات امضای HMAC و خطاهای Montify: [`EXTERNAL_PAYMENT_PARTNER.md`](./EXTERNAL_PAYMENT_PARTNER.md) --- ## ۱۲. ارجاع فایل‌های کلیدی ``` server/docs/WALLET_ONLINE_PAYMENT.md ← این سند server/docs/EXTERNAL_PAYMENT_PARTNER.md ← پروتکل Montify server/app/Services/Payment/... server/app/Http/Controllers/pay/WalletPaymentController.php server/app/Console/Commands/MontifyCheckReturnCommand.php ← عیب‌یابی امضای بازگشت server/routes/customer.php server/app/Models/Setting.php ← فیلتر secret server/database/migrations/2026_07_24_000001_add_external_payment_settings.php server/database/migrations/2026_07_24_000002_add_wallet_payment_provider_setting.php khaneghi_user/src/api/wallet.ts khaneghi_user/src/views/base/wallet/... admin/src/pages/menuPages/configuration/settingTypes.ts admin/src/pages/menuPages/configuration/settingGroups.ts ```