Files
boursecu_server/docs/WALLET_ONLINE_PAYMENT.md
T

299 lines
14 KiB
Markdown
Raw Normal View History

2026-07-24 16:50:21 +03:30
# شارژ آنلاین کیف پول (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()`:
2026-07-24 16:50:21 +03:30
- درخواست عمومی (`api/get_settings` بدون auth) → فقط ردیف‌های `show_to_user=1`؛ credential حذف می‌شود.
2026-07-24 16:50:21 +03:30
- پنل ادمین (`admin/get_settings` با `auth:user`) → همه کلیدها قابل مشاهده/ویرایش طبق `can_edit`.
کلیدهای مرتبط با پرداخت که به اپ عمومی نمی‌روند شامل:
- همه `external_payment_*`
- `wallet_payment_provider`
- همه `payping_*`
- الگوهای `*_secret` / `*_token` / `*_password` و لیست ثابت توکن‌ها
---
## ۴. Settings (جدول `settings`)
2026-07-26 22:19:07 +03:30
پیکربندی از پنل ادمین → گروه **تنظیمات درگاه** (`can_edit=1`).
2026-07-24 16:50:21 +03:30
| کلید | نوع | توضیح |
|------|-----|--------|
2026-07-26 22:19:07 +03:30
| `wallet_payment_provider` | select | `none` = خاموش، `montify` = پارتنر Montify |
| `external_payment_enabled` | switch | فعال‌سازی شارژ آنلاین (همراه با پیکربندی کامل) |
2026-07-24 16:50:21 +03:30
| `external_payment_base_url` | text | BASE_URL API Montify (بدون اسلش انتهایی) |
| `external_payment_client_id` | text | `client_id` پارتنر |
2026-07-26 22:19:07 +03:30
| `external_payment_client_secret` | text | `client_secret` — محرمانه، فقط سرور |
| `external_payment_gateway` | select | `zarinpall` / `zibal` / `payping` / `sepal` |
2026-07-24 16:50:21 +03:30
| `external_payment_return_url` | text | اختیاری؛ خالی = پیش‌فرض سرور |
| `external_payment_webhook_url` | text | اختیاری؛ خالی = پیش‌فرض سرور |
2026-07-26 22:19:07 +03:30
| `min_deposit` | price | حداقل مبلغ واریز (گروه کارمزد و محدودیت‌ها) |
2026-07-24 16:50:21 +03:30
### مقادیر پیشنهادی برای abaiegold
2026-07-24 16:50:21 +03:30
| فیلد 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)
2026-07-24 16:50:21 +03:30
### Migrationها
- `2026_07_24_000001_add_external_payment_settings.php`
- `2026_07_24_000002_add_wallet_payment_provider_setting.php`
2026-07-26 22:19:07 +03:30
- `2026_07_26_000001_expose_gateway_settings_in_admin_panel.php`
2026-07-24 16:50:21 +03:30
```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` → استعلام مستقیم از درگاه |
2026-07-24 16:50:21 +03:30
| 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` قبل از هر پاسخی با کاربر لاگین‌شده تطبیق داده می‌شود.
2026-07-24 16:50:21 +03:30
### سازگاری قدیمی
| مسیر قدیمی | معادل |
|------------|--------|
| `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 را بررسی کنید |
2026-07-24 16:50:21 +03:30
جزئیات امضای 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 ← عیب‌یابی امضای بازگشت
2026-07-24 16:50:21 +03:30
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
```