Files
boursecu_server/docs/WALLET_ONLINE_PAYMENT.md
T

299 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# شارژ آنلاین کیف پول (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
```