299 lines
14 KiB
Markdown
299 lines
14 KiB
Markdown
# شارژ آنلاین کیف پول (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
|
||
```
|