14 KiB
شارژ آنلاین کیف پول (Wallet Online Payment)
مستند پیادهسازی داخلی پروژه برای شارژ کیف پول از طریق درگاه. برای پروتکل Montify (HMAC، initiate، webhook) به سند جداگانه مراجعه کنید:
۱. هدف
- کاربر بتواند علاوه بر واریز دستی (کارت/شبا)، از پرداخت آنلاین کیف را شارژ کند.
- تمام 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 بالا
جریان برگشت فرانت:
- Montify کاربر را به
return_urlبا query امضاشده میفرستد - صفحه
khaneghi_user→/wallet/payment-result - فرانت
POST /customer/wallet/payment/verify-returnمیزند (امضا فقط سمت سرور verify میشود) - webhook هم موازی برای شارژ قطعی به بکاند میآید (idempotent)
Migrationها
2026_07_24_000001_add_external_payment_settings.php2026_07_24_000002_add_wallet_payment_provider_setting.php2026_07_26_000001_expose_gateway_settings_in_admin_panel.php
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 — جزئیات
- بررسی
WalletPaymentManager::isOnlineDepositAvailable() - اعتبارسنجی مبلغ (≥ ۱۰۰۰ و ≥
min_deposit) - ساخت
Withdrawباsts=1،note=واریز آنلاین reference_id = WD-{withdraw_id}- فراخوانی
gateway->initiate(...) - ذخیره
session_idدرtracked_numberدر صورت وجود - پاسخ فقط
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— مثلاًmontifyisAvailable(): bool— فعال + پیکربندی کاملinitiate(array $payload): array— حداقلpayment_urlreturnUrl()/webhookUrl()handleReturn(Request): arrayhandleWebhook(Request): array
افزودن درگاه جدید (چکلیست)
- کلاس جدید در
Gateways/باWalletPaymentGatewayInterface - ثبت در
WalletPaymentManager::drivers() - افزودن مقدار به optionsی
wallet_payment_providerدر migration/admin (settingTypes.ts) - Settings مخصوص آن درگاه (و اضافه کردن به فیلتر secret در
Setting::isSecretOrServerOnlyKey) - مسیرهای
return/{provider}وwebhook/{provider}از قبل generic هستند - تست: 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:
- کاربر مبلغ را وارد میکند و واریز میزند → مدال راهنما (کارت/شبا + پشتیبانی)
- اگر
online_enabledباشد دکمه درگاه نمایش داده میشود - کلیک →
initiate→window.location = payment_url
نکته: وضعیت دکمه از endpoint سرور میآید، نه از settingsStore و کلیدهای external_payment_*.
۸. پنل ادمین
- گروه تنظیمات:
admin/.../configuration/settingGroups.ts→ idexternal_payment - انواع/برچسب/قفل:
settingTypes.ts - ویرایش credentialها فقط در ادمین (و کلیدهای
can_edit=0فقط دیپلوی)
۹. مدل داده مرتبط
| مدل | نقش |
|---|---|
Withdraw |
درخواست واریز؛ sts: 1=در انتظار، 2=موفق، 3=ناموفق |
reference_id |
WD-{id} برای Montify |
Wallet TransactionInterface |
واریز نقدی پس از موفقیت |
واریز دستی تغییری نکرده؛ فقط راهنما + کپی کارت/شبا از Settings عمومی (deposit_card_number و …) است.
۱۰. راهاندازی عملیاتی
php artisan migrate- در پنل ادمین / دیپلوی:
wallet_payment_provider=montifyexternal_payment_base_url/client_id/client_secret/gateway
- در پنل Montify:
- ثبت
allowed_return_urlsوwebhook_urlمطابق URLهای سرور
- ثبت
- تست با مبلغ ≥
min_deposit - تأیید: 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
۱۲. ارجاع فایلهای کلیدی
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